mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-05-16 17:22:43 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17c066e093 | |||
| a70f0652b6 | |||
| fae4fbc0ae | |||
| bbe45ed708 | |||
| 6b6d22bb31 | |||
| a3b4f94368 | |||
| 178cdb1b57 |
@@ -145,7 +145,7 @@ SENTRY_RELEASE=local
|
||||
NEXT_PUBLIC_SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT}
|
||||
|
||||
#### Prowler release version ####
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.25.2
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.25.3
|
||||
|
||||
# Social login credentials
|
||||
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.26.2"
|
||||
version = "1.26.3"
|
||||
|
||||
[project.scripts]
|
||||
celery = "src.backend.config.settings.celery"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.26.2
|
||||
version: 1.26.3
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
|
||||
@@ -422,7 +422,7 @@ class SchemaView(SpectacularAPIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
spectacular_settings.TITLE = "Prowler API"
|
||||
spectacular_settings.VERSION = "1.26.2"
|
||||
spectacular_settings.VERSION = "1.26.3"
|
||||
spectacular_settings.DESCRIPTION = (
|
||||
"Prowler API specification.\n\nThis file is auto-generated."
|
||||
)
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
## [5.25.3] (Prowler v5.25.3)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Oracle Cloud identity scans known or supplied regions to better support non Ashburn tenancies [(#10529)](https://github.com/prowler-cloud/prowler/pull/10529)
|
||||
|
||||
---
|
||||
|
||||
## [5.25.2] (Prowler v5.25.2)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
@@ -48,7 +48,7 @@ class _MutableTimestamp:
|
||||
|
||||
timestamp = _MutableTimestamp(datetime.today())
|
||||
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
|
||||
prowler_version = "5.25.2"
|
||||
prowler_version = "5.25.3"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
|
||||
@@ -66,6 +66,7 @@ class OraclecloudProvider(Provider):
|
||||
_compartments: list = []
|
||||
_mutelist: OCIMutelist
|
||||
audit_metadata: Audit_Metadata
|
||||
_home_region: str = "us-ashburn-1"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -160,6 +161,13 @@ class OraclecloudProvider(Provider):
|
||||
|
||||
# Get regions
|
||||
self._regions = self.get_regions_to_audit(region)
|
||||
self._home_region = None
|
||||
if self._regions:
|
||||
self._home_region = next(
|
||||
(region.key for region in self._regions if region.is_home_region),
|
||||
self._regions[0].key,
|
||||
)
|
||||
logger.info(f"Home region is: {self._home_region}")
|
||||
|
||||
# Get compartments
|
||||
self._compartments = self.get_compartments_to_audit(
|
||||
@@ -217,6 +225,10 @@ class OraclecloudProvider(Provider):
|
||||
def regions(self):
|
||||
return self._regions
|
||||
|
||||
@property
|
||||
def home_region(self):
|
||||
return self._home_region
|
||||
|
||||
@property
|
||||
def compartments(self):
|
||||
return self._compartments
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""OCI Identity Service Module."""
|
||||
|
||||
from datetime import datetime
|
||||
from threading import Lock
|
||||
from typing import Optional
|
||||
|
||||
import oci
|
||||
@@ -26,6 +27,7 @@ class Identity(OCIService):
|
||||
self.policies = []
|
||||
self.dynamic_groups = []
|
||||
self.domains = []
|
||||
self._domains_lock = Lock()
|
||||
self.password_policy = None
|
||||
self.root_compartment_resources = []
|
||||
self.active_non_root_compartments = []
|
||||
@@ -61,8 +63,8 @@ class Identity(OCIService):
|
||||
regional_client: Regional OCI client
|
||||
"""
|
||||
try:
|
||||
# Identity is a global service, use home region
|
||||
if regional_client.region not in self.provider.identity.region:
|
||||
# Only use one region for global users
|
||||
if regional_client.region != self.provider.home_region:
|
||||
return
|
||||
|
||||
identity_client = self.__get_client__(regional_client.region)
|
||||
@@ -312,7 +314,8 @@ class Identity(OCIService):
|
||||
def __list_groups__(self, regional_client):
|
||||
"""List all IAM groups."""
|
||||
try:
|
||||
if regional_client.region not in self.provider.identity.region:
|
||||
# Only use one region for global groups
|
||||
if regional_client.region != self.provider.home_region:
|
||||
return
|
||||
|
||||
identity_client = self.__get_client__(regional_client.region)
|
||||
@@ -355,7 +358,8 @@ class Identity(OCIService):
|
||||
def __list_policies__(self, regional_client):
|
||||
"""List all IAM policies."""
|
||||
try:
|
||||
if regional_client.region not in self.provider.identity.region:
|
||||
# Only use one region for global policies
|
||||
if regional_client.region != self.provider.home_region:
|
||||
return
|
||||
|
||||
identity_client = self.__get_client__(regional_client.region)
|
||||
@@ -399,8 +403,8 @@ class Identity(OCIService):
|
||||
def __list_dynamic_groups__(self, regional_client):
|
||||
"""List all dynamic groups in the tenancy."""
|
||||
try:
|
||||
# Dynamic groups are only in the home region
|
||||
if regional_client.region not in self.provider.identity.region:
|
||||
# Only use one region for global dynamic groups
|
||||
if regional_client.region != self.provider.home_region:
|
||||
return
|
||||
|
||||
identity_client = self.__get_client__(regional_client.region)
|
||||
@@ -447,10 +451,6 @@ class Identity(OCIService):
|
||||
def __list_domains__(self, regional_client):
|
||||
"""List all identity domains."""
|
||||
try:
|
||||
# Domains are only in the home region
|
||||
if regional_client.region not in self.provider.identity.region:
|
||||
return
|
||||
|
||||
identity_client = self.__get_client__(regional_client.region)
|
||||
|
||||
logger.info("Identity - Listing Identity Domains...")
|
||||
@@ -458,6 +458,7 @@ class Identity(OCIService):
|
||||
try:
|
||||
# List all domains in the tenancy
|
||||
for compartment in self.audited_compartments:
|
||||
|
||||
domains = oci.pagination.list_call_get_all_results(
|
||||
identity_client.list_domains,
|
||||
compartment_id=compartment.id,
|
||||
@@ -465,20 +466,38 @@ class Identity(OCIService):
|
||||
).data
|
||||
|
||||
for domain in domains:
|
||||
self.domains.append(
|
||||
IdentityDomain(
|
||||
id=domain.id,
|
||||
display_name=domain.display_name,
|
||||
description=domain.description or "",
|
||||
url=domain.url,
|
||||
home_region=domain.home_region,
|
||||
compartment_id=compartment.id,
|
||||
lifecycle_state=domain.lifecycle_state,
|
||||
time_created=domain.time_created,
|
||||
region=regional_client.region,
|
||||
password_policies=[],
|
||||
|
||||
# Threads run __list_domains__ concurrently per
|
||||
# region; serialize the dedupe-then-append so two
|
||||
# regions returning the same domain cannot race
|
||||
# past each other and produce duplicates or lose
|
||||
# the home-region preference.
|
||||
with self._domains_lock:
|
||||
existing = next(
|
||||
(d for d in self.domains if d.id == domain.id),
|
||||
None,
|
||||
)
|
||||
if existing is not None:
|
||||
# Prefer the entry from the domain's home region
|
||||
if domain.home_region == regional_client.region:
|
||||
self.domains.remove(existing)
|
||||
else:
|
||||
continue
|
||||
|
||||
self.domains.append(
|
||||
IdentityDomain(
|
||||
id=domain.id,
|
||||
display_name=domain.display_name,
|
||||
description=domain.description or "",
|
||||
url=domain.url,
|
||||
home_region=domain.home_region,
|
||||
compartment_id=compartment.id,
|
||||
lifecycle_state=domain.lifecycle_state,
|
||||
time_created=domain.time_created,
|
||||
region=regional_client.region,
|
||||
password_policies=[],
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
@@ -493,8 +512,8 @@ class Identity(OCIService):
|
||||
def __list_domain_password_policies__(self, regional_client):
|
||||
"""List password policies for all identity domains."""
|
||||
try:
|
||||
# Password policies are only in the home region
|
||||
if regional_client.region not in self.provider.identity.region:
|
||||
# Only use one region for all domain scan
|
||||
if regional_client.region != self.provider.home_region:
|
||||
return
|
||||
|
||||
logger.info("Identity - Listing Domain Password Policies...")
|
||||
@@ -551,7 +570,8 @@ class Identity(OCIService):
|
||||
def __get_password_policy__(self, regional_client):
|
||||
"""Get the password policy for the tenancy."""
|
||||
try:
|
||||
if regional_client.region not in self.provider.identity.region:
|
||||
# Only use one region for global password policies
|
||||
if regional_client.region != self.provider.home_region:
|
||||
return
|
||||
|
||||
identity_client = self.__get_client__(regional_client.region)
|
||||
@@ -578,8 +598,8 @@ class Identity(OCIService):
|
||||
def __search_root_compartment_resources__(self, regional_client):
|
||||
"""Search for resources in the root compartment using OCI Resource Search."""
|
||||
try:
|
||||
# Search is a global service, use home region
|
||||
if regional_client.region not in self.provider.identity.region:
|
||||
# Only use one region for global search
|
||||
if regional_client.region != self.provider.home_region:
|
||||
return
|
||||
|
||||
logger.info("Identity - Searching for resources in root compartment...")
|
||||
@@ -626,10 +646,9 @@ class Identity(OCIService):
|
||||
def __search_active_non_root_compartments__(self, regional_client):
|
||||
"""Search for active non-root compartments using OCI Resource Search."""
|
||||
try:
|
||||
# Search is a global service, use home region
|
||||
if regional_client.region not in self.provider.identity.region:
|
||||
# Only use one region for global search
|
||||
if regional_client.region != self.provider.home_region:
|
||||
return
|
||||
|
||||
logger.info("Identity - Searching for active non-root compartments...")
|
||||
|
||||
# Create search client using the helper method for proper authentication
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
|
||||
name = "prowler"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.13"
|
||||
version = "5.25.2"
|
||||
version = "5.25.3"
|
||||
|
||||
[project.scripts]
|
||||
prowler = "prowler.__main__:prowler"
|
||||
|
||||
@@ -37,6 +37,7 @@ def set_mocked_oraclecloud_provider(
|
||||
signer=MagicMock(),
|
||||
profile="DEFAULT",
|
||||
)
|
||||
provider.home_region = region
|
||||
|
||||
# Mock identity
|
||||
provider.identity = OCIIdentityInfo(
|
||||
|
||||
@@ -6,7 +6,7 @@ from prowler.providers.oraclecloud.exceptions.exceptions import (
|
||||
OCIAuthenticationError,
|
||||
OCIInvalidConfigError,
|
||||
)
|
||||
from prowler.providers.oraclecloud.models import OCISession
|
||||
from prowler.providers.oraclecloud.models import OCIIdentityInfo, OCIRegion, OCISession
|
||||
from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvider
|
||||
|
||||
|
||||
@@ -199,3 +199,59 @@ MIIEpQIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8n0sMcD/QHWCJ7yGSEtLN2T
|
||||
)
|
||||
|
||||
assert connection.is_connected is True
|
||||
|
||||
|
||||
class TestOraclecloudProviderInit:
|
||||
"""Tests for OraclecloudProvider initialization"""
|
||||
|
||||
def test_init_with_region_set_populates_provider_state(self):
|
||||
mock_session = OCISession(
|
||||
config={"region": "us-ashburn-1"}, signer=None, profile="DEFAULT"
|
||||
)
|
||||
mock_identity = OCIIdentityInfo(
|
||||
tenancy_id="ocid1.tenancy.oc1..aaaaaaaexample",
|
||||
tenancy_name="test-tenancy",
|
||||
user_id="ocid1.user.oc1..aaaaaaaexample",
|
||||
region="us-ashburn-1",
|
||||
profile="DEFAULT",
|
||||
audited_regions=set(),
|
||||
audited_compartments=[],
|
||||
)
|
||||
mock_regions = [
|
||||
OCIRegion(key="us-phoenix-1", name="us-phoenix-1", is_home_region=False),
|
||||
OCIRegion(key="us-ashburn-1", name="us-ashburn-1", is_home_region=True),
|
||||
]
|
||||
mock_compartments = ["ocid1.compartment.oc1..aaaaaaaexample"]
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.setup_session",
|
||||
return_value=mock_session,
|
||||
) as mock_setup_session,
|
||||
patch(
|
||||
"prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.set_identity",
|
||||
return_value=mock_identity,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.get_regions_to_audit",
|
||||
return_value=mock_regions,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.oraclecloud.oraclecloud_provider.OraclecloudProvider.get_compartments_to_audit",
|
||||
return_value=mock_compartments,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.set_global_provider"
|
||||
) as mock_set_global,
|
||||
):
|
||||
provider = OraclecloudProvider(
|
||||
region={"us-ashburn-1"},
|
||||
config_content={"dummy": True},
|
||||
mutelist_content={"Accounts": {}},
|
||||
)
|
||||
assert mock_setup_session.call_args.kwargs["region"] == "us-ashburn-1"
|
||||
assert provider.session == mock_session
|
||||
assert provider.identity == mock_identity
|
||||
assert provider.regions == mock_regions
|
||||
assert provider.compartments == mock_compartments
|
||||
assert provider.home_region == "us-ashburn-1"
|
||||
mock_set_global.assert_called_once_with(provider)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from unittest.mock import patch
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
from threading import Lock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from tests.providers.oraclecloud.oci_fixtures import set_mocked_oraclecloud_provider
|
||||
|
||||
@@ -28,3 +31,184 @@ class TestIdentityService:
|
||||
# Verify service name
|
||||
assert identity_client.service == "identity"
|
||||
assert identity_client.provider == oraclecloud_provider
|
||||
|
||||
def test_list_domains_passwords_skipped_outside_home(self):
|
||||
"""Domains should be skipped when not in home region."""
|
||||
with patch(
|
||||
"prowler.providers.oraclecloud.services.identity.identity_service.Identity.__init__",
|
||||
return_value=None,
|
||||
):
|
||||
from prowler.providers.oraclecloud.services.identity.identity_service import (
|
||||
Identity,
|
||||
)
|
||||
|
||||
identity_client = Identity(None)
|
||||
identity_client.service = "identity"
|
||||
identity_client.provider = set_mocked_oraclecloud_provider()
|
||||
identity_client.provider._home_region = "us-ashburn-1"
|
||||
identity_client.audited_compartments = [
|
||||
MagicMock(id="ocid1.compartment.oc1..aaaaaaaexample")
|
||||
]
|
||||
identity_client.domains = []
|
||||
identity_client._domains_lock = Lock()
|
||||
identity_client.session_signer = None
|
||||
identity_client.session_config = None
|
||||
regional_client_ash = MagicMock()
|
||||
regional_client_ash.region = "us-ashburn-1"
|
||||
regional_client_chi = MagicMock()
|
||||
regional_client_chi.region = "us-chicago-1"
|
||||
|
||||
policy = MagicMock()
|
||||
policy.id = "123"
|
||||
policy.name = "Test Policy"
|
||||
policy.description = "This is a test policy"
|
||||
policy.min_length = 8
|
||||
policy.password_expires_after = 90
|
||||
policy.num_passwords_in_history = 5
|
||||
policy.password_expire_warning = 7
|
||||
policy.min_password_age = 1
|
||||
|
||||
domains = []
|
||||
for region in ["us-phoenix-1", "us-ashburn-1", "us-chicago-1"]:
|
||||
domain = MagicMock()
|
||||
domain.id = (
|
||||
"ocid1.domain.oc1.iad.aaaaaaaaexampleuniqueID"
|
||||
if region == "us-chicago-1"
|
||||
else "ocid1.domain.oc1.iad.aaaaaaaaexampleuniqueID2"
|
||||
)
|
||||
domain.display_name = "exampledomain"
|
||||
domain.description = "example"
|
||||
domain.url = "https://idcs-example.identity.oraclecloud.com"
|
||||
domain.home_region = region
|
||||
domain.region = "us-ashburn-1"
|
||||
domain.lifecycle_state = "ACTIVE"
|
||||
domain.time_created = datetime.now()
|
||||
domains.append(domain)
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.oraclecloud.services.identity.identity_service.Identity.__get_client__",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.oraclecloud.services.identity.identity_service.oci.pagination.list_call_get_all_results",
|
||||
return_value=MagicMock(data=domains),
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.oraclecloud.services.identity.identity_service.oci.identity_domains.IdentityDomainsClient",
|
||||
return_value=MagicMock(
|
||||
list_password_policies=lambda: MagicMock(
|
||||
data=MagicMock(resources=[policy])
|
||||
)
|
||||
),
|
||||
),
|
||||
):
|
||||
identity_client.__list_domains__(regional_client_ash)
|
||||
identity_client.__list_domains__(regional_client_chi)
|
||||
identity_client.__list_domain_password_policies__(regional_client_ash)
|
||||
identity_client.__list_domain_password_policies__(regional_client_chi)
|
||||
|
||||
assert (
|
||||
len(identity_client.domains) == 2
|
||||
and any(
|
||||
domain.home_region == "us-ashburn-1"
|
||||
and domain.region == "us-ashburn-1"
|
||||
for domain in identity_client.domains
|
||||
)
|
||||
and any(
|
||||
domain.home_region == "us-chicago-1"
|
||||
and domain.region == "us-chicago-1"
|
||||
for domain in identity_client.domains
|
||||
)
|
||||
and all(len(d.password_policies) == 1 for d in identity_client.domains)
|
||||
)
|
||||
|
||||
def test_list_domains_concurrent_dedupes_and_prefers_home_region(self):
|
||||
"""__list_domains__ runs across regions in parallel; the dedupe
|
||||
must stay correct under concurrent calls (no duplicates, home
|
||||
region wins)."""
|
||||
with patch(
|
||||
"prowler.providers.oraclecloud.services.identity.identity_service.Identity.__init__",
|
||||
return_value=None,
|
||||
):
|
||||
from prowler.providers.oraclecloud.services.identity.identity_service import (
|
||||
Identity,
|
||||
)
|
||||
|
||||
identity_client = Identity(None)
|
||||
identity_client.service = "identity"
|
||||
identity_client.provider = set_mocked_oraclecloud_provider()
|
||||
identity_client.audited_compartments = [
|
||||
MagicMock(id="ocid1.compartment.oc1..aaaaaaaexample")
|
||||
]
|
||||
identity_client.domains = []
|
||||
identity_client._domains_lock = Lock()
|
||||
identity_client.session_signer = None
|
||||
identity_client.session_config = None
|
||||
|
||||
regions = [
|
||||
"us-ashburn-1",
|
||||
"us-chicago-1",
|
||||
"us-phoenix-1",
|
||||
"eu-frankfurt-1",
|
||||
]
|
||||
home_region_by_domain = {
|
||||
"ocid1.domain.oc1..domainA": "us-ashburn-1",
|
||||
"ocid1.domain.oc1..domainB": "us-chicago-1",
|
||||
"ocid1.domain.oc1..domainC": "eu-frankfurt-1",
|
||||
}
|
||||
|
||||
# Each region returns the same set of domains (every domain
|
||||
# is visible from every region; only one of those regions is
|
||||
# actually the domain's home region).
|
||||
def make_domains_for_region(_region):
|
||||
ds = []
|
||||
for domain_id, home_region in home_region_by_domain.items():
|
||||
d = MagicMock()
|
||||
d.id = domain_id
|
||||
d.display_name = f"name-{domain_id}"
|
||||
d.description = ""
|
||||
d.url = "https://example.identity.oraclecloud.com"
|
||||
d.home_region = home_region
|
||||
d.lifecycle_state = "ACTIVE"
|
||||
d.time_created = datetime.now()
|
||||
ds.append(d)
|
||||
return MagicMock(data=ds)
|
||||
|
||||
regional_clients = []
|
||||
for region in regions:
|
||||
rc = MagicMock()
|
||||
rc.region = region
|
||||
regional_clients.append(rc)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.oraclecloud.services.identity.identity_service.Identity.__get_client__",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.oraclecloud.services.identity.identity_service.oci.pagination.list_call_get_all_results",
|
||||
side_effect=lambda _list_call, compartment_id, lifecycle_state: make_domains_for_region(
|
||||
compartment_id
|
||||
),
|
||||
),
|
||||
):
|
||||
# Run several iterations to make any race more likely
|
||||
# to surface; with the lock removed this loop fails
|
||||
# frequently with duplicates.
|
||||
for _ in range(20):
|
||||
identity_client.domains = []
|
||||
with ThreadPoolExecutor(
|
||||
max_workers=len(regional_clients)
|
||||
) as executor:
|
||||
futures = [
|
||||
executor.submit(identity_client.__list_domains__, rc)
|
||||
for rc in regional_clients
|
||||
]
|
||||
for f in futures:
|
||||
f.result()
|
||||
|
||||
assert len(identity_client.domains) == len(home_region_by_domain)
|
||||
by_id = {d.id: d for d in identity_client.domains}
|
||||
for domain_id, home_region in home_region_by_domain.items():
|
||||
assert by_id[domain_id].region == home_region
|
||||
assert by_id[domain_id].home_region == home_region
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
## [1.25.3] (Prowler v5.25.3)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- CLI command in the finding drawer no longer renders the line-number gutter, matching the original styled block while removing the leading `1` [(#11059)](https://github.com/prowler-cloud/prowler/pull/11059)
|
||||
|
||||
---
|
||||
|
||||
## [1.25.2] (Prowler v5.25.2)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
+29
-6
@@ -219,21 +219,25 @@ vi.mock("@/components/shared/query-code-editor", () => ({
|
||||
language,
|
||||
value,
|
||||
copyValue,
|
||||
showLineNumbers = true,
|
||||
}: {
|
||||
ariaLabel: string;
|
||||
language?: string;
|
||||
value: string;
|
||||
copyValue?: string;
|
||||
showLineNumbers?: boolean;
|
||||
}) => (
|
||||
<div
|
||||
data-testid="query-code-editor"
|
||||
data-aria-label={ariaLabel}
|
||||
data-language={language}
|
||||
data-show-line-numbers={String(showLineNumbers)}
|
||||
>
|
||||
<span>{ariaLabel}</span>
|
||||
<span>{value}</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Copy ${ariaLabel}`}
|
||||
onClick={() => mockClipboardWriteText(copyValue ?? value)}
|
||||
>
|
||||
Copy editor code
|
||||
@@ -255,7 +259,22 @@ vi.mock("@/components/icons/services/IconServices", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/code-snippet/code-snippet", () => ({
|
||||
CodeSnippet: ({ value }: { value: string }) => <span>{value}</span>,
|
||||
CodeSnippet: ({
|
||||
value,
|
||||
formatter,
|
||||
ariaLabel = "Copy to clipboard",
|
||||
}: {
|
||||
value: string;
|
||||
formatter?: (value: string) => string;
|
||||
ariaLabel?: string;
|
||||
}) => (
|
||||
<div data-testid="code-snippet">
|
||||
<span>{formatter ? formatter(value) : value}</span>
|
||||
<button type="button" onClick={() => mockClipboardWriteText(value)}>
|
||||
{ariaLabel}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/custom/custom-link", () => ({
|
||||
@@ -592,7 +611,7 @@ describe("ResourceDetailDrawerContent — Fix 2: Remediation heading labels", ()
|
||||
expect(allText).toContain("CLI Command");
|
||||
});
|
||||
|
||||
it("should render remediation snippets with the shared code editor and copy CLI without the visual prompt", async () => {
|
||||
it("should render CLI remediation in the code editor without line numbers and copy without the visual prompt", async () => {
|
||||
// Given
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
@@ -612,17 +631,19 @@ describe("ResourceDetailDrawerContent — Fix 2: Remediation heading labels", ()
|
||||
|
||||
// When
|
||||
const editors = screen.getAllByTestId("query-code-editor");
|
||||
await user.click(
|
||||
within(editors[0]).getByRole("button", { name: "Copy editor code" }),
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "Copy CLI Command" }));
|
||||
|
||||
// Then
|
||||
expect(editors).toHaveLength(3);
|
||||
expect(editors[0]).toHaveAttribute("data-aria-label", "CLI Command");
|
||||
expect(editors[0]).toHaveAttribute("data-show-line-numbers", "false");
|
||||
expect(editors[1]).toHaveAttribute("data-show-line-numbers", "true");
|
||||
expect(editors[2]).toHaveAttribute("data-show-line-numbers", "true");
|
||||
expect(mockClipboardWriteText).toHaveBeenCalledWith("aws s3 ...");
|
||||
expect(screen.getByText("$ aws s3 ...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should pass syntax highlighting languages to each remediation editor", () => {
|
||||
it("should pass syntax highlighting languages to all remediation editors", () => {
|
||||
// Given
|
||||
render(
|
||||
<ResourceDetailDrawerContent
|
||||
@@ -643,10 +664,12 @@ describe("ResourceDetailDrawerContent — Fix 2: Remediation heading labels", ()
|
||||
const editors = screen.getAllByTestId("query-code-editor");
|
||||
|
||||
// Then
|
||||
expect(editors).toHaveLength(3);
|
||||
expect(editors[0]).toHaveAttribute("data-language", "shell");
|
||||
expect(editors[1]).toHaveAttribute("data-language", "hcl");
|
||||
expect(editors[2]).toHaveAttribute("data-language", "yaml");
|
||||
expect(editors[0]).toHaveAttribute("data-aria-label", "CLI Command");
|
||||
expect(editors[1]).toHaveAttribute("data-aria-label", "Terraform");
|
||||
expect(editors[2]).toHaveAttribute("data-aria-label", "CloudFormation");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,11 +120,13 @@ function renderRemediationCodeBlock({
|
||||
value,
|
||||
copyValue,
|
||||
language = QUERY_EDITOR_LANGUAGE.PLAIN_TEXT,
|
||||
showLineNumbers = true,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
copyValue?: string;
|
||||
language?: QueryEditorLanguage;
|
||||
showLineNumbers?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<QueryCodeEditor
|
||||
@@ -135,6 +137,7 @@ function renderRemediationCodeBlock({
|
||||
editable={false}
|
||||
minHeight={96}
|
||||
showCopyButton
|
||||
showLineNumbers={showLineNumbers}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
);
|
||||
@@ -889,6 +892,7 @@ export function ResourceDetailDrawerContent({
|
||||
copyValue: stripCodeFences(
|
||||
checkMeta.remediation.code.cli,
|
||||
),
|
||||
showLineNumbers: false,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -10,8 +10,6 @@ import { EditorState } from "@codemirror/state";
|
||||
import { tags } from "@lezer/highlight";
|
||||
import CodeMirror, {
|
||||
EditorView,
|
||||
highlightActiveLineGutter,
|
||||
lineNumbers,
|
||||
placeholder as codeEditorPlaceholder,
|
||||
} from "@uiw/react-codemirror";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
@@ -1177,6 +1175,7 @@ interface QueryCodeEditorProps
|
||||
editable?: boolean;
|
||||
minHeight?: number;
|
||||
showCopyButton?: boolean;
|
||||
showLineNumbers?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
@@ -1195,6 +1194,7 @@ export const QueryCodeEditor = ({
|
||||
editable = true,
|
||||
minHeight = 320,
|
||||
showCopyButton = false,
|
||||
showLineNumbers = true,
|
||||
onChange,
|
||||
onBlur,
|
||||
...props
|
||||
@@ -1208,8 +1208,6 @@ export const QueryCodeEditor = ({
|
||||
: lightHighlightStyle;
|
||||
|
||||
const extensions = [
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
EditorView.lineWrapping,
|
||||
codeEditorPlaceholder(placeholder ?? ""),
|
||||
EditorView.contentAttributes.of({
|
||||
@@ -1260,6 +1258,7 @@ export const QueryCodeEditor = ({
|
||||
<div
|
||||
data-testid="query-code-editor"
|
||||
data-language={language}
|
||||
data-show-line-numbers={String(showLineNumbers)}
|
||||
className={cn(
|
||||
"border-border-neutral-secondary bg-bg-neutral-primary overflow-hidden rounded-xl border",
|
||||
invalid && "border-border-error-primary",
|
||||
@@ -1307,8 +1306,9 @@ export const QueryCodeEditor = ({
|
||||
basicSetup={{
|
||||
foldGutter: false,
|
||||
highlightActiveLine: false,
|
||||
highlightActiveLineGutter: false,
|
||||
highlightActiveLineGutter: showLineNumbers,
|
||||
searchKeymap: false,
|
||||
lineNumbers: showLineNumbers,
|
||||
}}
|
||||
editable={editable}
|
||||
onChange={onChange}
|
||||
|
||||
Reference in New Issue
Block a user