Skip to content

feat(api): add compliance provider filters#11587

Open
AdriiiPRodri wants to merge 1 commit into
implement-provider-groups-filterfrom
codex/compliance-overviews-provider-filters
Open

feat(api): add compliance provider filters#11587
AdriiiPRodri wants to merge 1 commit into
implement-provider-groups-filterfrom
codex/compliance-overviews-provider-filters

Conversation

@AdriiiPRodri

@AdriiiPRodri AdriiiPRodri commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Context

Compliance overview consumers can currently request data by filter[scan_id]. This PR adds provider-scoped filtering to the compliance overview endpoints so callers can request the latest completed scan per matching provider, consistent with the provider filtering added in the parent branch.

Description

  • Add provider_id, provider_id__in, provider_type, provider_type__in, provider_groups, and provider_groups__in filters to compliance overviews.
  • Resolve provider filters through the latest completed scan per provider before aggregating compliance data.
  • Keep existing filter[scan_id] behavior for scan-scoped callers.
  • Extend OpenAPI documentation for list, metadata, and requirements actions.
  • Add API tests covering exact and multi-value provider filters, provider groups, metadata, requirements, and latest-scan behavior.

Steps to review

  1. Run cd api && uv run pytest src/backend/api/tests/test_views.py::TestComplianceOverviewViewSet -q.
  2. Run cd api && uv run ruff format --check src/backend/api/filters.py src/backend/api/v1/views.py src/backend/api/tests/test_views.py && uv run ruff check src/backend/api/filters.py src/backend/api/v1/views.py src/backend/api/tests/test_views.py.
  3. Start the local API and request /api/v1/compliance-overviews with filter[provider_id], filter[provider_id__in], filter[provider_type], filter[provider_groups], and filter[provider_groups__in].
  4. Verify /api/v1/docs#tag/Compliance-Overview/operation/api_v1_compliance_overviews_list shows the new provider filters.

Checklist

Community Checklist
  • This feature/issue is listed in here or roadmap.prowler.com
  • Is it assigned to me, if not, request it via the issue/feature in here or Prowler Community Slack

SDK/CLI

  • Are there new checks included in this PR? No
    • If so, do we need to update permissions for the provider? No

UI

  • All issue/task requirements work as expected on the UI
  • If this PR adds or updates npm dependencies, include package-health evidence (maintenance, popularity, known vulnerabilities, license, release age) and explain why existing/native alternatives are insufficient.
  • Screenshots/Video of the functionality flow (if applicable) - Mobile (X < 640px)
  • Screenshots/Video of the functionality flow (if applicable) - Table (640px > X < 1024px)
  • Screenshots/Video of the functionality flow (if applicable) - Desktop (X > 1024px)
  • Ensure new entries are added to CHANGELOG.md, if applicable.

API

  • All issue/task requirements work as expected on the API
  • Endpoint response output (if applicable)
  • EXPLAIN ANALYZE output for new/modified queries or indexes (if applicable)
  • Performance test results (if applicable)
  • Any other relevant evidence of the implementation (if applicable)
  • Verify if API specs need to be regenerated.
  • Check if version updates are required (e.g., specs, uv, etc.).
  • Ensure new entries are added to CHANGELOG.md, if applicable.

API verification performed locally:

  • uv run pytest src/backend/api/tests/test_views.py::TestComplianceOverviewViewSet -q: 30 passed, 1 xpassed.
  • uv run ruff format --check src/backend/api/filters.py src/backend/api/v1/views.py src/backend/api/tests/test_views.py && uv run ruff check src/backend/api/filters.py src/backend/api/v1/views.py src/backend/api/tests/test_views.py: passed.
  • Real local API E2E created providers, provider groups, old scans, latest scans, and compliance rows, then verified requests and responses for all new filters.
  • Chrome verification confirmed the Redoc operation documents the new filters in /api/v1/docs#tag/Compliance-Overview/operation/api_v1_compliance_overviews_list.

Performance details:

  • The provider filter path first resolves latest completed scans per matching provider with ORDER BY provider_id, -inserted_at and DISTINCT ON (provider_id), then filters compliance rows by scan_id__in. Provider filters are excluded from the compliance filterset after scan resolution so the large compliance table does not repeat provider joins.
  • Existing indexes are used in the measured query plan: scans_prov_ins_desc_idx for latest completed scan lookup and cro_scan_comp_reg_idx for compliance overview rows.
  • Latest local E2E run used 10 measured HTTP samples per path after one warm-up call:
    • GET /api/v1/compliance-overviews?filter[provider_type]=aws: median 30.82 ms, max 34.84 ms.
    • GET /api/v1/compliance-overviews?filter[provider_groups__in]=...: median 25.26 ms, max 31.24 ms.
    • GET /api/v1/compliance-overviews/requirements?filter[provider_id__in]=...&filter[compliance_id]=cis_1.4_aws: median 24.16 ms, max 31.90 ms.
  • EXPLAIN ANALYZE for the latest-scan compliance aggregation completed in 0.504 ms, with 82 shared buffer hits and no sequential scan on compliance_requirements_overviews.

License

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

- Add latest scan provider filtering to compliance overviews
- Support provider ID, type, and group filters with multi-value variants
- Cover provider filter behavior with API tests
@AdriiiPRodri AdriiiPRodri requested a review from a team as a code owner June 15, 2026 09:24
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cd2505d1-e923-463f-ba85-9906de553d30

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/compliance-overviews-provider-filters

Comment @coderabbitai help to get the list of available commands and usage tips.

@josema-xyz josema-xyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two comments, also, no CHANGELOG.

Comment on lines +4767 to +4911
def _normalize_jsonapi_params(self, query_params, exclude_keys=None):
"""Convert JSON:API filter params into django-filter keys."""
exclude_keys = exclude_keys or set()
normalized = QueryDict(mutable=True)
for key, values in query_params.lists():
normalized_key = (
key[7:-1] if key.startswith("filter[") and key.endswith("]") else key
)
normalized_key = normalized_key.replace(".", "__")
if normalized_key not in exclude_keys:
normalized.setlist(normalized_key, values)
return normalized

def _apply_compliance_filterset(self, queryset, exclude_keys=None):
normalized_params = self._normalize_jsonapi_params(
self.request.query_params,
exclude_keys=set(exclude_keys or []),
)
filterset = self.filterset_class(normalized_params, queryset=queryset)
if not filterset.is_valid():
raise ValidationError(filterset.errors)
return filterset.qs

def _csv_filter_values(self, value):
return [item.strip() for item in value.split(",") if item.strip()]

def _validate_uuid_filter_values(self, field_name, values):
try:
for value in values:
uuid.UUID(str(value))
except (TypeError, ValueError, AttributeError):
raise ValidationError({field_name: ["Enter a valid UUID."]})

def _has_provider_filters(self):
return any(
self.request.query_params.get(f"filter[{key}]")
for key in self.PROVIDER_FILTER_QUERY_KEYS
)

def _validate_scan_selection(self, scan_id, has_provider_filters):
if scan_id and has_provider_filters:
raise ValidationError(
[
{
"detail": "Use either filter[scan_id] or provider filters.",
"status": 400,
"source": {"pointer": "filter[scan_id]"},
"code": "invalid",
}
]
)

if scan_id:
self._validate_uuid_filter_values("scan_id", [scan_id])
return

if has_provider_filters:
return

raise ValidationError(
[
{
"detail": "This query parameter is required unless a provider filter is provided.",
"status": 400,
"source": {"pointer": "filter[scan_id]"},
"code": "required",
}
]
)

def _extract_provider_filters_from_params(self):
"""Extract provider filters for the Scan queryset."""
params = self.request.query_params
filters = {}
valid_provider_types = {
choice[0] for choice in Provider.ProviderChoices.choices
}

provider_id = params.get("filter[provider_id]")
if provider_id:
self._validate_uuid_filter_values("provider_id", [provider_id])
filters["provider_id"] = provider_id

provider_id_in = params.get("filter[provider_id__in]") or params.get(
"filter[provider_id.in]"
)
if provider_id_in:
values = self._csv_filter_values(provider_id_in)
self._validate_uuid_filter_values("provider_id__in", values)
filters["provider_id__in"] = values

provider_type = params.get("filter[provider_type]")
if provider_type:
if provider_type not in valid_provider_types:
raise ValidationError(
{"provider_type": f"Invalid choice: {provider_type}"}
)
filters["provider__provider"] = provider_type

provider_type_in = params.get("filter[provider_type__in]") or params.get(
"filter[provider_type.in]"
)
if provider_type_in:
values = self._csv_filter_values(provider_type_in)
invalid = [value for value in values if value not in valid_provider_types]
if invalid:
raise ValidationError(
{"provider_type__in": f"Invalid choices: {', '.join(invalid)}"}
)
filters["provider__provider__in"] = values

provider_groups = params.get("filter[provider_groups]")
if provider_groups:
self._validate_uuid_filter_values("provider_groups", [provider_groups])
filters["provider__provider_groups__id"] = provider_groups

provider_groups_in = params.get("filter[provider_groups__in]") or params.get(
"filter[provider_groups.in]"
)
if provider_groups_in:
values = self._csv_filter_values(provider_groups_in)
self._validate_uuid_filter_values("provider_groups__in", values)
filters["provider__provider_groups__id__in"] = values

return filters

def _latest_scan_ids_for_provider_filters(self):
role = get_role(self.request.user, self.request.tenant_id)
scans = Scan.all_objects.filter(
tenant_id=self.request.tenant_id,
state=StateChoices.COMPLETED,
)

if not getattr(role, Permissions.UNLIMITED_VISIBILITY.value, False):
scans = scans.filter(provider__in=get_providers(role))

provider_filters = self._extract_provider_filters_from_params()
if provider_filters:
scans = scans.filter(**provider_filters)

return (
scans.order_by("provider_id", "-inserted_at")
.distinct("provider_id")
.values_list("id", flat=True)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These helpers are almost identical to ones already in OverviewViewSet and FindingGroupViewSet. That's a third copy. Could they live in one shared place instead? They've already started to drift apart.



class ComplianceOverviewFilter(FilterSet):
class ComplianceOverviewFilter(BaseScanProviderFilter):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class inherits the provider filters, but the viewset filters providers by hand and skips them. They still show up in the API docs, but they're not what does the filtering. Is keeping both on purpose? It's hard to tell which one is the real one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants