Merge branch 'PRWLR-7300-create-ia-c-provider' into PRWLR-7318-add-ia-c-provider-tests

This commit is contained in:
Andoni A.
2025-05-29 12:40:23 +02:00
36 changed files with 31779 additions and 281 deletions
+4 -4
View File
@@ -86,12 +86,12 @@ prowler dashboard
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) |
|---|---|---|---|---|
| AWS | 564 | 82 | 35 | 10 |
| AWS | 567 | 82 | 36 | 10 |
| GCP | 79 | 13 | 9 | 3 |
| Azure | 140 | 18 | 8 | 3 |
| Azure | 142 | 18 | 10 | 3 |
| Kubernetes | 83 | 7 | 5 | 7 |
| GitHub | 3 | 2 | 1 | 0 |
| M365 | 44 | 2 | 2 | 0 |
| GitHub | 16 | 2 | 1 | 0 |
| M365 | 69 | 7 | 2 | 2 |
| NHN (Unofficial) | 6 | 2 | 1 | 0 |
> [!Note]
+1
View File
@@ -16,6 +16,7 @@ All notable changes to the **Prowler API** are documented in this file.
### Fixed
- Fixed task lookup to use task_kwargs instead of task_args for scan report resolution. [(#7830)](https://github.com/prowler-cloud/prowler/pull/7830)
- Fixed Kubernetes UID validation to allow valid context names [(#7871)](https://github.com/prowler-cloud/prowler/pull/7871)
---
+1 -1
View File
@@ -242,7 +242,7 @@ class Provider(RowLevelSecurityProtectedModel):
@staticmethod
def validate_kubernetes_uid(value):
if not re.match(
r"^[a-z0-9][A-Za-z0-9_.:\/-]{1,250}$",
r"^[a-zA-Z0-9][a-zA-Z0-9._@:\/-]{1,250}$",
value,
):
raise ModelValidationError(
+11 -1
View File
@@ -914,6 +914,16 @@ class TestProviderViewSet:
"uid": "gke_aaaa-dev_europe-test1_dev-aaaa-test-cluster-long-name-123456789",
"alias": "GKE",
},
{
"provider": "kubernetes",
"uid": "gke_project/cluster-name",
"alias": "GKE",
},
{
"provider": "kubernetes",
"uid": "admin@k8s-demo",
"alias": "test",
},
{
"provider": "azure",
"uid": "8851db6b-42e5-4533-aa9e-30a32d67e875",
@@ -921,7 +931,7 @@ class TestProviderViewSet:
},
{
"provider": "m365",
"uid": "TestingPro.onMirosoft.com",
"uid": "TestingPro.onmicrosoft.com",
"alias": "test",
},
{
+43
View File
@@ -0,0 +1,43 @@
import warnings
from dashboard.common_methods import get_section_containers_3_levels
warnings.filterwarnings("ignore")
def get_table(data):
data["REQUIREMENTS_DESCRIPTION"] = (
data["REQUIREMENTS_ID"] + " - " + data["REQUIREMENTS_DESCRIPTION"]
)
data["REQUIREMENTS_DESCRIPTION"] = data["REQUIREMENTS_DESCRIPTION"].apply(
lambda x: x[:150] + "..." if len(str(x)) > 150 else x
)
data["REQUIREMENTS_ATTRIBUTES_SECTION"] = data[
"REQUIREMENTS_ATTRIBUTES_SECTION"
].apply(lambda x: x[:80] + "..." if len(str(x)) > 80 else x)
data["REQUIREMENTS_ATTRIBUTES_SUBSECTION"] = data[
"REQUIREMENTS_ATTRIBUTES_SUBSECTION"
].apply(lambda x: x[:150] + "..." if len(str(x)) > 150 else x)
aux = data[
[
"REQUIREMENTS_DESCRIPTION",
"REQUIREMENTS_ATTRIBUTES_SECTION",
"REQUIREMENTS_ATTRIBUTES_SUBSECTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
]
return get_section_containers_3_levels(
aux,
"REQUIREMENTS_ATTRIBUTES_SECTION",
"REQUIREMENTS_ATTRIBUTES_SUBSECTION",
"REQUIREMENTS_DESCRIPTION",
)
+22 -6
View File
@@ -90,12 +90,28 @@ def create_layout_overview(
),
html.Div(
[
(
html.Label(
"Table Rows:",
className="text-prowler-stone-900 font-bold text-sm",
style={"margin-right": "10px"},
)
html.Label(
"Search:",
className="text-prowler-stone-900 font-bold text-sm",
style={"margin-right": "10px"},
),
dcc.Input(
id="search-input",
type="text",
placeholder="Search by check title, service, region...",
debounce=True,
style={
"padding": "4px 8px",
"border": "1px solid #ccc",
"borderRadius": "4px",
"marginRight": "20px",
"width": "250px",
},
),
html.Label(
"Table Rows:",
className="text-prowler-stone-900 font-bold text-sm",
style={"margin-right": "10px"},
),
table_row_dropdown,
download_button_csv,
+11
View File
@@ -518,6 +518,7 @@ else:
Input("service-filter", "value"),
Input("table-rows", "value"),
Input("status-filter", "value"),
Input("search-input", "value"),
Input("aws_card", "n_clicks"),
Input("azure_card", "n_clicks"),
Input("gcp_card", "n_clicks"),
@@ -540,6 +541,7 @@ def filter_data(
service_values,
table_row_values,
status_values,
search_value,
aws_clicks,
azure_clicks,
gcp_clicks,
@@ -1144,6 +1146,15 @@ def filter_data(
}
index_count = 0
if search_value:
search_value = search_value.lower()
filtered_data = filtered_data[
filtered_data["CHECK_TITLE"].str.lower().str.contains(search_value)
| filtered_data["SERVICE_NAME"].str.lower().str.contains(search_value)
| filtered_data["REGION"].str.lower().str.contains(search_value)
| filtered_data["STATUS"].str.lower().str.contains(search_value)
]
full_filtered_data = filtered_data.copy()
filtered_data = filtered_data.head(table_row_values)
# Sort the filtered_data
+8 -55
View File
@@ -163,70 +163,24 @@ export AZURE_CLIENT_ID="XXXXXXXXX"
export AZURE_CLIENT_SECRET="XXXXXXXXX"
export AZURE_TENANT_ID="XXXXXXXXX"
export M365_USER="your_email@example.com"
export M365_PASSWORD="6500780061006d0070006c006500700061007300730077006f0072006400" # replace this to yours
export M365_PASSWORD="examplepassword"
```
These two new environment variables are **required** to execute the PowerShell modules needed to retrieve information from M365 services. Prowler uses Service Principal authentication to access Microsoft Graph and user credentials to authenticate to Microsoft PowerShell modules.
- `M365_USER` should be your Microsoft account email using the default domain. This means it must look like `example@YourCompany.onmicrosoft.com`.
- `M365_USER` should be your Microsoft account email using the **assigned domain in the tenant**. This means it must look like `example@YourCompany.onmicrosoft.com` or `example@YourCompany.com`, but it must be the exact domain assigned to that user in the tenant.
To ensure that you are using the default domain you can see how to verify it [here](../tutorials/microsoft365/getting-started-m365.md#step-1-obtain-your-domain).
???+ warning
Using a tenant domain other than the one assigned — even if it belongs to the same tenant — will cause Prowler to fail, as Microsoft authentication will not succeed.
If you don't have a user created with that domain, Prowler will not work as it will not be able to ensure both app an user belong to the same tenant. To proceed, you can either create a new user with that domain or modify the domain of an existing user.
Ensure you are using the right domain for the user you are trying to authenticate with.
![User Domains](../tutorials/microsoft365/img/user-domains.png)
- `M365_PASSWORD` must be an encrypted SecureString. To convert your password into a valid encrypted string, you need to use PowerShell.
???+ warning
Passwords encrypted using ConvertTo-SecureString can only be decrypted on the same OS/user context. If you generate an encrypted password on macOS or Linux (both UNIX), it should fail on Windows and vice versa. As Prowler Cloud runs on UNIX if you generate your password using Windows it won't work so you'll need to generate a new password using any UNIX distro (example above)
If you are working from Windows and you will use your encrypted password in a different system (like for example executing Prowler in macOS or adding your password to Prowler Cloud), you will need to generate a "UNIX compatible" version of your encrypted password. This can be done using WSL which is so easy to install on Windows.
=== "UNIX"
Open a PowerShell cmd with a [supported version](requirements.md#supported-powershell-versions) and then run the following command:
```console
$securePassword = ConvertTo-SecureString "examplepassword" -AsPlainText -Force
$encryptedPassword = $securePassword | ConvertFrom-SecureString
Write-Output $encryptedPassword
6500780061006d0070006c006500700061007300730077006f0072006400
```
If everything is done correctly, you will see the encrypted string that you need to set as the `M365_PASSWORD` environment variable.
=== "Windows"
How to install WSL and PowerShell on it to generate that password (you can use a different distro but this one will work for sure):
```console
wsl --install -d Ubuntu-22.04
```
Then, open the Ubuntu terminal and run the following commands:
```console
sudo apt update && sudo apt install -y wget apt-transport-https software-properties-common
wget -q "https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb"
sudo dpkg -i packages-microsoft-prod.deb
sudo apt update
sudo apt install -y powershell
pwsh
```
With this done you will see now that a prompt running PowerShell with the latest version is open so here you will be able to generate your encrypted password:
```console
$securePassword = ConvertTo-SecureString "examplepassword" -AsPlainText -Force
$encryptedPassword = $securePassword | ConvertFrom-SecureString
Write-Output $encryptedPassword
6500780061006d0070006c006500700061007300730077006f0072006400
```
If everything is done correctly, you will see the encrypted string that you need to set as the `M365_PASSWORD` environment variable.
- `M365_PASSWORD` must be the user password.
???+ note
Before we asked for a encrypted password, but now we ask for the user password directly. Prowler will now handle the password encryption for you.
### Interactive Browser authentication
@@ -248,7 +202,6 @@ Prowler for M365 requires two types of permission scopes to be set (if you want
- `Directory.Read.All`: Required for all services.
- `Policy.Read.All`: Required for all services.
- `User.Read` (IMPORTANT: this must be set as **delegated**): Required for the sign-in.
- `Sites.Read.All`: Required for SharePoint service.
- `SharePointTenantSettings.Read.All`: Required for SharePoint service.
- `AuditLog.Read.All`: Required for Entra service.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 KiB

After

Width:  |  Height:  |  Size: 119 KiB

@@ -4,9 +4,9 @@ Set up your M365 account to enable security scanning using Prowler Cloud/App.
## Requirements
To configure your M365 account, youll need:
To configure your M365 account, you'll need:
1. Obtain your `Default Domain` from the Entra ID portal.
1. Obtain a domain from the Entra ID portal.
2. Access Prowler Cloud/App and add a new cloud provider `Microsoft 365`.
@@ -18,8 +18,6 @@ To configure your M365 account, youll need:
3.3 Assign the required roles to your user.
3.4 Retrieve your encrypted password.
4. Add the credentials to Prowler Cloud/App.
## Step 1: Obtain your Domain
@@ -32,9 +30,7 @@ Go to the Entra ID portal, then you can search for `Domain` or go to Identity >
![Custom Domain Names](./img/custom-domain-names.png)
Once you are there just look for the `Default Domain` this should be something similar to `YourCompany.onmicrosoft.com`. To ensure that you are picking the correct domain just click on it and verify that the type is `Initial` and you can't delete it.
![Search Default Domain](./img/search-default-domain.png)
Once you are there just select the domain you want to use.
---
@@ -78,11 +74,11 @@ A Service Principal is required to grant Prowler the necessary privileges.
![New Registration](./img/new-registration.png)
4. Go to `Certificates & secrets` > `+ New client secret`
4. Go to `Certificates & secrets` > `Client secrets` > `+ New client secret`
![Certificate & Secrets nav](./img/certificates-and-secrets.png)
5. Fill in the required fields and click `Add`, then copy the generated value (that value will be `AZURE_CLIENT_SECRET`)
5. Fill in the required fields and click `Add`, then copy the generated `value` (that value will be `AZURE_CLIENT_SECRET`)
![New Client Secret](./img/new-client-secret.png)
@@ -102,9 +98,9 @@ Assign the following Microsoft Graph permissions:
- `Directory.Read.All`: Required for all services.
- `Policy.Read.All`: Required for all services.
- `User.Read` (IMPORTANT: this is set as **delegated**): Required for the sign-in.
- `Sites.Read.All`: Required for SharePoint service.
- `SharePointTenantSettings.Read.All`: Required for SharePoint service.
- `AuditLog.Read.All`: Required for Entra service.
- `User.Read` (IMPORTANT: this is set as **delegated**): Required for the sign-in.
Follow these steps to assign the permissions:
@@ -120,8 +116,8 @@ Follow these steps to assign the permissions:
- `Directory.Read.All`
- `Policy.Read.All`
- `Sites.Read.All`
- `SharePointTenantSettings.Read.All`
- `AuditLog.Read.All`: Required for Entra service.
![Permission Screenshots](./img/directory-permission.png)
@@ -174,25 +170,34 @@ Follow these steps to assign the role:
---
### Get your encrypted password
For this step you will need to use PowerShell, here you will have to create your Encrypted Password based on the password of the User that you are going to use. For more information about how to generate this Password go [here](../../getting-started/requirements.md#service-principal-and-user-credentials-authentication-recommended) and follow the steps needed to obtain `M365_PASSWORD`.
---
## Step 4: Add credentials to Prowler Cloud/App
1. Go to your App Registration overview and copy the `Client ID` and `Tenant ID`
![App Overview](./img/app-overview.png)
???+ warning
For Prowler Cloud encrypted password is still needed (when we update Prowler Cloud and regular password is accepted this warning will be deleted), so the password that you paste in the next step should be generated following this steps:
- UNIX: Open a PowerShell cmd with a [supported version](../../getting-started/requirements.md#supported-powershell-versions) and then run the following command:
```console
$securePassword = ConvertTo-SecureString "examplepassword" -AsPlainText -Force
$encryptedPassword = $securePassword | ConvertFrom-SecureString
Write-Output $encryptedPassword
6500780061006d0070006c006500700061007300730077006f0072006400
```
- Windows: Install WSL using `wsl --install -d Ubuntu-22.04`, then open the Ubuntu terminal, install powershell and run the same command above.
2. Go to Prowler Cloud/App and paste:
- `Client ID`
- `Tenant ID`
- `AZURE_CLIENT_SECRET` from earlier
- `M365_USER` your user using the default domain, more info [here](../../getting-started/requirements.md#service-principal-and-user-credentials-authentication-recommended)
- `M365_PASSWORD` generated before
- `M365_USER` the user using the correct assigned domain, more info [here](../../getting-started/requirements.md#service-principal-and-user-credentials-authentication-recommended)
- `M365_PASSWORD` the password of the user
![Prowler Cloud M365 Credentials](./img/m365-credentials.png)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 KiB

After

Width:  |  Height:  |  Size: 119 KiB

+3
View File
@@ -20,11 +20,14 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Add `repository_secret_scanning_enabled` check for GitHub provider. [(#7759)](https://github.com/prowler-cloud/prowler/pull/7759)
- Add `repository_default_branch_requires_codeowners_review` check for GitHub provider. [(#7753)](https://github.com/prowler-cloud/prowler/pull/7753)
- Add NIS 2 compliance framework for AWS. [(7839)](https://github.com/prowler-cloud/prowler/pull/7839)
- Add NIS 2 compliance framework for Azure. [(7857)](https://github.com/prowler-cloud/prowler/pull/7857)
- Add search bar in Dashboard Overview page. [(#7804)](https://github.com/prowler-cloud/prowler/pull/7804)
### Fixed
- Fix `m365_powershell test_credentials` to use sanitized credentials. [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761)
- Fix `admincenter_users_admins_reduced_license_footprint` check logic to pass when admin user has no license. [(#7779)](https://github.com/prowler-cloud/prowler/pull/7779)
- Fix `m365_powershell` to close the PowerShell sessions in msgraph services. [(#7816)](https://github.com/prowler-cloud/prowler/pull/7816)
- Fix `defender_ensure_notify_alerts_severity_is_high`check to accept high or lower severity. [(#7862)](https://github.com/prowler-cloud/prowler/pull/7862)
---
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -329,9 +329,8 @@ class CheckMetadata(BaseModel):
checks = set()
if service:
# This is a special case for the AWS provider since `lambda` is a reserved keyword in Python
if service == "awslambda":
service = "lambda"
if service == "lambda":
service = "awslambda"
checks = {
check_name
for check_name, check_metadata in bulk_checks_metadata.items()
@@ -1,16 +1,16 @@
{
"Provider": "azure",
"CheckID": "defender_ensure_notify_alerts_severity_is_high",
"CheckTitle": "Ensure That 'Notify about alerts with the following severity' is Set to 'High'",
"CheckTitle": "Ensure that email notifications are configured for alerts with a minimum severity of 'High' or lower",
"CheckType": [],
"ServiceName": "defender",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "AzureEmailNotifications",
"Description": "Microsoft Defender for Cloud emails the subscription owners whenever a high-severity alert is triggered for their subscription. You should provide a security contact email address as an additional email address.",
"Risk": "Microsoft Defender for Cloud emails the Subscription Owner to notify them about security alerts. Adding your Security Contact's email address to the 'Additional email addresses' field ensures that your organization's Security Team is included in these alerts. This ensures that the proper people are aware of any potential compromise in order to mitigate the risk in a timely fashion.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/security-center/security-center-provide-security-contact-details",
"Description": "Microsoft Defender for Cloud sends email notifications when alerts of a certain severity level or higher are triggered. By setting the minimum severity to 'High', 'Medium', or even 'Low', you ensure that alerts with equal or greater severity (e.g., High or Critical) are still delivered. Selecting a lower threshold like 'Low' results in more comprehensive alert coverage.",
"Risk": "If this setting is too restrictive (e.g., set to 'Critical' only), important security alerts with 'High' or 'Medium' severity might be missed. Ensuring that 'High' or a lower threshold is configured helps security teams stay informed about significant threats and respond in a timely manner.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/email-notifications-alerts#manage-notifications-on-email",
"Remediation": {
"Code": {
"CLI": "",
@@ -19,7 +19,7 @@
"Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/bc_azr_general_4#terraform"
},
"Recommendation": {
"Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Defender for Cloud 3. Click on Environment Settings 4. Click on the appropriate Management Group, Subscription, or Workspace 5. Click on Email notifications 6. Enter a valid security contact email address (or multiple addresses separated by commas) in the Additional email addresses field 7. Click Save",
"Text": "1. From Azure Home select the Portal Menu. 2. Select Microsoft Defender for Cloud. 3. Click on Environment Settings. 4. Click on the appropriate Management Group, Subscription, or Workspace. 5. Click on Email notifications. 6. Under 'Notify about alerts with the following severity (or higher)', select at least 'High' (or optionally 'Medium' or 'Low' for broader coverage). 7. Click Save.",
"Url": "https://docs.microsoft.com/en-us/rest/api/securitycenter/securitycontacts/list"
}
},
@@ -13,12 +13,15 @@ class defender_ensure_notify_alerts_severity_is_high(Check):
for contact in security_contacts.values():
report = Check_Report_Azure(metadata=self.metadata(), resource=contact)
report.subscription = subscription_name
report.status = "PASS"
report.status_extended = f"Notifiy alerts are enabled for severity high in subscription {subscription_name}."
report.status = "FAIL"
report.status_extended = f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {subscription_name}."
if contact.alert_notifications_minimal_severity != "High":
report.status = "FAIL"
report.status_extended = f"Notifiy alerts are not enabled for severity high in subscription {subscription_name}."
if (
contact.alert_notifications_minimal_severity != "Critical"
and contact.alert_notifications_minimal_severity != ""
):
report.status = "PASS"
report.status_extended = f"Notifications are enabled for alerts with a minimum severity of high or lower ({contact.alert_notifications_minimal_severity}) in subscription {subscription_name}."
findings.append(report)
@@ -36,7 +36,7 @@ class admincenter_groups_not_public_visibility(Check):
report.status = "FAIL"
report.status_extended = f"Group {group.name} has {group.visibility} visibility and should be Private."
if group.visibility != "Public":
if group.visibility and group.visibility != "Public":
report.status = "PASS"
report.status_extended = (
f"Group {group.name} has {group.visibility} visibility."
@@ -11,8 +11,6 @@ from prowler.providers.m365.m365_provider import M365Provider
class AdminCenter(M365Service):
def __init__(self, provider: M365Provider):
super().__init__(provider)
if self.powershell:
self.powershell.close()
self.organization_config = None
self.sharing_policy = None
@@ -203,7 +201,7 @@ class DirectoryRole(BaseModel):
class Group(BaseModel):
id: str
name: str
visibility: str
visibility: Optional[str]
class Domain(BaseModel):
@@ -493,6 +493,7 @@ class ConditionalAccessGrantControl(Enum):
BLOCK = "block"
DOMAIN_JOINED_DEVICE = "domainJoinedDevice"
PASSWORD_CHANGE = "passwordChange"
COMPLIANT_DEVICE = "compliantDevice"
class GrantControlOperator(Enum):
+1 -1
View File
@@ -37,7 +37,7 @@ mock_metadata_lambda = CheckMetadata(
CheckID="awslambda_function_url_public",
CheckTitle="Check 1",
CheckType=["type1"],
ServiceName="lambda",
ServiceName="awslambda",
SubServiceName="subservice1",
ResourceIdTemplate="template1",
Severity="high",
-4
View File
@@ -857,7 +857,6 @@ KISA_ISMSP_AWS = Compliance(
],
)
PROWLER_THREATSCORE_AWS_NAME = "prowler_threatscore_aws"
PROWLER_THREATSCORE_AWS = Compliance(
Framework="ProwlerThreatScore",
Version="1.0",
@@ -901,7 +900,6 @@ PROWLER_THREATSCORE_AWS = Compliance(
],
)
PROWLER_THREATSCORE_AZURE_NAME = "prowler_threatscore_azure"
PROWLER_THREATSCORE_AZURE = Compliance(
Framework="ProwlerThreatScore",
Version="1.0",
@@ -945,7 +943,6 @@ PROWLER_THREATSCORE_AZURE = Compliance(
],
)
PROWLER_THREATSCORE_GCP_NAME = "prowler_threatscore_gcp"
PROWLER_THREATSCORE_GCP = Compliance(
Framework="ProwlerThreatScore",
Version="1.0",
@@ -989,7 +986,6 @@ PROWLER_THREATSCORE_GCP = Compliance(
],
)
PROWLER_THREATSCORE_M365_NAME = "prowler_threatscore_m365"
PROWLER_THREATSCORE_M365 = Compliance(
Framework="ProwlerThreatScore",
Version="1.0",
@@ -10,10 +10,7 @@ from prowler.lib.outputs.compliance.prowler_threatscore.models import (
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_aws import (
ProwlerThreatScoreAWS,
)
from tests.lib.outputs.compliance.fixtures import (
PROWLER_THREATSCORE_AWS,
PROWLER_THREATSCORE_AWS_NAME,
)
from tests.lib.outputs.compliance.fixtures import PROWLER_THREATSCORE_AWS
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1
@@ -24,9 +21,7 @@ class TestProwlerThreatScoreAWS:
generate_finding_output(compliance={"ProwlerThreatScore-1.0": "1.1.1"})
]
output = ProwlerThreatScoreAWS(
findings, PROWLER_THREATSCORE_AWS, PROWLER_THREATSCORE_AWS_NAME
)
output = ProwlerThreatScoreAWS(findings, PROWLER_THREATSCORE_AWS)
output_data = output.data[0]
assert isinstance(output_data, ProwlerThreatScoreAWSModel)
assert output_data.Provider == "aws"
@@ -134,9 +129,7 @@ class TestProwlerThreatScoreAWS:
findings = [
generate_finding_output(compliance={"ProwlerThreatScore-1.0": "1.1.1"})
]
output = ProwlerThreatScoreAWS(
findings, PROWLER_THREATSCORE_AWS, PROWLER_THREATSCORE_AWS_NAME
)
output = ProwlerThreatScoreAWS(findings, PROWLER_THREATSCORE_AWS)
output._file_descriptor = mock_file
with patch.object(mock_file, "close", return_value=None):
@@ -10,10 +10,7 @@ from prowler.lib.outputs.compliance.prowler_threatscore.models import (
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_azure import (
ProwlerThreatScoreAzure,
)
from tests.lib.outputs.compliance.fixtures import (
PROWLER_THREATSCORE_AZURE,
PROWLER_THREATSCORE_AZURE_NAME,
)
from tests.lib.outputs.compliance.fixtures import PROWLER_THREATSCORE_AZURE
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_ID,
@@ -33,9 +30,7 @@ class TestProwlerThreatScoreAzure:
)
]
output = ProwlerThreatScoreAzure(
findings, PROWLER_THREATSCORE_AZURE, PROWLER_THREATSCORE_AZURE_NAME
)
output = ProwlerThreatScoreAzure(findings, PROWLER_THREATSCORE_AZURE)
output_data = output.data[0]
assert isinstance(output_data, ProwlerThreatScoreAzureModel)
assert output_data.Provider == "azure"
@@ -144,9 +139,7 @@ class TestProwlerThreatScoreAzure:
findings = [
generate_finding_output(compliance={"ProwlerThreatScore-1.0": "1.1.1"})
]
output = ProwlerThreatScoreAzure(
findings, PROWLER_THREATSCORE_AZURE, PROWLER_THREATSCORE_AZURE_NAME
)
output = ProwlerThreatScoreAzure(findings, PROWLER_THREATSCORE_AZURE)
output._file_descriptor = mock_file
with patch.object(mock_file, "close", return_value=None):
@@ -10,10 +10,7 @@ from prowler.lib.outputs.compliance.prowler_threatscore.models import (
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_gcp import (
ProwlerThreatScoreGCP,
)
from tests.lib.outputs.compliance.fixtures import (
PROWLER_THREATSCORE_GCP,
PROWLER_THREATSCORE_GCP_NAME,
)
from tests.lib.outputs.compliance.fixtures import PROWLER_THREATSCORE_GCP
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID
@@ -30,9 +27,7 @@ class TestProwlerThreatScoreGCP:
)
]
output = ProwlerThreatScoreGCP(
findings, PROWLER_THREATSCORE_GCP, PROWLER_THREATSCORE_GCP_NAME
)
output = ProwlerThreatScoreGCP(findings, PROWLER_THREATSCORE_GCP)
output_data = output.data[0]
assert isinstance(output_data, ProwlerThreatScoreGCPModel)
assert output_data.Provider == "gcp"
@@ -140,9 +135,7 @@ class TestProwlerThreatScoreGCP:
findings = [
generate_finding_output(compliance={"ProwlerThreatScore-1.0": "1.1.1"})
]
output = ProwlerThreatScoreGCP(
findings, PROWLER_THREATSCORE_GCP, PROWLER_THREATSCORE_GCP_NAME
)
output = ProwlerThreatScoreGCP(findings, PROWLER_THREATSCORE_GCP)
output._file_descriptor = mock_file
with patch.object(mock_file, "close", return_value=None):
@@ -10,10 +10,7 @@ from prowler.lib.outputs.compliance.prowler_threatscore.models import (
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_m365 import (
ProwlerThreatScoreM365,
)
from tests.lib.outputs.compliance.fixtures import (
PROWLER_THREATSCORE_M365,
PROWLER_THREATSCORE_M365_NAME,
)
from tests.lib.outputs.compliance.fixtures import PROWLER_THREATSCORE_M365
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
from tests.providers.m365.m365_fixtures import TENANT_ID
@@ -30,9 +27,7 @@ class TestProwlerThreatScoreM365:
)
]
output = ProwlerThreatScoreM365(
findings, PROWLER_THREATSCORE_M365, PROWLER_THREATSCORE_M365_NAME
)
output = ProwlerThreatScoreM365(findings, PROWLER_THREATSCORE_M365)
output_data = output.data[0]
assert isinstance(output_data, ProwlerThreatScoreM365Model)
assert output_data.Provider == "m365"
@@ -142,9 +137,7 @@ class TestProwlerThreatScoreM365:
findings = [
generate_finding_output(compliance={"ProwlerThreatScore-1.0": "1.1.1"})
]
output = ProwlerThreatScoreM365(
findings, PROWLER_THREATSCORE_M365, PROWLER_THREATSCORE_M365_NAME
)
output = ProwlerThreatScoreM365(findings, PROWLER_THREATSCORE_M365)
output._file_descriptor = mock_file
with patch.object(mock_file, "close", return_value=None):
@@ -31,7 +31,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
result = check.execute()
assert len(result) == 0
def test_defender_severity_alerts_low(self):
def test_defender_severity_alerts_critical(self):
resource_id = str(uuid4())
defender_client = mock.MagicMock
defender_client.security_contacts = {
@@ -41,7 +41,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
name="default",
emails="",
phone="",
alert_notifications_minimal_severity="Low",
alert_notifications_minimal_severity="Critical",
alert_notifications_state="On",
notified_roles=["Contributor"],
notified_roles_state="On",
@@ -69,7 +69,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Notifiy alerts are not enabled for severity high in subscription {AZURE_SUBSCRIPTION_ID}."
== f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {AZURE_SUBSCRIPTION_ID}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == "default"
@@ -113,7 +113,51 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Notifiy alerts are enabled for severity high in subscription {AZURE_SUBSCRIPTION_ID}."
== f"Notifications are enabled for alerts with a minimum severity of high or lower (High) in subscription {AZURE_SUBSCRIPTION_ID}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == "default"
assert result[0].resource_id == resource_id
def test_defender_severity_alerts_low(self):
resource_id = str(uuid4())
defender_client = mock.MagicMock
defender_client.security_contacts = {
AZURE_SUBSCRIPTION_ID: {
resource_id: SecurityContacts(
resource_id=resource_id,
name="default",
emails="",
phone="",
alert_notifications_minimal_severity="Low",
alert_notifications_state="On",
notified_roles=["Contributor"],
notified_roles_state="On",
)
}
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.defender.defender_ensure_notify_alerts_severity_is_high.defender_ensure_notify_alerts_severity_is_high.defender_client",
new=defender_client,
),
):
from prowler.providers.azure.services.defender.defender_ensure_notify_alerts_severity_is_high.defender_ensure_notify_alerts_severity_is_high import (
defender_ensure_notify_alerts_severity_is_high,
)
check = defender_ensure_notify_alerts_severity_is_high()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Notifications are enabled for alerts with a minimum severity of high or lower (Low) in subscription {AZURE_SUBSCRIPTION_ID}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == "default"
@@ -156,7 +200,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Notifiy alerts are not enabled for severity high in subscription {AZURE_SUBSCRIPTION_ID}."
== f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {AZURE_SUBSCRIPTION_ID}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == "default"
@@ -93,7 +93,7 @@ async def mock_entra_get_conditional_access_policy(_):
"include": ["797f4846-ba00-4fd7-ba43-dac1f8f63013"],
"exclude": [],
},
access_controls={"grant": ["MFA"], "block": []},
access_controls={"grant": ["MFA", "compliantDevice"], "block": []},
)
}
}
@@ -216,7 +216,7 @@ class Test_Entra_Service:
)
assert entra_client.conditional_access_policy[DOMAIN]["id-1"].access_controls[
"grant"
] == ["MFA"]
] == ["MFA", "compliantDevice"]
assert (
entra_client.conditional_access_policy[DOMAIN]["id-1"].access_controls[
"block"
@@ -114,3 +114,91 @@ class Test_admincenter_groups_not_public_visibility:
assert result[0].resource_name == "Group1"
assert result[0].resource_id == id_group1
assert result[0].location == "global"
def test_admincenter_group_public_visibility(self):
admincenter_client = mock.MagicMock
admincenter_client.audited_tenant = "audited_tenant"
admincenter_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
mock.patch(
"prowler.providers.m365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility.admincenter_client",
new=admincenter_client,
),
):
from prowler.providers.m365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility import (
admincenter_groups_not_public_visibility,
)
from prowler.providers.m365.services.admincenter.admincenter_service import (
Group,
)
id_group1 = str(uuid4())
admincenter_client.groups = {
id_group1: Group(id=id_group1, name="Group1", visibility="Public"),
}
check = admincenter_groups_not_public_visibility()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Group Group1 has Public visibility and should be Private."
)
assert result[0].resource == admincenter_client.groups[id_group1].dict()
assert result[0].resource_name == "Group1"
assert result[0].resource_id == id_group1
assert result[0].location == "global"
def test_admincenter_group_none_visibility(self):
admincenter_client = mock.MagicMock
admincenter_client.audited_tenant = "audited_tenant"
admincenter_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
mock.patch(
"prowler.providers.m365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility.admincenter_client",
new=admincenter_client,
),
):
from prowler.providers.m365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility import (
admincenter_groups_not_public_visibility,
)
from prowler.providers.m365.services.admincenter.admincenter_service import (
Group,
)
id_group1 = str(uuid4())
admincenter_client.groups = {
id_group1: Group(id=id_group1, name="Group1", visibility=None),
}
check = admincenter_groups_not_public_visibility()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Group Group1 has None visibility and should be Private."
)
assert result[0].resource == admincenter_client.groups[id_group1].dict()
assert result[0].resource_name == "Group1"
assert result[0].resource_id == id_group1
assert result[0].location == "global"
@@ -68,7 +68,10 @@ async def mock_entra_get_conditional_access_policies(_):
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
built_in_controls=[
ConditionalAccessGrantControl.BLOCK,
ConditionalAccessGrantControl.COMPLIANT_DEVICE,
],
operator=GrantControlOperator.OR,
authentication_strength=AuthenticationStrength.PHISHING_RESISTANT_MFA,
),
@@ -211,7 +214,10 @@ class Test_Entra_Service:
),
),
grant_controls=GrantControls(
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
built_in_controls=[
ConditionalAccessGrantControl.BLOCK,
ConditionalAccessGrantControl.COMPLIANT_DEVICE,
],
operator=GrantControlOperator.OR,
authentication_strength=AuthenticationStrength.PHISHING_RESISTANT_MFA,
),
+5
View File
@@ -11,6 +11,11 @@ All notable changes to the **Prowler UI** are documented in this file.
- Possibility to edit the organization name. [(#7829)](https://github.com/prowler-cloud/prowler/pull/7829)
- Add `Provider UID` filter to scans page. [(#7820)](https://github.com/prowler-cloud/prowler/pull/7820)
- Download report behaviour updated to show feedback based on API response. [(#7758)](https://github.com/prowler-cloud/prowler/pull/7758)
- Missing KISA and ProwlerThreat icons added to the compliance page. [(#7860)(https://github.com/prowler-cloud/prowler/pull/7860)]
- Improve CustomDropdownFilter component. [(#7868)(https://github.com/prowler-cloud/prowler/pull/7868)]
### 🐞 Fixes
- Retrieve more than 10 scans in /compliance page. [(#7865)](https://github.com/prowler-cloud/prowler/pull/7865)
---
+1
View File
@@ -33,6 +33,7 @@ export default async function Compliance({
filters: {
"filter[state]": "completed",
},
pageSize: 50,
});
if (!scansData?.data) {
@@ -8,9 +8,12 @@ import GDPRLogo from "./gdpr.svg";
import GxPLogo from "./gxp-aws.svg";
import HIPAALogo from "./hipaa.svg";
import ISOLogo from "./iso-27001.svg";
import KISALogo from "./kisa.svg";
import MITRELogo from "./mitre-attack.svg";
import NIS2Logo from "./nis2.svg";
import NISTLogo from "./nist.svg";
import PCILogo from "./pci-dss.svg";
import PROWLERTHREATLogo from "./prowlerThreat.svg";
import RBILogo from "./rbi.svg";
import SOC2Logo from "./soc2.svg";
@@ -60,4 +63,13 @@ export const getComplianceIcon = (complianceTitle: string) => {
if (complianceTitle.toLowerCase().includes("soc2")) {
return SOC2Logo;
}
if (complianceTitle.toLowerCase().includes("kisa")) {
return KISALogo;
}
if (complianceTitle.toLowerCase().includes("prowlerthreatscore")) {
return PROWLERTHREATLogo;
}
if (complianceTitle.toLowerCase().includes("nis2")) {
return NIS2Logo;
}
};
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 126 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.6 MiB

@@ -0,0 +1,16 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="200.000000pt" height="200.000000pt" viewBox="0 0 200.000000 200.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,200.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M423 1492 l157 -157 0 -503 0 -502 175 0 175 0 0 250 c0 138 4 250 8
250 4 0 30 -23 57 -51 l50 -51 160 4 c156 3 162 4 228 35 138 65 241 201 267
350 38 230 -113 458 -344 519 -40 11 -163 14 -571 14 l-520 0 158 -158z m870
-213 c29 -13 57 -58 57 -91 0 -40 -36 -86 -77 -98 -23 -7 -98 -10 -188 -8
l-150 3 -3 103 -3 102 170 0 c101 0 180 -4 194 -11z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 790 B

+135 -123
View File
@@ -10,167 +10,183 @@ import {
PopoverTrigger,
ScrollShadow,
} from "@nextui-org/react";
import { XCircle } from "lucide-react";
import { ChevronDown, X } from "lucide-react";
import { useSearchParams } from "next/navigation";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { PlusCircleIcon } from "@/components/icons";
import { useUrlFilters } from "@/hooks/use-url-filters";
import { CustomDropdownFilterProps } from "@/types";
import { EntityInfoShort } from "../entities";
const filterSelectedClass =
"inline-flex items-center border py-1 text-xs transition-colors border-transparent bg-default-500 text-secondary-foreground hover:bg-default-500/80 rounded-md px-2 font-normal";
export const CustomDropdownFilter: React.FC<CustomDropdownFilterProps> = ({
export const CustomDropdownFilter = ({
filter,
onFilterChange,
}) => {
}: CustomDropdownFilterProps) => {
const searchParams = useSearchParams();
const { clearFilter } = useUrlFilters();
const [groupSelected, setGroupSelected] = useState(new Set<string>());
const [pendingClearFilter, setPendingClearFilter] = useState<string | null>(
null,
const [isOpen, setIsOpen] = useState(false);
const filterValues = useMemo(() => filter?.values || [], [filter?.values]);
const selectedValues = Array.from(groupSelected).filter(
(value) => value !== "all",
);
const isAllSelected =
selectedValues.length === filterValues.length && filterValues.length > 0;
const allFilterKeys = useMemo(() => filter?.values || [], [filter?.values]);
const getActiveFilter = useMemo(() => {
const currentFilters: Record<string, string> = {};
Array.from(searchParams.entries()).forEach(([key, value]) => {
if (key.startsWith("filter[") && key.endsWith("]")) {
const filterKey = key.slice(7, -1);
if (filter && filter.key === filterKey) {
// eslint-disable-next-line security/detect-object-injection
currentFilters[filterKey] = value;
}
}
});
return currentFilters;
}, [searchParams, filter]);
const memoizedFilterValues = useMemo(
() => filter?.values || [],
[filter?.values],
);
const activeFilterValue = useMemo(() => {
const filterParam = searchParams.get(`filter[${filter?.key}]`);
return filterParam ? filterParam.split(",") : [];
}, [searchParams, filter?.key]);
// Sync URL state with component state
useEffect(() => {
if (filter && getActiveFilter[filter.key]) {
const activeValues = getActiveFilter[filter.key].split(",");
const newSelection = new Set(activeValues);
if (newSelection.size === memoizedFilterValues.length) {
if (activeFilterValue.length > 0) {
const newSelection = new Set(activeFilterValue);
if (newSelection.size === filterValues.length) {
newSelection.add("all");
}
setGroupSelected(newSelection);
} else {
setGroupSelected(new Set());
}
}, [getActiveFilter, filter?.key, memoizedFilterValues, filter]);
}, [activeFilterValue, filterValues.length]);
const updateSelection = useCallback(
(newValues: string[]) => {
const actualValues = newValues.filter((key) => key !== "all");
const newSelection = new Set(actualValues);
// Auto-add "all" if all items are selected
if (
actualValues.length === filterValues.length &&
filterValues.length > 0
) {
newSelection.add("all");
}
setGroupSelected(newSelection);
// Notify parent with actual values (excluding "all")
onFilterChange?.(filter.key, actualValues);
},
[filterValues.length, onFilterChange, filter.key],
);
const onSelectionChange = useCallback(
(keys: string[]) => {
setGroupSelected((prevGroupSelected) => {
const newSelection = new Set(keys);
const currentSelection = Array.from(groupSelected);
const newKeys = new Set(keys);
const oldKeys = new Set(currentSelection);
if (
newSelection.size === allFilterKeys.length &&
!newSelection.has("all")
) {
return new Set(["all", ...allFilterKeys]);
} else if (prevGroupSelected.has("all")) {
newSelection.delete("all");
return new Set(allFilterKeys.filter((key) => newSelection.has(key)));
}
return newSelection;
});
// Check if "all" was just toggled
const allWasSelected = oldKeys.has("all");
const allIsSelected = newKeys.has("all");
if (onFilterChange && filter) {
const selectedValues = keys.filter((key) => key !== "all");
onFilterChange(filter.key, selectedValues);
if (allIsSelected && !allWasSelected) {
// "all" was just selected - select all items
updateSelection(filterValues);
} else if (!allIsSelected && allWasSelected) {
// "all" was just deselected - deselect all items
updateSelection([]);
} else if (allIsSelected && allWasSelected) {
// "all" was already selected, but individual items changed
// Remove "all" and keep only the individual selections
const individualSelections = keys.filter((key) => key !== "all");
updateSelection(individualSelections);
} else {
// Normal individual selection without "all"
updateSelection(keys);
}
},
[allFilterKeys, onFilterChange, filter],
[groupSelected, updateSelection, filterValues],
);
const handleSelectAllClick = useCallback(() => {
setGroupSelected((prevGroupSelected: Set<string>) => {
const newSelection: Set<string> = prevGroupSelected.has("all")
? new Set()
: new Set(["all", ...allFilterKeys]);
const handleClearAll = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
updateSelection([]);
},
[updateSelection],
);
if (onFilterChange && filter) {
const selectedValues = Array.from(newSelection).filter(
(key) => key !== "all",
);
onFilterChange(filter.key, selectedValues);
}
return newSelection;
});
}, [allFilterKeys, onFilterChange, filter]);
// Update the pending clear filter
const onClearFilter = useCallback((filterKey: string) => {
setPendingClearFilter(filterKey);
}, []);
// Execute the update in the router after the render
useEffect(() => {
if (pendingClearFilter && filter) {
clearFilter(pendingClearFilter);
setPendingClearFilter(null); // Reset the state
}
}, [pendingClearFilter, clearFilter, filter]);
const getDisplayLabel = useCallback(
(value: string) => {
const entity = filter.valueLabelMapping?.find((entry) => entry[value])?.[
value
];
return entity?.alias || entity?.uid || value;
},
[filter.valueLabelMapping],
);
return (
<div className="relative flex w-full flex-col gap-2">
<Button
isIconOnly
variant="light"
onPress={() => onClearFilter(filter.key)}
className={`absolute right-2 top-1/2 z-40 -translate-y-1/2 ${
groupSelected.size === 0 ? "hidden" : ""
}`}
<div className="flex w-full flex-col gap-2">
<Popover
backdrop="transparent"
placement="bottom-start"
isOpen={isOpen}
onOpenChange={setIsOpen}
>
<XCircle className="h-4 w-4 text-default-400" />
</Button>
<Popover backdrop="transparent" placement="bottom-start">
<PopoverTrigger>
<Button
className="border-input hover:bg-accent hover:text-accent-foreground inline-flex h-10 items-center justify-center whitespace-nowrap rounded-md border border-dashed bg-background px-3 text-xs font-medium shadow-sm transition-colors focus-visible:outline-none disabled:opacity-50 dark:bg-prowler-blue-800"
startContent={<PlusCircleIcon size={16} />}
className="border-input hover:bg-accent hover:text-accent-foreground inline-flex h-auto min-h-10 items-center justify-between whitespace-nowrap rounded-md border border-dashed bg-background px-3 py-2 text-xs font-medium shadow-sm transition-colors focus-visible:outline-none disabled:opacity-50 dark:bg-prowler-blue-800"
endContent={
<ChevronDown
className={`h-4 w-4 transition-transform ${isOpen ? "rotate-180" : ""}`}
/>
}
size="md"
variant="flat"
>
<h3 className="text-small">{filter?.labelCheckboxGroup}</h3>
<div className="flex min-w-0 flex-1 items-center gap-2">
<span className="flex-shrink-0 text-small">
{filter?.labelCheckboxGroup}
</span>
{groupSelected.size > 0 && (
<>
<Divider orientation="vertical" className="mx-2 h-4" />
<div className="flex items-center gap-2">
<div className="no-scrollbar hidden max-w-24 space-x-1 overflow-x-auto lg:flex">
{groupSelected.size > 3 ? (
<span className={filterSelectedClass}>
{`+${groupSelected.size - 2} selected`}
{selectedValues.length > 0 && (
<>
<Divider
orientation="vertical"
className="h-4 flex-shrink-0"
/>
<div className="flex min-w-0 flex-shrink items-center">
{selectedValues.length <= 2 ? (
<span
className="max-w-32 truncate text-xs text-default-500"
title={selectedValues.map(getDisplayLabel).join(", ")}
>
{selectedValues.map(getDisplayLabel).join(", ")}
</span>
) : (
Array.from(groupSelected)
.filter((value) => value !== "all")
.map((value) => (
<div key={value} className={filterSelectedClass}>
{value}
</div>
))
<span className="truncate text-xs text-default-500">
{isAllSelected
? "All selected"
: `${selectedValues.length} selected`}
</span>
)}
<div
onClick={handleClearAll}
className="ml-1 flex h-4 w-4 flex-shrink-0 cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-default-200"
aria-label="Clear selection"
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleClearAll(e as any);
}
}}
>
<X className="h-3 w-3 text-default-400 hover:text-default-600" />
</div>
</div>
</div>
</>
)}
</>
)}
</div>
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 dark:bg-prowler-blue-800">
<div className="flex w-full flex-col gap-6 p-2">
<div className="flex w-full flex-col gap-4 p-2">
<CheckboxGroup
color="default"
label={filter?.labelCheckboxGroup}
@@ -184,22 +200,18 @@ export const CustomDropdownFilter: React.FC<CustomDropdownFilterProps> = ({
wrapper: "checkbox-update",
}}
value="all"
isSelected={groupSelected.has("all")}
onClick={handleSelectAllClick}
>
Select All
</Checkbox>
<Divider orientation="horizontal" className="mt-2" />
<ScrollShadow
hideScrollBar
className="flex max-h-96 max-w-56 flex-col gap-y-2 py-2"
className="flex max-h-96 max-w-full flex-col gap-y-2 py-2"
>
{memoizedFilterValues.map((value) => {
// Find the corresponding entity from valueLabelMapping
const matchingEntry = filter.valueLabelMapping?.find(
{filterValues.map((value) => {
const entity = filter.valueLabelMapping?.find(
(entry) => entry[value],
);
const entity = matchingEntry?.[value];
)?.[value];
return (
<Checkbox