Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions api/src/backend/api/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ class CommonFindingFilters(FilterSet):
field_name="resources__type", lookup_expr="icontains"
)

categories = CharFilter(method="filter_categories")
categories__in = CharInFilter(method="filter_categories_in")

# Temporarily disabled until we implement tag filtering in the UI
# resource_tag_key = CharFilter(field_name="resources__tags__key")
# resource_tag_key__in = CharInFilter(
Expand Down Expand Up @@ -195,6 +198,15 @@ def filter_resource_tag(self, queryset, name, value):
)
return queryset.filter(overall_query).distinct()

def filter_categories(self, queryset, name, value):
return queryset.filter(check_metadata__Categories__contains=[value])

def filter_categories_in(self, queryset, name, value):
query = Q()
for category in value:
query |= Q(check_metadata__Categories__contains=[category])
return queryset.filter(query)


class TenantFilter(FilterSet):
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
Expand Down Expand Up @@ -656,6 +668,18 @@ def maybe_date_to_datetime(value):


class LatestFindingFilter(CommonFindingFilters):
categories = CharFilter(method="filter_categories")
categories__in = CharInFilter(method="filter_categories_in")

def filter_categories(self, queryset, name, value):
return queryset.filter(check_metadata__Categories__contains=[value])

def filter_categories_in(self, queryset, name, value):
query = Q()
for category in value:
query |= Q(check_metadata__Categories__contains=[category])
return queryset.filter(query)

class Meta:
model = Finding
fields = {
Expand Down
9 changes: 9 additions & 0 deletions api/src/backend/api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,10 +381,19 @@ def get_findings_metadata_no_aggregations(tenant_id: str, filtered_queryset):
regions = sorted({region for region in aggregation["regions"] or [] if region})
resource_types = sorted(set(aggregation["resource_types"] or []))

# Extract unique categories from check_metadata
categories = set()
for finding in filtered_queryset.only("check_metadata"):
check_categories = finding.check_metadata.get("categories", [])
if check_categories:
categories.update(check_categories)
categories = sorted(list(categories))

result = {
"services": services,
"regions": regions,
"resource_types": resource_types,
"categories": categories,
}

serializer = FindingMetadataSerializer(data=result)
Expand Down
1 change: 1 addition & 0 deletions api/src/backend/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1340,6 +1340,7 @@ class FindingMetadataSerializer(serializers.Serializer):
resource_types = serializers.ListField(
child=serializers.CharField(), allow_empty=True
)
categories = serializers.ListField(child=serializers.CharField(), allow_empty=True)
# Temporarily disabled until we implement tag filtering in the UI
# tags = serializers.JSONField(help_text="Tags are described as key-value pairs.")

Expand Down
22 changes: 22 additions & 0 deletions api/src/backend/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2702,10 +2702,19 @@ def metadata(self, request):
.order_by("resource_type")
)

# Extract unique categories from check_metadata
categories = set()
for finding in filtered_queryset.only("check_metadata"):
check_categories = finding.check_metadata.get("categories", [])
if check_categories:
categories.update(check_categories)
categories = sorted(list(categories))

result = {
"services": services,
"regions": regions,
"resource_types": resource_types,
"categories": categories,
}

serializer = self.get_serializer(data=result)
Expand Down Expand Up @@ -2810,10 +2819,23 @@ def metadata_latest(self, request):
.order_by("resource_type")
)

# Extract unique categories from check_metadata
categories = set()
for finding in (
self.filter_queryset(self.get_queryset())
.filter(tenant_id=tenant_id, scan_id__in=latest_scans_ids)
.only("check_metadata")
):
check_categories = finding.check_metadata.get("categories", [])
if check_categories:
categories.update(check_categories)
categories = sorted(list(categories))

result = {
"services": services,
"regions": regions,
"resource_types": resource_types,
"categories": categories,
}

serializer = self.get_serializer(data=result)
Expand Down
25 changes: 25 additions & 0 deletions dashboard/lib/dropdowns.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,28 @@ def create_table_row_dropdown(table_rows: list) -> html.Div:
),
],
)


def create_category_dropdown(categories: list) -> html.Div:
"""
Dropdown to select the category.
Args:
categories (list): List of categories.
Returns:
html.Div: Dropdown to select the category.
"""
return html.Div(
[
html.Label(
"Category:", className="text-prowler-stone-900 font-bold text-sm"
),
dcc.Dropdown(
id="category-filter",
options=[{"label": i, "value": i} for i in categories],
value=["All"],
clearable=False,
multi=True,
style={"color": "#000000"},
),
],
)
4 changes: 3 additions & 1 deletion dashboard/lib/layouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def create_layout_overview(
provider_dropdown: html.Div,
table_row_dropdown: html.Div,
status_dropdown: html.Div,
category_dropdown: html.Div,
table_div_header: html.Div,
amount_providers: int,
) -> html.Div:
Expand Down Expand Up @@ -51,8 +52,9 @@ def create_layout_overview(
html.Div([service_dropdown], className=""),
html.Div([provider_dropdown], className=""),
html.Div([status_dropdown], className=""),
html.Div([category_dropdown], className=""),
],
className="grid gap-x-4 mb-[30px] sm:grid-cols-2 lg:grid-cols-4",
className="grid gap-x-4 mb-[30px] sm:grid-cols-2 lg:grid-cols-5",
),
html.Div(
[
Expand Down
58 changes: 58 additions & 0 deletions dashboard/pages/overview.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from dashboard.lib.cards import create_provider_card
from dashboard.lib.dropdowns import (
create_account_dropdown,
create_category_dropdown,
create_date_dropdown,
create_provider_dropdown,
create_region_dropdown,
Expand Down Expand Up @@ -336,6 +337,19 @@ def load_csv_files(csv_files):
status = [x for x in status if str(x) != "nan" and x.__class__.__name__ == "str"]

status_dropdown = create_status_dropdown(status)

# Create the category dropdown
categories = []
if "CATEGORIES" in data.columns:
for cat_list in data["CATEGORIES"].dropna().unique():
if cat_list and str(cat_list) != "nan":
for cat in str(cat_list).split(","):
cat = cat.strip()
if cat and cat not in categories:
categories.append(cat)
categories = ["All"] + sorted(categories)
category_dropdown = create_category_dropdown(categories)

table_div_header = []
table_div_header.append(
html.Div(
Expand Down Expand Up @@ -497,6 +511,7 @@ def load_csv_files(csv_files):
provider_dropdown,
table_row_dropdown,
status_dropdown,
category_dropdown,
table_div_header,
len(data["PROVIDER"].unique()),
)
Expand Down Expand Up @@ -532,6 +547,8 @@ def load_csv_files(csv_files):
Output("table-rows", "options"),
Output("status-filter", "value"),
Output("status-filter", "options"),
Output("category-filter", "value"),
Output("category-filter", "options"),
Output("aws_card", "n_clicks"),
Output("azure_card", "n_clicks"),
Output("gcp_card", "n_clicks"),
Expand All @@ -548,6 +565,7 @@ def load_csv_files(csv_files):
Input("provider-filter", "value"),
Input("table-rows", "value"),
Input("status-filter", "value"),
Input("category-filter", "value"),
Input("search-input", "value"),
Input("aws_card", "n_clicks"),
Input("azure_card", "n_clicks"),
Expand All @@ -572,6 +590,7 @@ def filter_data(
provider_values,
table_row_values,
status_values,
category_values,
search_value,
aws_clicks,
azure_clicks,
Expand Down Expand Up @@ -931,6 +950,41 @@ def filter_data(

status_filter_options = ["All"] + list(filtered_data["STATUS"].unique())

# Filter Category
if "CATEGORIES" in filtered_data.columns:
if category_values == ["All"]:
updated_category_values = None
elif "All" in category_values and len(category_values) > 1:
category_values.remove("All")
updated_category_values = category_values
elif len(category_values) == 0:
updated_category_values = None
category_values = ["All"]
else:
updated_category_values = category_values

if updated_category_values:
filtered_data = filtered_data[
filtered_data["CATEGORIES"].apply(
lambda x: any(
cat.strip() in updated_category_values
for cat in str(x).split(",")
if str(x) != "nan"
)
)
]

category_filter_options = ["All"]
for cat_list in filtered_data["CATEGORIES"].dropna().unique():
if cat_list and str(cat_list) != "nan":
for cat in str(cat_list).split(","):
cat = cat.strip()
if cat and cat not in category_filter_options:
category_filter_options.append(cat)
category_filter_options = sorted(category_filter_options)
else:
category_filter_options = ["All"]

if len(filtered_data_sp) == 0:
fig = px.pie()
fig.update_layout(
Expand Down Expand Up @@ -1464,6 +1518,8 @@ def filter_data(
table_row_options,
status_values,
status_filter_options,
category_values,
category_filter_options,
aws_clicks,
azure_clicks,
gcp_clicks,
Expand Down Expand Up @@ -1499,6 +1555,8 @@ def filter_data(
table_row_options,
status_values,
status_filter_options,
category_values,
category_filter_options,
aws_clicks,
azure_clicks,
gcp_clicks,
Expand Down
102 changes: 102 additions & 0 deletions docs/user-guide/cli/tutorials/dashboard-category-filter.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
title: 'Dashboard Category Filter'
---

## Overview

The Prowler Dashboard includes a **Category** filter that allows you to filter findings by their assigned categories. This is particularly useful when you want to focus on specific security concerns like internet-exposed resources, encryption issues, or logging configurations.

## Using the Category Filter

### Step 1: Run Prowler with Categories

First, run Prowler with the `--categories` flag to generate findings for specific categories:

```sh
prowler aws --categories internet-exposed
```

This will scan your AWS environment and identify all resources that are exposed to the internet.

### Step 2: Launch the Dashboard

Start the Prowler dashboard:

```sh
prowler dashboard
```

### Step 3: Apply Category Filter

In the dashboard's Overview page, you'll find a **Category** dropdown filter alongside other filters like Severity, Service, Provider, and Status.

<img src="/images/cli/dashboard/dashboard-overview.png" />

The Category filter allows you to:

- Select **All** to view findings from all categories
- Select one or more specific categories to filter results
- Combine category filtering with other filters for precise analysis

## Available Categories

Common categories include:

- `internet-exposed` - Resources accessible from the internet
- `encryption` - Encryption-related findings
- `logging` - Logging and monitoring configurations
- `secrets` - Secrets management issues
- `forensics-ready` - Forensic readiness checks
- `trustboundaries` - Trust boundary violations
- And more...

To see all available categories for a provider:

```sh
prowler <provider> --list-categories
```

## Example Use Cases

### Filter Internet-Exposed Resources

```sh
# Run scan for internet-exposed resources
prowler aws --categories internet-exposed

# Launch dashboard
prowler dashboard

# In the dashboard, select "internet-exposed" from the Category dropdown
```

### Multiple Categories

You can scan and filter by multiple categories:

```sh
prowler aws --categories internet-exposed,encryption
```

Then use the Category filter in the dashboard to view findings from either or both categories.

## Category Filter Behavior

- **Default**: Set to "All" (shows all findings regardless of category)
- **Multi-select**: You can select multiple categories simultaneously
- **Dynamic**: The available categories update based on your current data and other active filters
- **Comma-separated**: Findings can belong to multiple categories, and the filter handles this automatically

## Integration with Other Filters

The Category filter works seamlessly with other dashboard filters:

```sh
# Example: View only FAIL status findings in the internet-exposed category
# 1. Run: prowler aws --categories internet-exposed
# 2. Launch dashboard
# 3. Set Status filter to "FAIL"
# 4. Set Category filter to "internet-exposed"
```

This allows for powerful, multi-dimensional analysis of your security posture.
2 changes: 2 additions & 0 deletions docs/user-guide/cli/tutorials/dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ This page allows for multiple functions:
* Region
* Severity
* Service
* Provider
* Status
* Category

* See which files has been scanned to generate the dashboard by placing your mouse on the `?` icon:

Expand Down
Loading