Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fbb8e72
docs: add ResourceGroup field specification to developer guide
AdriiiPRodri Dec 23, 2025
a706182
chore: rename identity to IAM
AdriiiPRodri Dec 23, 2025
52750ad
feat(api): add resource group overview endpoint and filtering
AdriiiPRodri Dec 24, 2025
8d03474
test(api): add unit tests
AdriiiPRodri Dec 26, 2025
7b8ff8d
chore: update changelog
AdriiiPRodri Dec 26, 2025
00c3f13
chore: restore v1
AdriiiPRodri Dec 26, 2025
7da6735
refactor(api): remove fallback
AdriiiPRodri Dec 26, 2025
ab0613b
refactor(api): change to TextField
AdriiiPRodri Dec 26, 2025
4a7c243
chore: remove duplicated entry
AdriiiPRodri Dec 26, 2025
f79f1d3
feat(api): add resource_group field to Resource model
Alan-TheGentleman Dec 31, 2025
6482535
refactor(api): remove resource_group backfill migration
Alan-TheGentleman Dec 31, 2025
c8d8516
feat(api): add resource_groups to resources metadata endpoints
Alan-TheGentleman Dec 31, 2025
4447382
refactor(api): rename resource_group to group in Resource model
Alan-TheGentleman Jan 7, 2026
e9fbf58
refactor(api): rename resource_group to group across Finding and Summ…
Alan-TheGentleman Jan 8, 2026
04b9874
refactor(api): rename resource_group to group
AdriiiPRodri Jan 9, 2026
842f4c2
Merge branch 'master' into PROWLER-37-resource-inventory-component-api
vicferpoy Jan 13, 2026
9cf543f
feat: add resource group model and fields
vicferpoy Jan 14, 2026
84347fd
feat: add resource group overview endpoint and filters
vicferpoy Jan 14, 2026
4d4fa1b
feat: implement resource group aggregation in scan and backfill
vicferpoy Jan 14, 2026
5791b03
test: add resource group feature tests
vicferpoy Jan 14, 2026
bdf5751
docs: update API spec and changelog for resource groups
vicferpoy Jan 14, 2026
cfa194d
style: apply ruff
vicferpoy Jan 14, 2026
a3581f5
fix: use kebab-case for response objects
vicferpoy Jan 14, 2026
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
2 changes: 2 additions & 0 deletions api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ All notable changes to the **Prowler API** are documented in this file.
### Added
- `/api/v1/overviews/compliance-watchlist` to retrieve the compliance watchlist [(#9596)](https://github.com/prowler-cloud/prowler/pull/9596)
- Support AlibabaCloud provider [(#9485)](https://github.com/prowler-cloud/prowler/pull/9485)
- `/api/v1/overviews/resource-groups` to retrieve an overview of the resource groups based on finding severities [(#9694)](https://github.com/prowler-cloud/prowler/pull/9694)
- Endpoints `GET /findings` and `GET /findings/metadata/latest` now support the `group` filter [(#9694)](https://github.com/prowler-cloud/prowler/pull/9694)
- `provider_id` and `provider_id__in` filter aliases for findings endpoints to enable consistent frontend parameter naming [(#9701)](https://github.com/prowler-cloud/prowler/pull/9701)

---
Expand Down
34 changes: 34 additions & 0 deletions api/src/backend/api/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
Role,
Scan,
ScanCategorySummary,
ScanGroupSummary,
ScanSummary,
SeverityChoices,
StateChoices,
Expand Down Expand Up @@ -214,6 +215,9 @@ class CommonFindingFilters(FilterSet):
category = CharFilter(method="filter_category")
category__in = CharInFilter(field_name="categories", lookup_expr="overlap")

resource_groups = CharFilter(field_name="resource_groups", lookup_expr="exact")
resource_groups__in = CharInFilter(field_name="resource_groups", lookup_expr="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 @@ -439,6 +443,8 @@ class ResourceFilter(ProviderRelationshipFilterSet):
updated_at = DateFilter(field_name="updated_at", lookup_expr="date")
scan = UUIDFilter(field_name="provider__scan", lookup_expr="exact")
scan__in = UUIDInFilter(field_name="provider__scan", lookup_expr="in")
groups = CharFilter(method="filter_groups")
groups__in = CharInFilter(field_name="groups", lookup_expr="overlap")

class Meta:
model = Resource
Expand All @@ -453,6 +459,9 @@ class Meta:
"updated_at": ["gte", "lte"],
}

def filter_groups(self, queryset, name, value):
return queryset.filter(groups__contains=[value])

def filter_queryset(self, queryset):
if not (self.data.get("scan") or self.data.get("scan__in")) and not (
self.data.get("updated_at")
Expand Down Expand Up @@ -517,6 +526,8 @@ class LatestResourceFilter(ProviderRelationshipFilterSet):
tag_value = CharFilter(method="filter_tag_value")
tag = CharFilter(method="filter_tag")
tags = CharFilter(method="filter_tag")
groups = CharFilter(method="filter_groups")
groups__in = CharInFilter(field_name="groups", lookup_expr="overlap")

class Meta:
model = Resource
Expand All @@ -529,6 +540,9 @@ class Meta:
"type": ["exact", "icontains", "in"],
}

def filter_groups(self, queryset, name, value):
return queryset.filter(groups__contains=[value])

def filter_tag_key(self, queryset, name, value):
return queryset.filter(Q(tags__key=value) | Q(tags__key__icontains=value))

Expand Down Expand Up @@ -1154,6 +1168,26 @@ class CategoryOverviewFilter(BaseScanProviderFilter):

class Meta(BaseScanProviderFilter.Meta):
model = ScanCategorySummary
fields = {}


class ResourceGroupOverviewFilter(FilterSet):
provider_id = UUIDFilter(field_name="scan__provider__id", lookup_expr="exact")
provider_id__in = UUIDInFilter(field_name="scan__provider__id", lookup_expr="in")
provider_type = ChoiceFilter(
field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices
)
provider_type__in = ChoiceInFilter(
field_name="scan__provider__provider",
choices=Provider.ProviderChoices.choices,
lookup_expr="in",
)
resource_group = CharFilter(field_name="resource_group", lookup_expr="exact")
resource_group__in = CharInFilter(field_name="resource_group", lookup_expr="in")

class Meta:
model = ScanGroupSummary
fields = {}


class ComplianceWatchlistFilter(BaseProviderFilter):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import uuid

import django.db.models.deletion
from django.db import migrations, models

import api.db_utils
import api.rls


class Migration(migrations.Migration):
dependencies = [
("api", "0067_tenant_compliance_summary"),
]

operations = [
migrations.AddField(
model_name="finding",
name="resource_groups",
field=models.TextField(
blank=True,
help_text="Resource group from check metadata for efficient filtering",
null=True,
),
),
migrations.CreateModel(
name="ScanGroupSummary",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="api.tenant",
),
),
(
"inserted_at",
models.DateTimeField(auto_now_add=True),
),
(
"scan",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="resource_group_summaries",
related_query_name="resource_group_summary",
to="api.scan",
),
),
(
"resource_group",
models.CharField(max_length=50),
),
(
"severity",
api.db_utils.SeverityEnumField(
choices=[
("critical", "Critical"),
("high", "High"),
("medium", "Medium"),
("low", "Low"),
("informational", "Informational"),
],
),
),
(
"total_findings",
models.IntegerField(
default=0, help_text="Non-muted findings (PASS + FAIL)"
),
),
(
"failed_findings",
models.IntegerField(
default=0,
help_text="Non-muted FAIL findings (subset of total_findings)",
),
),
(
"new_failed_findings",
models.IntegerField(
default=0,
help_text="Non-muted FAIL with delta='new' (subset of failed_findings)",
),
),
(
"resources_count",
models.IntegerField(
default=0, help_text="Count of distinct resource_uid values"
),
),
],
options={
"db_table": "scan_resource_group_summaries",
"abstract": False,
},
),
migrations.AddIndex(
model_name="scangroupsummary",
index=models.Index(
fields=["tenant_id", "scan"], name="srgs_tenant_scan_idx"
),
),
migrations.AddConstraint(
model_name="scangroupsummary",
constraint=models.UniqueConstraint(
fields=("tenant_id", "scan_id", "resource_group", "severity"),
name="unique_resource_group_severity_per_scan",
),
),
migrations.AddConstraint(
model_name="scangroupsummary",
constraint=api.rls.RowLevelSecurityConstraint(
field="tenant_id",
name="rls_on_scangroupsummary",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
),
]
21 changes: 21 additions & 0 deletions api/src/backend/api/migrations/0069_resource_resource_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.contrib.postgres.fields import ArrayField
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("api", "0068_finding_resource_group_scangroupsummary"),
]

operations = [
migrations.AddField(
model_name="resource",
name="groups",
field=ArrayField(
models.CharField(max_length=100),
blank=True,
help_text="Groups for categorization (e.g., compute, storage, IAM)",
null=True,
),
),
]
72 changes: 72 additions & 0 deletions api/src/backend/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,12 @@ class Resource(RowLevelSecurityProtectedModel):
metadata = models.TextField(blank=True, null=True)
details = models.TextField(blank=True, null=True)
partition = models.TextField(blank=True, null=True)
groups = ArrayField(
models.CharField(max_length=100),
blank=True,
null=True,
help_text="Groups for categorization (e.g., compute, storage, IAM)",
)

failed_findings_count = models.IntegerField(default=0)

Expand Down Expand Up @@ -890,6 +896,11 @@ class DeltaChoices(models.TextChoices):
null=True,
help_text="Categories from check metadata for efficient filtering",
)
resource_groups = models.TextField(
blank=True,
null=True,
help_text="Resource group from check metadata for efficient filtering",
)

# Relationships
scan = models.ForeignKey(to=Scan, related_name="findings", on_delete=models.CASCADE)
Expand Down Expand Up @@ -2032,6 +2043,67 @@ class JSONAPIMeta:
resource_name = "scan-category-summaries"


class ScanGroupSummary(RowLevelSecurityProtectedModel):
"""
Pre-aggregated resource group metrics per scan by severity.

Stores one row per (resource_group, severity) combination per scan for efficient
overview queries. Resource groups come from check_metadata.Group.

Count relationships (each is a subset of the previous):
- total_findings >= failed_findings >= new_failed_findings
"""

id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)

scan = models.ForeignKey(
Scan,
on_delete=models.CASCADE,
related_name="resource_group_summaries",
related_query_name="resource_group_summary",
)

resource_group = models.CharField(max_length=50)
severity = SeverityEnumField(choices=SeverityChoices)

total_findings = models.IntegerField(
default=0, help_text="Non-muted findings (PASS + FAIL)"
)
failed_findings = models.IntegerField(
default=0, help_text="Non-muted FAIL findings (subset of total_findings)"
)
new_failed_findings = models.IntegerField(
default=0,
help_text="Non-muted FAIL with delta='new' (subset of failed_findings)",
)
resources_count = models.IntegerField(
default=0, help_text="Count of distinct resource_uid values"
)

class Meta(RowLevelSecurityProtectedModel.Meta):
db_table = "scan_resource_group_summaries"

indexes = [
models.Index(fields=["tenant_id", "scan"], name="srgs_tenant_scan_idx"),
]

constraints = [
models.UniqueConstraint(
fields=("tenant_id", "scan_id", "resource_group", "severity"),
name="unique_resource_group_severity_per_scan",
),
RowLevelSecurityConstraint(
field="tenant_id",
name="rls_on_%(class)s",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
]

class JSONAPIMeta:
resource_name = "scan-resource-group-summaries"


class LighthouseConfiguration(RowLevelSecurityProtectedModel):
"""
Stores configuration and API keys for LLM services.
Expand Down
Loading
Loading