Skip to content

Commit 2fe92cf

Browse files
authored
feat(api): add check title search for finding groups (#10377)
1 parent cece2cb commit 2fe92cf

File tree

6 files changed

+65
-22
lines changed

6 files changed

+65
-22
lines changed

api/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ All notable changes to the **Prowler API** are documented in this file.
77
### 🚀 Added
88

99
- `CORS_ALLOWED_ORIGINS` configurable via environment variable [(#10355)](https://github.com/prowler-cloud/prowler/pull/10355)
10+
- Finding groups support `check_title` substring filtering [(#10377)](https://github.com/prowler-cloud/prowler/pull/10377)
1011
- Attack Paths: Tenant and provider related labels to the nodes so they can be easily filtered on custom queries [(#10308)](https://github.com/prowler-cloud/prowler/pull/10308)
1112

1213
### 🔄 Changed

api/src/backend/api/filters.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,9 @@ class FindingGroupSummaryFilter(FilterSet):
926926
check_id = CharFilter(field_name="check_id", lookup_expr="exact")
927927
check_id__in = CharInFilter(field_name="check_id", lookup_expr="in")
928928
check_id__icontains = CharFilter(field_name="check_id", lookup_expr="icontains")
929+
check_title__icontains = CharFilter(
930+
field_name="check_title", lookup_expr="icontains"
931+
)
929932

930933
# Provider filters
931934
provider_id = UUIDFilter(field_name="provider_id", lookup_expr="exact")
@@ -1025,6 +1028,9 @@ class LatestFindingGroupSummaryFilter(FilterSet):
10251028
check_id = CharFilter(field_name="check_id", lookup_expr="exact")
10261029
check_id__in = CharInFilter(field_name="check_id", lookup_expr="in")
10271030
check_id__icontains = CharFilter(field_name="check_id", lookup_expr="icontains")
1031+
check_title__icontains = CharFilter(
1032+
field_name="check_title", lookup_expr="icontains"
1033+
)
10281034

10291035
# Provider filters
10301036
provider_id = UUIDFilter(field_name="provider_id", lookup_expr="exact")

api/src/backend/api/tests/integration/test_authentication.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ def test_switch_tenant_with_invalid_token(self, create_test_user, tenants_fixtur
301301
assert invalid_tenant_response.status_code == 400
302302
assert invalid_tenant_response.json()["errors"][0]["code"] == "invalid"
303303
assert invalid_tenant_response.json()["errors"][0]["detail"] == (
304-
"Tenant does not exist or user is not a " "member."
304+
"Tenant does not exist or user is not a member."
305305
)
306306

307307

@@ -912,10 +912,9 @@ def test_api_key_becomes_invalid_when_user_deleted(self, tenants_fixture):
912912
auth_response = client.get(reverse("provider-list"), headers=api_key_headers)
913913

914914
# Must return 401 Unauthorized, not 500 Internal Server Error
915-
assert auth_response.status_code == 401, (
916-
f"Expected 401 but got {auth_response.status_code}: "
917-
f"{auth_response.json()}"
918-
)
915+
assert (
916+
auth_response.status_code == 401
917+
), f"Expected 401 but got {auth_response.status_code}: {auth_response.json()}"
919918

920919
# Verify error message is present
921920
response_json = auth_response.json()

api/src/backend/api/tests/test_apps.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
import api
1111
import api.apps as api_apps_module
1212
from api.apps import (
13-
ApiConfig,
1413
PRIVATE_KEY_FILE,
1514
PUBLIC_KEY_FILE,
1615
SIGNING_KEY_ENV,
1716
VERIFYING_KEY_ENV,
17+
ApiConfig,
1818
)
1919

2020

@@ -187,9 +187,10 @@ def test_ready_initializes_driver_for_api_process(monkeypatch):
187187
_set_argv(monkeypatch, ["gunicorn"])
188188
_set_testing(monkeypatch, False)
189189

190-
with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch(
191-
"api.attack_paths.database.init_driver"
192-
) as init_driver:
190+
with (
191+
patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None),
192+
patch("api.attack_paths.database.init_driver") as init_driver,
193+
):
193194
config.ready()
194195

195196
init_driver.assert_called_once()
@@ -200,9 +201,10 @@ def test_ready_skips_driver_for_celery(monkeypatch):
200201
_set_argv(monkeypatch, ["celery", "-A", "api"])
201202
_set_testing(monkeypatch, False)
202203

203-
with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch(
204-
"api.attack_paths.database.init_driver"
205-
) as init_driver:
204+
with (
205+
patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None),
206+
patch("api.attack_paths.database.init_driver") as init_driver,
207+
):
206208
config.ready()
207209

208210
init_driver.assert_not_called()
@@ -213,9 +215,10 @@ def test_ready_skips_driver_for_manage_py_skip_command(monkeypatch):
213215
_set_argv(monkeypatch, ["manage.py", "migrate"])
214216
_set_testing(monkeypatch, False)
215217

216-
with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch(
217-
"api.attack_paths.database.init_driver"
218-
) as init_driver:
218+
with (
219+
patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None),
220+
patch("api.attack_paths.database.init_driver") as init_driver,
221+
):
219222
config.ready()
220223

221224
init_driver.assert_not_called()
@@ -226,9 +229,10 @@ def test_ready_skips_driver_when_testing(monkeypatch):
226229
_set_argv(monkeypatch, ["gunicorn"])
227230
_set_testing(monkeypatch, True)
228231

229-
with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch(
230-
"api.attack_paths.database.init_driver"
231-
) as init_driver:
232+
with (
233+
patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None),
234+
patch("api.attack_paths.database.init_driver") as init_driver,
235+
):
232236
config.ready()
233237

234238
init_driver.assert_not_called()

api/src/backend/api/tests/test_views.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15526,6 +15526,22 @@ def test_finding_groups_check_id_icontains(
1552615526
assert len(response.json()["data"]) == 1
1552715527
assert "bucket" in response.json()["data"][0]["id"].lower()
1552815528

15529+
def test_finding_groups_check_title_icontains(
15530+
self, authenticated_client, finding_groups_fixture
15531+
):
15532+
"""Test searching check titles with icontains."""
15533+
response = authenticated_client.get(
15534+
reverse("finding-group-list"),
15535+
{
15536+
"filter[inserted_at]": TODAY,
15537+
"filter[check_title.icontains]": "public access",
15538+
},
15539+
)
15540+
assert response.status_code == status.HTTP_200_OK
15541+
data = response.json()["data"]
15542+
assert len(data) == 1
15543+
assert data[0]["id"] == "s3_bucket_public_access"
15544+
1552915545
def test_resources_not_found(self, authenticated_client):
1553015546
"""Test 404 returned for nonexistent check_id."""
1553115547
response = authenticated_client.get(

api/src/backend/api/v1/views.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@
44
import logging
55
import os
66
import time
7-
87
from collections import defaultdict
98
from copy import deepcopy
109
from datetime import datetime, timedelta, timezone
1110
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
1211
from urllib.parse import urljoin
1312

1413
import sentry_sdk
15-
1614
from allauth.socialaccount.models import SocialAccount, SocialApp
1715
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
1816
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
@@ -76,6 +74,7 @@
7674
)
7775
from rest_framework.generics import GenericAPIView, get_object_or_404
7876
from rest_framework.permissions import SAFE_METHODS
77+
from rest_framework_json_api import filters as jsonapi_filters
7978
from rest_framework_json_api.views import RelationshipView, Response
8079
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
8180
from tasks.beat import schedule_provider_scan
@@ -100,7 +99,6 @@
10099
from api.attack_paths import get_queries_for_provider, get_query_by_id
101100
from api.attack_paths import views_helpers as attack_paths_views_helpers
102101
from api.base_views import BaseRLSViewSet, BaseTenantViewset, BaseUserViewset
103-
from api.renderers import APIJSONRenderer, PlainTextRenderer
104102
from api.compliance import (
105103
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE,
106104
get_compliance_frameworks,
@@ -199,6 +197,7 @@
199197
)
200198
from api.pagination import ComplianceOverviewPagination
201199
from api.rbac.permissions import Permissions, get_providers, get_role
200+
from api.renderers import APIJSONRenderer, PlainTextRenderer
202201
from api.rls import Tenant
203202
from api.utils import (
204203
CustomOAuth2Client,
@@ -6777,13 +6776,29 @@ class FindingGroupViewSet(BaseRLSViewSet):
67776776
queryset = FindingGroupDailySummary.objects.all()
67786777
serializer_class = FindingGroupSerializer
67796778
filterset_class = FindingGroupSummaryFilter
6779+
filter_backends = [
6780+
jsonapi_filters.QueryParameterValidationFilter,
6781+
jsonapi_filters.OrderingFilter,
6782+
CustomDjangoFilterBackend,
6783+
]
67806784
http_method_names = ["get"]
67816785
required_permissions = []
67826786

67836787
def get_filterset_class(self):
6784-
"""Return appropriate filter based on action."""
6788+
"""Return the filterset class used for schema generation and the list action.
6789+
6790+
Note: The resources and latest_resources actions do not use this method
6791+
at runtime. They manually instantiate FindingGroupFilter /
6792+
LatestFindingGroupFilter against a Finding queryset (see
6793+
_get_finding_queryset). The class returned here for those actions only
6794+
affects the OpenAPI schema generated by drf-spectacular.
6795+
"""
67856796
if self.action == "latest":
67866797
return LatestFindingGroupSummaryFilter
6798+
if self.action == "resources":
6799+
return FindingGroupFilter
6800+
if self.action == "latest_resources":
6801+
return LatestFindingGroupFilter
67876802
return FindingGroupSummaryFilter
67886803

67896804
def get_queryset(self):
@@ -7237,6 +7252,7 @@ def latest(self, request):
72377252
and timing information including how long they have been failing.
72387253
""",
72397254
tags=["Finding Groups"],
7255+
filters=True,
72407256
)
72417257
@action(detail=True, methods=["get"], url_path="resources")
72427258
def resources(self, request, pk=None):
@@ -7311,6 +7327,7 @@ def resources(self, request, pk=None):
73117327
and timing information. No date filters required.
73127328
""",
73137329
tags=["Finding Groups"],
7330+
filters=True,
73147331
)
73157332
@action(
73167333
detail=False,

0 commit comments

Comments
 (0)