mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 21:11:53 +00:00
Merge branch 'master' into PRWLR-7606-add-github-provider-to-ui
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <your-project-id>` 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 <project-id>` or by setting an environment variable:
|
||||
`export GOOGLE_CLOUD_QUOTA_PROJECT=<project-id>`
|
||||
|
||||
|
||||
@@ -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 <your-project-id>` 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 <project-id>` or by setting an environment variable:
|
||||
`export GOOGLE_CLOUD_QUOTA_PROJECT=<project-id>`
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
+14
-1
@@ -5,9 +5,22 @@ 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)
|
||||
|
||||
---
|
||||
|
||||
## [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)
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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} --"
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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} --"
|
||||
|
||||
@@ -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
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
@@ -23,6 +23,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
|
||||
|
||||
|
||||
@@ -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.
|
||||
</AlertDescription>
|
||||
<Link href="/" className="font-bold">
|
||||
<CustomLink href="/" target="_self" className="font-bold">
|
||||
Go to the homepage
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
<Link
|
||||
<CustomLink
|
||||
href="https://prowler.com/terms-of-service/"
|
||||
size="sm"
|
||||
target="_blank"
|
||||
>
|
||||
Terms of Service
|
||||
</Link>
|
||||
</CustomLink>
|
||||
of Prowler
|
||||
</Checkbox>
|
||||
</FormControl>
|
||||
@@ -359,13 +359,9 @@ export const AuthForm = ({
|
||||
content={
|
||||
<div className="flex-inline text-small">
|
||||
Social Login with Google is not enabled.{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs font-medium text-primary"
|
||||
>
|
||||
<CustomLink href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app-social-login/#google-oauth-configuration">
|
||||
Read the docs
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</div>
|
||||
}
|
||||
placement="right-start"
|
||||
@@ -392,13 +388,9 @@ export const AuthForm = ({
|
||||
content={
|
||||
<div className="flex-inline text-small">
|
||||
Social Login with Github is not enabled.{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs font-medium text-primary"
|
||||
>
|
||||
<CustomLink href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app-social-login/#github-oauth-configuration">
|
||||
Read the docs
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</div>
|
||||
}
|
||||
placement="right-start"
|
||||
@@ -451,12 +443,16 @@ export const AuthForm = ({
|
||||
{type === "sign-in" ? (
|
||||
<p className="text-center text-small">
|
||||
Need to create an account?
|
||||
<Link href="/sign-up">Sign up</Link>
|
||||
<CustomLink size="base" href="/sign-up" target="_self">
|
||||
Sign up
|
||||
</CustomLink>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-center text-small">
|
||||
Already have an account?
|
||||
<Link href="/sign-in">Log in</Link>
|
||||
<CustomLink size="base" href="/sign-in" target="_self">
|
||||
Log in
|
||||
</CustomLink>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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 && (
|
||||
<ComplianceDetailSection title="Implementation Guidance">
|
||||
<ComplianceLink
|
||||
href={requirement.implementation_guidance_url as string}
|
||||
>
|
||||
<CustomLink href={requirement.implementation_guidance_url as string}>
|
||||
{requirement.implementation_guidance_url as string}
|
||||
</ComplianceLink>
|
||||
</CustomLink>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
</ComplianceDetailContainer>
|
||||
|
||||
@@ -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) => (
|
||||
<div key={index}>
|
||||
<ComplianceLink href={url}>{url}</ComplianceLink>
|
||||
<CustomLink href={url}>{url}</CustomLink>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -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 && (
|
||||
<ComplianceDetailSection title="MITRE ATT&CK Reference">
|
||||
<ComplianceLink href={requirement.technique_url as string}>
|
||||
<CustomLink href={requirement.technique_url as string}>
|
||||
{requirement.technique_url as string}
|
||||
</ComplianceLink>
|
||||
</CustomLink>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Link
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="break-all text-sm text-blue-600 decoration-1 transition-colors hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export const ComplianceDetailContainer = ({
|
||||
children,
|
||||
}: {
|
||||
|
||||
@@ -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}
|
||||
</p>
|
||||
{attributes.check_metadata.remediation.recommendation.url && (
|
||||
<Link
|
||||
<CustomLink
|
||||
href={
|
||||
attributes.check_metadata.remediation.recommendation.url
|
||||
}
|
||||
target="_blank"
|
||||
className="text-sm text-blue-500 hover:underline"
|
||||
size="sm"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</CustomLink>
|
||||
)}
|
||||
</div>
|
||||
</InfoField>
|
||||
@@ -179,13 +178,12 @@ export const FindingDetail = ({
|
||||
{/* Additional Resources section */}
|
||||
{attributes.check_metadata.remediation.code.other && (
|
||||
<InfoField label="Additional Resources">
|
||||
<Link
|
||||
<CustomLink
|
||||
href={attributes.check_metadata.remediation.code.other}
|
||||
target="_blank"
|
||||
className="text-sm text-blue-500 hover:underline"
|
||||
size="sm"
|
||||
>
|
||||
View documentation
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</InfoField>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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{" "}
|
||||
<Link href="/roles">
|
||||
<span className="underline">Roles</span>
|
||||
</Link>{" "}
|
||||
<CustomLink href="/roles" target="_self">
|
||||
<span>Roles</span>
|
||||
</CustomLink>{" "}
|
||||
page.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -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.{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app-sso"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs font-medium text-primary"
|
||||
>
|
||||
<CustomLink href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app-sso">
|
||||
Read the docs
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
@@ -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."}
|
||||
</p>
|
||||
<Link
|
||||
<CustomLink
|
||||
href="/lighthouse/config"
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90"
|
||||
target="_self"
|
||||
size="sm"
|
||||
>
|
||||
Configure API Key
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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 = ({
|
||||
</li>
|
||||
<li>
|
||||
Learn more about configuring the Mutelist{" "}
|
||||
<Link
|
||||
href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/mutelist/"
|
||||
target="_blank"
|
||||
className="text-primary-600 hover:underline"
|
||||
>
|
||||
<CustomLink href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/mutelist/">
|
||||
here
|
||||
</Link>
|
||||
</CustomLink>
|
||||
.
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
<div className="flex w-full justify-end sm:space-x-6">
|
||||
{apiErrorMessage ? (
|
||||
<Link
|
||||
<CustomLink
|
||||
href="/providers"
|
||||
target="_self"
|
||||
className="mr-3 flex w-fit items-center justify-center space-x-2 rounded-lg border border-solid border-gray-200 px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
<Icon
|
||||
@@ -304,7 +305,7 @@ export const TestConnectionForm = ({
|
||||
className="h-5 w-5 text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
<span>Back to providers</span>
|
||||
</Link>
|
||||
</CustomLink>
|
||||
) : connectionStatus?.error ? (
|
||||
<CustomButton
|
||||
onPress={isUpdated ? () => router.back() : onResetCredentials}
|
||||
|
||||
@@ -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{" "}
|
||||
<Link
|
||||
<CustomLink
|
||||
href="https://azure.microsoft.com/en-us/blog/announcing-mandatory-multi-factor-authentication-for-azure-sign-in/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-primary hover:underline"
|
||||
size="sm"
|
||||
>
|
||||
Microsoft docs
|
||||
</Link>
|
||||
</CustomLink>
|
||||
).
|
||||
</p>
|
||||
|
||||
@@ -76,14 +74,12 @@ export const M365CredentialsForm = ({
|
||||
</div>
|
||||
<p className="text-sm text-default-500">
|
||||
Due to that change, you must only{" "}
|
||||
<Link
|
||||
<CustomLink
|
||||
href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/microsoft365/getting-started-m365/#step-3-configure-your-m365-account"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-primary hover:underline"
|
||||
size="sm"
|
||||
>
|
||||
use application authentication
|
||||
</Link>{" "}
|
||||
</CustomLink>{" "}
|
||||
to maintain all Prowler M365 scan capabilities.
|
||||
</p>
|
||||
<CustomInput
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from "next/link";
|
||||
"use client";
|
||||
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { getProviderName } from "@/components/ui/entities/get-provider-logo";
|
||||
import { getProviderLogo } from "@/components/ui/entities/get-provider-logo";
|
||||
import { getProviderHelpText } from "@/lib";
|
||||
@@ -24,13 +25,12 @@ export const ProviderTitleDocs = ({
|
||||
<p className="whitespace-nowrap text-sm text-default-500">
|
||||
{getProviderHelpText(providerType as string).text}
|
||||
</p>
|
||||
<Link
|
||||
<CustomLink
|
||||
href={getProviderHelpText(providerType as string).link}
|
||||
target="_blank"
|
||||
className="whitespace-nowrap text-sm font-medium text-primary"
|
||||
size="sm"
|
||||
>
|
||||
Read the docs
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
import { cn } from "@/lib";
|
||||
|
||||
interface CustomLinkProps
|
||||
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
href: string;
|
||||
target?: "_self" | "_blank" | string;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
scroll?: boolean;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
export const CustomLink = React.forwardRef<HTMLAnchorElement, CustomLinkProps>(
|
||||
(
|
||||
{
|
||||
href,
|
||||
target = "_blank",
|
||||
ariaLabel,
|
||||
className,
|
||||
children,
|
||||
scroll = true,
|
||||
size = "xs",
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<Link
|
||||
ref={ref}
|
||||
href={href}
|
||||
scroll={scroll}
|
||||
className={cn(
|
||||
`text-${size} break-all font-medium text-primary decoration-1 hover:underline`,
|
||||
className,
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
target={target}
|
||||
rel="noopener noreferrer"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CustomLink.displayName = "CustomLink";
|
||||
@@ -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 = () => {
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem className="hover:cursor-pointer" asChild>
|
||||
<Link href="/profile" className="flex items-center">
|
||||
<CustomLink
|
||||
href="/profile"
|
||||
className="flex items-center font-normal text-prowler-black dark:text-prowler-white"
|
||||
target="_self"
|
||||
>
|
||||
<User className="text-muted-foreground mr-3 h-4 w-4" />
|
||||
Account
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
@@ -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",
|
||||
@@ -34,6 +37,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@",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
+2
-2
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user