From 676cc44fe2d7cfe3c0274f8766ce13d43eeb9bbd Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:44:28 +0200 Subject: [PATCH 1/8] feat: env keys behavior updated (#8348) --- .github/workflows/ui-pull-request.yml | 11 +++++++++-- ui/playwright.config.ts | 2 ++ ui/tests/helpers.ts | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ui-pull-request.yml b/.github/workflows/ui-pull-request.yml index c67014dc73..ad22630e32 100644 --- a/.github/workflows/ui-pull-request.yml +++ b/.github/workflows/ui-pull-request.yml @@ -53,16 +53,20 @@ jobs: AUTH_TRUST_HOST: true NEXTAUTH_URL: http://localhost:3000 PROWLER_API_PORT: 8080 - NEXT_PUBLIC_API_BASE_URL: http://localhost:8080/api/v1 + NEXT_PUBLIC_API_BASE_URL: ${{ secrets.API_BASE_URL || 'http://localhost:8080/api/v1' }} + E2E_USER: ${{ secrets.E2E_USER }} + E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }} steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Start needed services with docker compose + if: github.repository == 'prowler-cloud/prowler' run: | docker compose up -d api worker worker-beat - name: Wait for prowler-api to respond + if: github.repository == 'prowler-cloud/prowler' run: | echo "Waiting for prowler-api..." for i in {1..30}; do @@ -74,6 +78,7 @@ jobs: sleep 3 done - name: Run database migrations + if: github.repository == 'prowler-cloud/prowler' run: | echo "Running Django migrations..." docker compose exec -T api sh -c ' @@ -81,9 +86,11 @@ jobs: ' echo "Database migrations completed!" - name: Copy local fixtures into API container + if: github.repository == 'prowler-cloud/prowler' run: | docker cp ./api/src/backend/api/fixtures/dev/. prowler-api-1:/home/prowler/backend/api/fixtures/dev - name: Load database fixtures for e2e tests + if: github.repository == 'prowler-cloud/prowler' run: | docker compose exec -T api sh -c ' echo "Loading all fixtures from api/fixtures/dev/..." @@ -131,7 +138,7 @@ jobs: retention-days: 30 - name: Cleanup services - if: always() + if: github.repository == 'prowler-cloud/prowler' run: | echo "Shutting down services..." docker-compose down -v || true diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index a31621d537..8fd6bc0dd4 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -34,6 +34,8 @@ export default defineConfig({ AUTH_SECRET: process.env.AUTH_SECRET || "fallback-ci-secret-for-testing", AUTH_TRUST_HOST: process.env.AUTH_TRUST_HOST || "true", NEXTAUTH_URL: process.env.NEXTAUTH_URL || "http://localhost:3000", + E2E_USER: process.env.E2E_USER || "e2e@prowler.com", + E2E_PASSWORD: process.env.E2E_PASSWORD || "Thisisapassword123@", }, }, }); diff --git a/ui/tests/helpers.ts b/ui/tests/helpers.ts index e807c595fd..cb2e8d2857 100644 --- a/ui/tests/helpers.ts +++ b/ui/tests/helpers.ts @@ -13,8 +13,8 @@ export const URLS = { export const TEST_CREDENTIALS = { VALID: { - email: "e2e@prowler.com", - password: "Thisisapassword123@", + email: process.env.E2E_USER || "e2e@prowler.com", + password: process.env.E2E_PASSWORD || "Thisisapassword123@", }, INVALID: { email: "invalid@example.com", From a69d0d16c0bfdd9898e3d404fa12ada229da145e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20De=20la=20Torre=20Vico?= Date: Wed, 23 Jul 2025 11:11:04 +0200 Subject: [PATCH 2/8] fix(azure/storage): handle when Azure API set values to None (#8325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Pedro Martín Co-authored-by: Sergio Garcia --- prowler/CHANGELOG.md | 7 +++ .../azure/services/storage/storage_service.py | 43 +++++++++++++++---- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 1883fe67f9..86f91170bb 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -10,6 +10,13 @@ All notable changes to the **Prowler SDK** are documented in this file. --- +## [v5.9.3] (Prowler UNRELEASED) + +### Fixed +- Add more validations to Azure Storage models when some values are None to avoid serialization issues [(#8325)](https://github.com/prowler-cloud/prowler/pull/8325) + +--- + ## [v5.9.2] (Prowler v5.9.2) ### Fixed diff --git a/prowler/providers/azure/services/storage/storage_service.py b/prowler/providers/azure/services/storage/storage_service.py index 887fb80b7b..e93b920380 100644 --- a/prowler/providers/azure/services/storage/storage_service.py +++ b/prowler/providers/azure/services/storage/storage_service.py @@ -70,17 +70,44 @@ class Storage(AzureService): ], key_expiration_period_in_days=key_expiration_period_in_days, location=storage_account.location, - default_to_entra_authorization=getattr( - storage_account, - "default_to_o_auth_authentication", - False, + default_to_entra_authorization=( + False + if getattr( + storage_account, + "default_to_o_auth_authentication", + False, + ) + is None + else getattr( + storage_account, + "default_to_o_auth_authentication", + False, + ) ), replication_settings=replication_settings, - allow_cross_tenant_replication=getattr( - storage_account, "allow_cross_tenant_replication", True + allow_cross_tenant_replication=( + True + if getattr( + storage_account, + "allow_cross_tenant_replication", + True, + ) + is None + else getattr( + storage_account, + "allow_cross_tenant_replication", + True, + ) ), - allow_shared_key_access=getattr( - storage_account, "allow_shared_key_access", True + allow_shared_key_access=( + True + if getattr( + storage_account, "allow_shared_key_access", True + ) + is None + else getattr( + storage_account, "allow_shared_key_access", True + ) ), ) ) From 922f9d2f91eca4d86adfd84530686048574a4a2f Mon Sep 17 00:00:00 2001 From: Sergio Garcia Date: Wed, 23 Jul 2025 17:43:42 +0800 Subject: [PATCH 3/8] docs(gcp): update GCP permissions (#8350) --- docs/getting-started/requirements.md | 3 +-- docs/tutorials/gcp/authentication.md | 5 ++--- docs/tutorials/gcp/organization.md | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/getting-started/requirements.md b/docs/getting-started/requirements.md index d2be93c630..eb623f5f28 100644 --- a/docs/getting-started/requirements.md +++ b/docs/getting-started/requirements.md @@ -109,13 +109,12 @@ Prowler will follow the same credentials search as [Google authentication librar Prowler for Google Cloud needs the following permissions to be set: -- **Viewer (`roles/viewer`) IAM role**: granted at the project / folder / org level in order to scan the target projects +- **Reader (`roles/reader`) IAM role**: granted at the project / folder / org level in order to scan the target projects - **Project level settings**: you need to have at least one project with the below settings: - Identity and Access Management (IAM) API (`iam.googleapis.com`) enabled by either using the [Google Cloud API UI](https://console.cloud.google.com/apis/api/iam.googleapis.com/metrics) or by using the gcloud CLI `gcloud services enable iam.googleapis.com --project ` command - - Service Usage Consumer (`roles/serviceusage.serviceUsageConsumer`) IAM role - Set the quota project to be this project by either running `gcloud auth application-default set-quota-project ` or by setting an environment variable: `export GOOGLE_CLOUD_QUOTA_PROJECT=` diff --git a/docs/tutorials/gcp/authentication.md b/docs/tutorials/gcp/authentication.md index 971796a753..da4104041d 100644 --- a/docs/tutorials/gcp/authentication.md +++ b/docs/tutorials/gcp/authentication.md @@ -51,7 +51,7 @@ Prowler follows the same search order as [Google authentication libraries](https ???+ note The credentials must belong to a user or service account with the necessary permissions. - To ensure full access, assign the roles/viewer IAM role to the identity being used. + To ensure full access, assign the roles/reader IAM role to the identity being used. ???+ note Prowler will use the enabled Google Cloud APIs to get the information needed to perform the checks. @@ -63,13 +63,12 @@ Prowler follows the same search order as [Google authentication libraries](https Prowler for Google Cloud needs the following permissions to be set: -- **Viewer (`roles/viewer`) IAM role**: granted at the project / folder / org level in order to scan the target projects +- **Reader (`roles/reader`) IAM role**: granted at the project / folder / org level in order to scan the target projects - **Project level settings**: you need to have at least one project with the below settings: - Identity and Access Management (IAM) API (`iam.googleapis.com`) enabled by either using the [Google Cloud API UI](https://console.cloud.google.com/apis/api/iam.googleapis.com/metrics) or by using the gcloud CLI `gcloud services enable iam.googleapis.com --project ` command - - Service Usage Consumer (`roles/serviceusage.serviceUsageConsumer`) IAM role - Set the quota project to be this project by either running `gcloud auth application-default set-quota-project ` or by setting an environment variable: `export GOOGLE_CLOUD_QUOTA_PROJECT=` diff --git a/docs/tutorials/gcp/organization.md b/docs/tutorials/gcp/organization.md index 43a58aa7b9..272fd6b7c6 100644 --- a/docs/tutorials/gcp/organization.md +++ b/docs/tutorials/gcp/organization.md @@ -9,7 +9,7 @@ prowler gcp --organization-id organization-id ``` ???+ warning - Make sure that the used credentials have the role Cloud Asset Viewer (`roles/cloudasset.viewer`) or Cloud Asset Owner (`roles/cloudasset.owner`) on the organization level. + Make sure that the used credentials have a role with the `cloudasset.assets.listResource` permission on the organization level like `roles/cloudasset.viewer` (Cloud Asset Viewer) or `roles/cloudasset.owner` (Cloud Asset Owner). ???+ note With this option, Prowler retrieves all projects within the specified organization, including those organized in folders and nested subfolders. This ensures that every project under the organization’s hierarchy is scanned, providing full visibility across the entire organization. From a6c88c0d9ee281f7ce55b2a15ccc0ba453383d61 Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:11:32 +0200 Subject: [PATCH 4/8] test: timeout updated for E2E (#8351) --- ui/playwright.config.ts | 3 +++ ui/tests/auth-login.spec.ts | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index 8fd6bc0dd4..a680feabdb 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -8,6 +8,9 @@ export default defineConfig({ workers: process.env.CI ? 1 : undefined, reporter: [["list"]], outputDir: "/tmp/playwright-tests", + expect: { + timeout: 20000, + }, use: { baseURL: "http://localhost:3000", diff --git a/ui/tests/auth-login.spec.ts b/ui/tests/auth-login.spec.ts index c1764a3b28..fc612a71a5 100644 --- a/ui/tests/auth-login.spec.ts +++ b/ui/tests/auth-login.spec.ts @@ -196,6 +196,11 @@ test.describe("Accessibility", () => { await page.keyboard.press("Tab"); // Show password button await page.keyboard.press("Tab"); // Login button + + if (process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true") { + await page.keyboard.press("Tab"); // Forgot password + } + await expect(page.getByRole("button", { name: "Log in" })).toBeFocused(); }); From 83b328ea92ec3951f1e6120c6caf9d7c3f009148 Mon Sep 17 00:00:00 2001 From: Kay Agahd Date: Wed, 23 Jul 2025 15:03:02 +0200 Subject: [PATCH 5/8] fix(aws): avoid false positives in SQS encryption check for ephemeral queues (#8330) Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> --- prowler/CHANGELOG.md | 3 +++ prowler/providers/aws/services/sqs/sqs_service.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 86f91170bb..bbb3ed5515 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -8,6 +8,9 @@ All notable changes to the **Prowler SDK** are documented in this file. - Add `bedrock_api_key_no_administrative_privileges` check for AWS provider [(#8321)](https://github.com/prowler-cloud/prowler/pull/8321) - Support App Key Content in GitHub provider [(#8271)](https://github.com/prowler-cloud/prowler/pull/8271) +### Fixed +- False positives in SQS encryption check for ephemeral queues [(#8330)](https://github.com/prowler-cloud/prowler/pull/8330) + --- ## [v5.9.3] (Prowler UNRELEASED) diff --git a/prowler/providers/aws/services/sqs/sqs_service.py b/prowler/providers/aws/services/sqs/sqs_service.py index 598cd625a4..9e6bb0c230 100644 --- a/prowler/providers/aws/services/sqs/sqs_service.py +++ b/prowler/providers/aws/services/sqs/sqs_service.py @@ -51,6 +51,7 @@ class SQS(AWSService): def _get_queue_attributes(self): try: logger.info("SQS - describing queue attributes...") + valid_queues = [] for queue in self.queues: try: regional_client = self.regional_clients[queue.region] @@ -72,6 +73,7 @@ class SQS(AWSService): == "true" ): queue.kms_key_id = "SqsManagedSseEnabled" + valid_queues.append(queue) except ClientError as error: if ( error.response["Error"]["Code"] @@ -84,10 +86,13 @@ class SQS(AWSService): logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + valid_queues.append(queue) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + valid_queues.append(queue) + self.queues = valid_queues except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" From 5669a42039974136ff4bc0bbd0c4a07efb00602d Mon Sep 17 00:00:00 2001 From: Cole Murray Date: Wed, 23 Jul 2025 07:06:55 -0700 Subject: [PATCH 6/8] fix(wazuh): patch command injection vulnerability in prowler-wrapper.py (#8331) Co-authored-by: Test User Co-authored-by: MrCloudSec --- contrib/wazuh/prowler-wrapper.py | 9 +- .../wazuh/prowler_wrapper_security_test.py | 256 ++++++++++++++++++ 2 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 tests/contrib/wazuh/prowler_wrapper_security_test.py diff --git a/contrib/wazuh/prowler-wrapper.py b/contrib/wazuh/prowler-wrapper.py index a76344a00b..e7e7faa9af 100644 --- a/contrib/wazuh/prowler-wrapper.py +++ b/contrib/wazuh/prowler-wrapper.py @@ -23,6 +23,7 @@ import argparse import json import os import re +import shlex import signal import socket import subprocess @@ -145,11 +146,11 @@ def _get_script_arguments(): def _run_prowler(prowler_args): _debug("Running prowler with args: {0}".format(prowler_args), 1) - _prowler_command = "{prowler}/prowler {args}".format( - prowler=PATH_TO_PROWLER, args=prowler_args + _prowler_command = shlex.split( + "{prowler}/prowler {args}".format(prowler=PATH_TO_PROWLER, args=prowler_args) ) - _debug("Running command: {0}".format(_prowler_command), 2) - _process = subprocess.Popen(_prowler_command, stdout=subprocess.PIPE, shell=True) + _debug("Running command: {0}".format(" ".join(_prowler_command)), 2) + _process = subprocess.Popen(_prowler_command, stdout=subprocess.PIPE) _output, _error = _process.communicate() _debug("Raw prowler output: {0}".format(_output), 3) _debug("Raw prowler error: {0}".format(_error), 3) diff --git a/tests/contrib/wazuh/prowler_wrapper_security_test.py b/tests/contrib/wazuh/prowler_wrapper_security_test.py new file mode 100644 index 0000000000..0f10144168 --- /dev/null +++ b/tests/contrib/wazuh/prowler_wrapper_security_test.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python +""" +Security test for prowler-wrapper.py command injection vulnerability +This test demonstrates the command injection vulnerability and validates the fix +""" + +import os +import shutil +import sys +import tempfile +import unittest +from unittest.mock import MagicMock, patch + + +class TestProwlerWrapperSecurity(unittest.TestCase): + """Test cases for command injection vulnerability in prowler-wrapper.py""" + + def setUp(self): + """Set up test environment""" + # Create a temporary directory for testing + self.test_dir = tempfile.mkdtemp() + self.prowler_wrapper_path = os.path.join( + os.path.dirname( + os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + ), + "contrib", + "wazuh", + "prowler-wrapper.py", + ) + + def tearDown(self): + """Clean up test environment""" + shutil.rmtree(self.test_dir, ignore_errors=True) + + def _import_prowler_wrapper(self): + """Helper to import prowler_wrapper with mocked WAZUH_PATH""" + sys.path.insert(0, os.path.dirname(self.prowler_wrapper_path)) + + # Mock the WAZUH_PATH that's read at module level + with patch("builtins.open", create=True) as mock_open: + mock_open.return_value.readline.return_value = 'DIRECTORY="/opt/wazuh"' + + import importlib.util + + spec = importlib.util.spec_from_file_location( + "prowler_wrapper", self.prowler_wrapper_path + ) + prowler_wrapper = importlib.util.module_from_spec(spec) + spec.loader.exec_module(prowler_wrapper) + return prowler_wrapper._run_prowler + + def test_command_injection_semicolon(self): + """Test command injection using semicolon""" + # Create a test file that should not be created if injection is prevented + test_file = os.path.join(self.test_dir, "pwned.txt") + + # Malicious profile that attempts to create a file + malicious_profile = f"test; touch {test_file}" + + # Mock the subprocess.Popen to capture the command + with patch("subprocess.Popen") as mock_popen: + mock_process = MagicMock() + mock_process.communicate.return_value = (b"test output", None) + mock_popen.return_value = mock_process + + # Import and run the vulnerable function + _run_prowler = self._import_prowler_wrapper() + + # Run with malicious input + _run_prowler(f'-p "{malicious_profile}" -V') + + # Check that Popen was called + self.assertTrue(mock_popen.called) + + # Get the actual command that was passed to Popen + actual_command = mock_popen.call_args[0][0] + + # With the fix, the command should be a list (from shlex.split) + # and should NOT have shell=True + self.assertIsInstance( + actual_command, list, "Command should be a list after shlex.split" + ) + + # Check that shell=True is not in the call + call_kwargs = mock_popen.call_args[1] + self.assertNotIn( + "shell", + call_kwargs, + "shell parameter should not be present (defaults to False)", + ) + + def test_command_injection_ampersand(self): + """Test command injection using ampersand""" + # Create a test file that should not be created if injection is prevented + test_file = os.path.join(self.test_dir, "pwned2.txt") + + # Malicious profile that attempts to create a file + malicious_profile = f"test && touch {test_file}" + + with patch("subprocess.Popen") as mock_popen: + mock_process = MagicMock() + mock_process.communicate.return_value = (b"test output", None) + mock_popen.return_value = mock_process + + # Import and run the function + _run_prowler = self._import_prowler_wrapper() + + # Run with malicious input + _run_prowler(f'-p "{malicious_profile}" -V') + + # Get the actual command + actual_command = mock_popen.call_args[0][0] + + # Verify it's a list (safe execution) + self.assertIsInstance(actual_command, list) + + # The malicious characters should be preserved as part of the argument + # not interpreted as shell commands + command_str = " ".join(actual_command) + self.assertIn( + "&&", + command_str, + "Shell metacharacters should be preserved as literals", + ) + + def test_command_injection_pipe(self): + """Test command injection using pipe""" + malicious_profile = 'test | echo "injected"' + + with patch("subprocess.Popen") as mock_popen: + mock_process = MagicMock() + mock_process.communicate.return_value = (b"test output", None) + mock_popen.return_value = mock_process + + # Import and run the function + _run_prowler = self._import_prowler_wrapper() + + # Run with malicious input + _run_prowler(f'-p "{malicious_profile}" -V') + + # Get the actual command + actual_command = mock_popen.call_args[0][0] + + # Verify safe execution + self.assertIsInstance(actual_command, list) + + # Pipe should be preserved as literal + command_str = " ".join(actual_command) + self.assertIn("|", command_str) + + def test_command_injection_backticks(self): + """Test command injection using backticks""" + malicious_profile = "test `echo injected`" + + with patch("subprocess.Popen") as mock_popen: + mock_process = MagicMock() + mock_process.communicate.return_value = (b"test output", None) + mock_popen.return_value = mock_process + + # Import and run the function + _run_prowler = self._import_prowler_wrapper() + + # Run with malicious input + _run_prowler(f'-p "{malicious_profile}" -V') + + # Get the actual command + actual_command = mock_popen.call_args[0][0] + + # Verify safe execution + self.assertIsInstance(actual_command, list) + + # Backticks should be preserved as literals + command_str = " ".join(actual_command) + self.assertIn("`", command_str) + + def test_command_injection_dollar_parentheses(self): + """Test command injection using $() syntax""" + malicious_profile = "test $(echo injected)" + + with patch("subprocess.Popen") as mock_popen: + mock_process = MagicMock() + mock_process.communicate.return_value = (b"test output", None) + mock_popen.return_value = mock_process + + # Import and run the function + _run_prowler = self._import_prowler_wrapper() + + # Run with malicious input + _run_prowler(f'-p "{malicious_profile}" -V') + + # Get the actual command + actual_command = mock_popen.call_args[0][0] + + # Verify safe execution + self.assertIsInstance(actual_command, list) + + # $() should be preserved as literals + command_str = " ".join(actual_command) + self.assertIn("$(", command_str) + + def test_legitimate_profile_name(self): + """Test that legitimate profile names still work correctly""" + legitimate_profile = "production-aws-profile" + + with patch("subprocess.Popen") as mock_popen: + mock_process = MagicMock() + mock_process.communicate.return_value = (b"test output", None) + mock_popen.return_value = mock_process + + # Import and run the function + _run_prowler = self._import_prowler_wrapper() + + # Run with legitimate input + result = _run_prowler(f"-p {legitimate_profile} -V") + + # Verify the function returns output + self.assertEqual(result, b"test output") + + # Verify Popen was called correctly + actual_command = mock_popen.call_args[0][0] + self.assertIsInstance(actual_command, list) + + # Check the profile is passed correctly + command_str = " ".join(actual_command) + self.assertIn(legitimate_profile, command_str) + + def test_shlex_split_behavior(self): + """Test that shlex properly handles quoted arguments""" + profile_with_spaces = "my profile name" + + with patch("subprocess.Popen") as mock_popen: + mock_process = MagicMock() + mock_process.communicate.return_value = (b"test output", None) + mock_popen.return_value = mock_process + + # Import and run the function + _run_prowler = self._import_prowler_wrapper() + + # Run with profile containing spaces + _run_prowler(f'-p "{profile_with_spaces}" -V') + + # Get the actual command + actual_command = mock_popen.call_args[0][0] + + # Verify it's properly split + self.assertIsInstance(actual_command, list) + + # The profile name should be preserved as a single argument + # despite containing spaces + self.assertIn("my profile name", actual_command) + + +if __name__ == "__main__": + unittest.main() From ad0b8a42081e624e95e08f28e21b72be48135470 Mon Sep 17 00:00:00 2001 From: sumit-tft <70506234+sumit-tft@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:40:51 +0530 Subject: [PATCH 7/8] feat(ui): create CustomLink component and refactor links to use it (#8341) Co-authored-by: alejandrobailo --- ui/CHANGELOG.md | 1 + ui/app/(prowler)/error.tsx | 6 +-- ui/components/auth/oss/auth-form.tsx | 32 +++++------- .../aws-well-architected-details.tsx | 8 ++- .../compliance-custom-details/cis-details.tsx | 4 +- .../mitre-details.tsx | 6 +-- .../shared-components.tsx | 21 -------- .../findings/table/finding-detail.tsx | 16 +++--- .../integrations/forms/saml-config-form.tsx | 8 +-- .../integrations/saml-integration-card.tsx | 11 ++-- ui/components/lighthouse/chat.tsx | 10 ++-- .../forms/muted-findings-config-form.tsx | 10 ++-- .../workflow/forms/test-connection-form.tsx | 7 +-- .../via-credentials/m365-credentials-form.tsx | 18 +++---- .../workflow/provider-title-docs.tsx | 10 ++-- ui/components/ui/custom/custom-link.tsx | 51 +++++++++++++++++++ ui/components/ui/user-nav/user-nav.tsx | 13 +++-- 17 files changed, 124 insertions(+), 108 deletions(-) create mode 100644 ui/components/ui/custom/custom-link.tsx diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 647e668191..836f06c391 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -22,6 +22,7 @@ All notable changes to the **Prowler UI** are documented in this file. - Upgrade to Next.js 14.2.30 and lock TypeScript to 5.5.4 for ESLint compatibility [(#8189)](https://github.com/prowler-cloud/prowler/pull/8189) - Improved active step highlighting and updated step titles and descriptions in the Cloud Provider credentials update flow [(#8303)](https://github.com/prowler-cloud/prowler/pull/8303) +- Refactored all existing links across the app to use new custom-link component for consistent styling [(#8341)](https://github.com/prowler-cloud/prowler/pull/8341) ### 🐞 Fixed diff --git a/ui/app/(prowler)/error.tsx b/ui/app/(prowler)/error.tsx index d5083d9d33..f1813e0ede 100644 --- a/ui/app/(prowler)/error.tsx +++ b/ui/app/(prowler)/error.tsx @@ -1,10 +1,10 @@ "use client"; -import Link from "next/link"; import { useEffect } from "react"; import { RocketIcon } from "@/components/icons"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui"; +import { CustomLink } from "@/components/ui/custom/custom-link"; export default function Error({ error, @@ -27,9 +27,9 @@ export default function Error({ We're sorry for the inconvenience. Please try again or contact support if the problem persists. - + Go to the homepage - + ); } diff --git a/ui/components/auth/oss/auth-form.tsx b/ui/components/auth/oss/auth-form.tsx index a965204dbc..401a691354 100644 --- a/ui/components/auth/oss/auth-form.tsx +++ b/ui/components/auth/oss/auth-form.tsx @@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Icon } from "@iconify/react"; -import { Button, Checkbox, Divider, Link, Tooltip } from "@nextui-org/react"; +import { Button, Checkbox, Divider, Tooltip } from "@nextui-org/react"; import { useRouter, useSearchParams } from "next/navigation"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; @@ -15,6 +15,7 @@ import { NotificationIcon, ProwlerExtended } from "@/components/icons"; import { ThemeSwitch } from "@/components/ThemeSwitch"; import { useToast } from "@/components/ui"; import { CustomButton, CustomInput } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { Form, FormControl, @@ -301,13 +302,12 @@ export const AuthForm = ({ onChange={(e) => field.onChange(e.target.checked)} > I agree with the  - Terms of Service - +  of Prowler @@ -359,13 +359,9 @@ export const AuthForm = ({ content={
Social Login with Google is not enabled.{" "} - + Read the docs - +
} placement="right-start" @@ -392,13 +388,9 @@ export const AuthForm = ({ content={
Social Login with Github is not enabled.{" "} - + Read the docs - +
} placement="right-start" @@ -451,12 +443,16 @@ export const AuthForm = ({ {type === "sign-in" ? (

Need to create an account?  - Sign up + + Sign up +

) : (

Already have an account?  - Log in + + Log in +

)} diff --git a/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx b/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx index 9317c61485..96ed16a8f1 100644 --- a/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx +++ b/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx @@ -1,3 +1,4 @@ +import { CustomLink } from "@/components/ui/custom/custom-link"; import { SeverityBadge } from "@/components/ui/table"; import { Requirement } from "@/types/compliance"; @@ -7,7 +8,6 @@ import { ComplianceDetailContainer, ComplianceDetailSection, ComplianceDetailText, - ComplianceLink, } from "./shared-components"; export const AWSWellArchitectedCustomDetails = ({ @@ -75,11 +75,9 @@ export const AWSWellArchitectedCustomDetails = ({ {requirement.implementation_guidance_url && ( - + {requirement.implementation_guidance_url as string} - + )} diff --git a/ui/components/compliance/compliance-custom-details/cis-details.tsx b/ui/components/compliance/compliance-custom-details/cis-details.tsx index f804f8e7d0..e9244d6e57 100644 --- a/ui/components/compliance/compliance-custom-details/cis-details.tsx +++ b/ui/components/compliance/compliance-custom-details/cis-details.tsx @@ -1,5 +1,6 @@ import ReactMarkdown from "react-markdown"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { Requirement } from "@/types/compliance"; import { @@ -8,7 +9,6 @@ import { ComplianceDetailContainer, ComplianceDetailSection, ComplianceDetailText, - ComplianceLink, } from "./shared-components"; interface CISDetailsProps { @@ -121,7 +121,7 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => { {processReferences(requirement.references).map( (url: string, index: number) => (
- {url} + {url}
), )} diff --git a/ui/components/compliance/compliance-custom-details/mitre-details.tsx b/ui/components/compliance/compliance-custom-details/mitre-details.tsx index 71eca0d370..747531af3c 100644 --- a/ui/components/compliance/compliance-custom-details/mitre-details.tsx +++ b/ui/components/compliance/compliance-custom-details/mitre-details.tsx @@ -1,3 +1,4 @@ +import { CustomLink } from "@/components/ui/custom/custom-link"; import { Requirement } from "@/types/compliance"; import { @@ -7,7 +8,6 @@ import { ComplianceDetailContainer, ComplianceDetailSection, ComplianceDetailText, - ComplianceLink, } from "./shared-components"; export const MITRECustomDetails = ({ @@ -63,9 +63,9 @@ export const MITRECustomDetails = ({ {requirement.technique_url && ( - + {requirement.technique_url as string} - + )} diff --git a/ui/components/compliance/compliance-custom-details/shared-components.tsx b/ui/components/compliance/compliance-custom-details/shared-components.tsx index 2d1344b19b..416df6cc9c 100644 --- a/ui/components/compliance/compliance-custom-details/shared-components.tsx +++ b/ui/components/compliance/compliance-custom-details/shared-components.tsx @@ -1,26 +1,5 @@ -import Link from "next/link"; - import { cn } from "@/lib/utils"; -export const ComplianceLink = ({ - href, - children, -}: { - href: string; - children: React.ReactNode; -}) => { - return ( - - {children} - - ); -}; - export const ComplianceDetailContainer = ({ children, }: { diff --git a/ui/components/findings/table/finding-detail.tsx b/ui/components/findings/table/finding-detail.tsx index 06d665658c..00c88e285f 100644 --- a/ui/components/findings/table/finding-detail.tsx +++ b/ui/components/findings/table/finding-detail.tsx @@ -1,10 +1,10 @@ "use client"; import { Snippet } from "@nextui-org/react"; -import Link from "next/link"; import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; import { CustomSection } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { EntityInfoShort, InfoField } from "@/components/ui/entities"; import { DateWithTime } from "@/components/ui/entities/date-with-time"; import { SeverityBadge } from "@/components/ui/table/severity-badge"; @@ -151,15 +151,14 @@ export const FindingDetail = ({ {attributes.check_metadata.remediation.recommendation.text}

{attributes.check_metadata.remediation.recommendation.url && ( - Learn more - + )} @@ -179,13 +178,12 @@ export const FindingDetail = ({ {/* Additional Resources section */} {attributes.check_metadata.remediation.code.other && ( - View documentation - + )} diff --git a/ui/components/integrations/forms/saml-config-form.tsx b/ui/components/integrations/forms/saml-config-form.tsx index ecea01343f..c9e71d628d 100644 --- a/ui/components/integrations/forms/saml-config-form.tsx +++ b/ui/components/integrations/forms/saml-config-form.tsx @@ -1,6 +1,5 @@ "use client"; -import Link from "next/link"; import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; import { useFormState } from "react-dom"; import { z } from "zod"; @@ -9,6 +8,7 @@ import { createSamlConfig, updateSamlConfig } from "@/actions/integrations"; import { AddIcon } from "@/components/icons"; import { useToast } from "@/components/ui"; import { CustomButton, CustomServerInput } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { SnippetChip } from "@/components/ui/entities"; import { FormButtons } from "@/components/ui/form"; import { apiBaseUrl } from "@/lib"; @@ -239,9 +239,9 @@ export const SamlConfigForm = ({ assign the user's role. If the role does not exist, one will be created with minimal permissions. You can assign permissions to roles on the{" "} - - Roles - {" "} + + Roles + {" "} page.

diff --git a/ui/components/integrations/saml-integration-card.tsx b/ui/components/integrations/saml-integration-card.tsx index ae3eb6f60d..6ad745aa85 100644 --- a/ui/components/integrations/saml-integration-card.tsx +++ b/ui/components/integrations/saml-integration-card.tsx @@ -1,13 +1,13 @@ "use client"; import { Card, CardBody, CardHeader } from "@nextui-org/react"; -import { Link } from "@nextui-org/react"; import { CheckIcon, Trash2Icon } from "lucide-react"; import { useState } from "react"; import { deleteSamlConfig } from "@/actions/integrations"; import { useToast } from "@/components/ui"; import { CustomAlertModal, CustomButton } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { SamlConfigForm } from "./forms"; @@ -73,14 +73,9 @@ export const SamlIntegrationCard = ({ samlConfig }: { samlConfig?: any }) => { ) : ( <> Configure SAML Single Sign-On for secure authentication.{" "} - + Read the docs - + )}

diff --git a/ui/components/lighthouse/chat.tsx b/ui/components/lighthouse/chat.tsx index c31c6a08b9..54ca5604e7 100644 --- a/ui/components/lighthouse/chat.tsx +++ b/ui/components/lighthouse/chat.tsx @@ -1,12 +1,12 @@ "use client"; import { useChat } from "@ai-sdk/react"; -import Link from "next/link"; import { useEffect, useRef } from "react"; import { useForm } from "react-hook-form"; import { MemoizedMarkdown } from "@/components/lighthouse/memoized-markdown"; import { CustomButton, CustomTextarea } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { Form } from "@/components/ui/form"; interface SuggestedAction { @@ -138,12 +138,14 @@ export const Chat = ({ hasConfig, isActive }: ChatProps) => { ? "Please configure your OpenAI API key to use Lighthouse AI." : "OpenAI API key is invalid. Please update your key to use Lighthouse AI."}

- Configure API Key - + )} diff --git a/ui/components/providers/forms/muted-findings-config-form.tsx b/ui/components/providers/forms/muted-findings-config-form.tsx index 52c5e2c7df..1a5241b330 100644 --- a/ui/components/providers/forms/muted-findings-config-form.tsx +++ b/ui/components/providers/forms/muted-findings-config-form.tsx @@ -1,7 +1,6 @@ "use client"; import { Textarea } from "@nextui-org/react"; -import Link from "next/link"; import { Dispatch, SetStateAction, useEffect, useState } from "react"; import { useFormState } from "react-dom"; @@ -14,6 +13,7 @@ import { import { DeleteIcon } from "@/components/icons"; import { useToast } from "@/components/ui"; import { CustomButton } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { FormButtons } from "@/components/ui/form"; import { fontMono } from "@/config/fonts"; import { convertToYaml, parseYamlValidation } from "@/lib/yaml"; @@ -178,13 +178,9 @@ export const MutedFindingsConfigForm = ({
  • Learn more about configuring the Mutelist{" "} - + here - + .
  • diff --git a/ui/components/providers/workflow/forms/test-connection-form.tsx b/ui/components/providers/workflow/forms/test-connection-form.tsx index 6fdc6e2215..0ac38c03a1 100644 --- a/ui/components/providers/workflow/forms/test-connection-form.tsx +++ b/ui/components/providers/workflow/forms/test-connection-form.tsx @@ -3,7 +3,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Icon } from "@iconify/react"; import { Checkbox } from "@nextui-org/react"; -import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -18,6 +17,7 @@ import { getTask } from "@/actions/task/tasks"; import { CheckIcon, RocketIcon } from "@/components/icons"; import { useToast } from "@/components/ui"; import { CustomButton } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { Form } from "@/components/ui/form"; import { checkTaskStatus } from "@/lib/helper"; import { ProviderType } from "@/types"; @@ -295,8 +295,9 @@ export const TestConnectionForm = ({
    {apiErrorMessage ? ( - Back to providers - + ) : connectionStatus?.error ? ( router.back() : onResetCredentials} diff --git a/ui/components/providers/workflow/forms/via-credentials/m365-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/m365-credentials-form.tsx index 5fa8f2f9a1..13eb61710d 100644 --- a/ui/components/providers/workflow/forms/via-credentials/m365-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/via-credentials/m365-credentials-form.tsx @@ -1,8 +1,8 @@ -import Link from "next/link"; import { Control } from "react-hook-form"; import { InfoIcon } from "@/components/icons"; import { CustomInput } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { M365Credentials } from "@/types"; export const M365CredentialsForm = ({ @@ -57,14 +57,12 @@ export const M365CredentialsForm = ({ {" "} User and password authentication is being deprecated due to Microsoft's on-going MFA enforcement across all tenants (see{" "} - Microsoft docs - + ).

    @@ -76,14 +74,12 @@ export const M365CredentialsForm = ({

    Due to that change, you must only{" "} - use application authentication - {" "} + {" "} to maintain all Prowler M365 scan capabilities.

    {getProviderHelpText(providerType as string).text}

    - Read the docs - + ); diff --git a/ui/components/ui/custom/custom-link.tsx b/ui/components/ui/custom/custom-link.tsx new file mode 100644 index 0000000000..d52fbeaca2 --- /dev/null +++ b/ui/components/ui/custom/custom-link.tsx @@ -0,0 +1,51 @@ +import Link from "next/link"; +import React from "react"; + +import { cn } from "@/lib"; + +interface CustomLinkProps + extends React.AnchorHTMLAttributes { + href: string; + target?: "_self" | "_blank" | string; + ariaLabel?: string; + className?: string; + children: React.ReactNode; + scroll?: boolean; + size?: string; +} + +export const CustomLink = React.forwardRef( + ( + { + href, + target = "_blank", + ariaLabel, + className, + children, + scroll = true, + size = "xs", + ...props + }, + ref, + ) => { + return ( + + {children} + + ); + }, +); + +CustomLink.displayName = "CustomLink"; diff --git a/ui/components/ui/user-nav/user-nav.tsx b/ui/components/ui/user-nav/user-nav.tsx index ff441f0ec8..f4734e6384 100644 --- a/ui/components/ui/user-nav/user-nav.tsx +++ b/ui/components/ui/user-nav/user-nav.tsx @@ -1,7 +1,6 @@ "use client"; import { LogOut, User } from "lucide-react"; -import Link from "next/link"; import { useSession } from "next-auth/react"; import { logOut } from "@/actions/auth"; @@ -10,6 +9,8 @@ import { AvatarFallback, AvatarImage, } from "@/components/ui/avatar/avatar"; +import { Button } from "@/components/ui/button/button"; +import { CustomLink } from "@/components/ui/custom/custom-link"; import { DropdownMenu, DropdownMenuContent, @@ -26,8 +27,6 @@ import { TooltipTrigger, } from "@/components/ui/tooltip/tooltip"; -import { Button } from "../button/button"; - export const UserNav = () => { const { data: session } = useSession(); @@ -80,10 +79,14 @@ export const UserNav = () => { - + Account - + From 95791a9909b20a414607ba1d97de637dd2681986 Mon Sep 17 00:00:00 2001 From: Andoni Alonso <14891798+andoniaf@users.noreply.github.com> Date: Thu, 24 Jul 2025 09:34:45 +0200 Subject: [PATCH 8/8] chore(aws): replace known errors with warnings (#8347) Co-authored-by: Sergio Garcia --- prowler/CHANGELOG.md | 5 +++- .../elasticbeanstalk_service.py | 27 ++++++++++++++++--- .../providers/aws/services/iam/iam_service.py | 5 +++- .../secretsmanager/secretsmanager_service.py | 16 +++++++++++ .../providers/aws/services/ssm/ssm_service.py | 15 +++++++++++ 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index bbb3ed5515..e8250cf07f 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -5,9 +5,12 @@ All notable changes to the **Prowler SDK** are documented in this file. ## [v5.10.0] (Prowler UNRELEASED) ### Added -- Add `bedrock_api_key_no_administrative_privileges` check for AWS provider [(#8321)](https://github.com/prowler-cloud/prowler/pull/8321) +- `bedrock_api_key_no_administrative_privileges` check for AWS provider [(#8321)](https://github.com/prowler-cloud/prowler/pull/8321) - Support App Key Content in GitHub provider [(#8271)](https://github.com/prowler-cloud/prowler/pull/8271) +### Changed +- Handle some AWS errors as warnings instead of errors [(#8347)](https://github.com/prowler-cloud/prowler/pull/8347) + ### Fixed - False positives in SQS encryption check for ephemeral queues [(#8330)](https://github.com/prowler-cloud/prowler/pull/8330) diff --git a/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_service.py b/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_service.py index 0005177bdb..c8d821687d 100644 --- a/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_service.py +++ b/prowler/providers/aws/services/elasticbeanstalk/elasticbeanstalk_service.py @@ -1,5 +1,6 @@ from typing import Optional +from botocore.client import ClientError from pydantic.v1 import BaseModel from prowler.lib.logger import logger @@ -71,6 +72,17 @@ class ElasticBeanstalk(AWSService): and option["OptionName"] == "StreamLogs" ): environment.cloudwatch_stream_logs = option.get("Value", "false") + except ClientError as error: + if error.response["Error"]["Code"] in [ + "InvalidParameterValue", + ]: + logger.warning( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + else: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -84,10 +96,17 @@ class ElasticBeanstalk(AWSService): "ResourceTags" ] resource.tags = response - except Exception as error: - logger.error( - f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) + except ClientError as error: + if error.response["Error"]["Code"] in [ + "ResourceNotFoundException", + ]: + logger.warning( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + else: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" diff --git a/prowler/providers/aws/services/iam/iam_service.py b/prowler/providers/aws/services/iam/iam_service.py index 914483081b..1ff3dbd07a 100644 --- a/prowler/providers/aws/services/iam/iam_service.py +++ b/prowler/providers/aws/services/iam/iam_service.py @@ -865,7 +865,10 @@ class IAM(AWSService): SAMLProviderArn=resource.arn ).get("Tags", []) except Exception as error: - if error.response["Error"]["Code"] == "NoSuchEntityException": + if error.response["Error"]["Code"] in [ + "NoSuchEntity", + "NoSuchEntityException", + ]: logger.warning( f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) diff --git a/prowler/providers/aws/services/secretsmanager/secretsmanager_service.py b/prowler/providers/aws/services/secretsmanager/secretsmanager_service.py index 8a85d33502..695072c878 100644 --- a/prowler/providers/aws/services/secretsmanager/secretsmanager_service.py +++ b/prowler/providers/aws/services/secretsmanager/secretsmanager_service.py @@ -2,6 +2,7 @@ import json from datetime import datetime, timezone from typing import Dict, List, Optional +from botocore.client import ClientError from pydantic.v1 import BaseModel, Field from prowler.lib.logger import logger @@ -67,6 +68,21 @@ class SecretsManager(AWSService): ) if secret_policy.get("ResourcePolicy"): secret.policy = json.loads(secret_policy["ResourcePolicy"]) + except ClientError as error: + if error.response["Error"]["Code"] in [ + "ResourceNotFoundException", + ]: + logger.warning( + f"{self.region} --" + f" {error.__class__.__name__}[{error.__traceback__.tb_lineno}]:" + f" {error}" + ) + else: + logger.error( + f"{self.region} --" + f" {error.__class__.__name__}[{error.__traceback__.tb_lineno}]:" + f" {error}" + ) except Exception as error: logger.error( f"{self.region} --" diff --git a/prowler/providers/aws/services/ssm/ssm_service.py b/prowler/providers/aws/services/ssm/ssm_service.py index 33f1187993..689047551c 100644 --- a/prowler/providers/aws/services/ssm/ssm_service.py +++ b/prowler/providers/aws/services/ssm/ssm_service.py @@ -99,6 +99,21 @@ class SSM(AWSService): "AccountIds" ] + except ClientError as error: + if error.response["Error"]["Code"] in [ + "InvalidDocumentOperation", + ]: + logger.warning( + f"{regional_client.region} --" + f" {error.__class__.__name__}[{error.__traceback__.tb_lineno}]:" + f" {error}" + ) + else: + logger.error( + f"{regional_client.region} --" + f" {error.__class__.__name__}[{error.__traceback__.tb_lineno}]:" + f" {error}" + ) except Exception as error: logger.error( f"{regional_client.region} --"