mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-21 18:58:04 +00:00
feat(api): add check title search for finding groups (#10377)
This commit is contained in:
@@ -7,6 +7,7 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
### 🚀 Added
|
||||
|
||||
- `CORS_ALLOWED_ORIGINS` configurable via environment variable [(#10355)](https://github.com/prowler-cloud/prowler/pull/10355)
|
||||
- Finding groups support `check_title` substring filtering [(#10377)](https://github.com/prowler-cloud/prowler/pull/10377)
|
||||
- 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)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
@@ -926,6 +926,9 @@ class FindingGroupSummaryFilter(FilterSet):
|
||||
check_id = CharFilter(field_name="check_id", lookup_expr="exact")
|
||||
check_id__in = CharInFilter(field_name="check_id", lookup_expr="in")
|
||||
check_id__icontains = CharFilter(field_name="check_id", lookup_expr="icontains")
|
||||
check_title__icontains = CharFilter(
|
||||
field_name="check_title", lookup_expr="icontains"
|
||||
)
|
||||
|
||||
# Provider filters
|
||||
provider_id = UUIDFilter(field_name="provider_id", lookup_expr="exact")
|
||||
@@ -1025,6 +1028,9 @@ class LatestFindingGroupSummaryFilter(FilterSet):
|
||||
check_id = CharFilter(field_name="check_id", lookup_expr="exact")
|
||||
check_id__in = CharInFilter(field_name="check_id", lookup_expr="in")
|
||||
check_id__icontains = CharFilter(field_name="check_id", lookup_expr="icontains")
|
||||
check_title__icontains = CharFilter(
|
||||
field_name="check_title", lookup_expr="icontains"
|
||||
)
|
||||
|
||||
# Provider filters
|
||||
provider_id = UUIDFilter(field_name="provider_id", lookup_expr="exact")
|
||||
|
||||
@@ -301,7 +301,7 @@ class TestTokenSwitchTenant:
|
||||
assert invalid_tenant_response.status_code == 400
|
||||
assert invalid_tenant_response.json()["errors"][0]["code"] == "invalid"
|
||||
assert invalid_tenant_response.json()["errors"][0]["detail"] == (
|
||||
"Tenant does not exist or user is not a " "member."
|
||||
"Tenant does not exist or user is not a member."
|
||||
)
|
||||
|
||||
|
||||
@@ -912,10 +912,9 @@ class TestAPIKeyLifecycle:
|
||||
auth_response = client.get(reverse("provider-list"), headers=api_key_headers)
|
||||
|
||||
# Must return 401 Unauthorized, not 500 Internal Server Error
|
||||
assert auth_response.status_code == 401, (
|
||||
f"Expected 401 but got {auth_response.status_code}: "
|
||||
f"{auth_response.json()}"
|
||||
)
|
||||
assert (
|
||||
auth_response.status_code == 401
|
||||
), f"Expected 401 but got {auth_response.status_code}: {auth_response.json()}"
|
||||
|
||||
# Verify error message is present
|
||||
response_json = auth_response.json()
|
||||
|
||||
@@ -10,11 +10,11 @@ from django.conf import settings
|
||||
import api
|
||||
import api.apps as api_apps_module
|
||||
from api.apps import (
|
||||
ApiConfig,
|
||||
PRIVATE_KEY_FILE,
|
||||
PUBLIC_KEY_FILE,
|
||||
SIGNING_KEY_ENV,
|
||||
VERIFYING_KEY_ENV,
|
||||
ApiConfig,
|
||||
)
|
||||
|
||||
|
||||
@@ -187,9 +187,10 @@ def test_ready_initializes_driver_for_api_process(monkeypatch):
|
||||
_set_argv(monkeypatch, ["gunicorn"])
|
||||
_set_testing(monkeypatch, False)
|
||||
|
||||
with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch(
|
||||
"api.attack_paths.database.init_driver"
|
||||
) as init_driver:
|
||||
with (
|
||||
patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None),
|
||||
patch("api.attack_paths.database.init_driver") as init_driver,
|
||||
):
|
||||
config.ready()
|
||||
|
||||
init_driver.assert_called_once()
|
||||
@@ -200,9 +201,10 @@ def test_ready_skips_driver_for_celery(monkeypatch):
|
||||
_set_argv(monkeypatch, ["celery", "-A", "api"])
|
||||
_set_testing(monkeypatch, False)
|
||||
|
||||
with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch(
|
||||
"api.attack_paths.database.init_driver"
|
||||
) as init_driver:
|
||||
with (
|
||||
patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None),
|
||||
patch("api.attack_paths.database.init_driver") as init_driver,
|
||||
):
|
||||
config.ready()
|
||||
|
||||
init_driver.assert_not_called()
|
||||
@@ -213,9 +215,10 @@ def test_ready_skips_driver_for_manage_py_skip_command(monkeypatch):
|
||||
_set_argv(monkeypatch, ["manage.py", "migrate"])
|
||||
_set_testing(monkeypatch, False)
|
||||
|
||||
with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch(
|
||||
"api.attack_paths.database.init_driver"
|
||||
) as init_driver:
|
||||
with (
|
||||
patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None),
|
||||
patch("api.attack_paths.database.init_driver") as init_driver,
|
||||
):
|
||||
config.ready()
|
||||
|
||||
init_driver.assert_not_called()
|
||||
@@ -226,9 +229,10 @@ def test_ready_skips_driver_when_testing(monkeypatch):
|
||||
_set_argv(monkeypatch, ["gunicorn"])
|
||||
_set_testing(monkeypatch, True)
|
||||
|
||||
with patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None), patch(
|
||||
"api.attack_paths.database.init_driver"
|
||||
) as init_driver:
|
||||
with (
|
||||
patch.object(ApiConfig, "_ensure_crypto_keys", return_value=None),
|
||||
patch("api.attack_paths.database.init_driver") as init_driver,
|
||||
):
|
||||
config.ready()
|
||||
|
||||
init_driver.assert_not_called()
|
||||
|
||||
@@ -15526,6 +15526,22 @@ class TestFindingGroupViewSet:
|
||||
assert len(response.json()["data"]) == 1
|
||||
assert "bucket" in response.json()["data"][0]["id"].lower()
|
||||
|
||||
def test_finding_groups_check_title_icontains(
|
||||
self, authenticated_client, finding_groups_fixture
|
||||
):
|
||||
"""Test searching check titles with icontains."""
|
||||
response = authenticated_client.get(
|
||||
reverse("finding-group-list"),
|
||||
{
|
||||
"filter[inserted_at]": TODAY,
|
||||
"filter[check_title.icontains]": "public access",
|
||||
},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert len(data) == 1
|
||||
assert data[0]["id"] == "s3_bucket_public_access"
|
||||
|
||||
def test_resources_not_found(self, authenticated_client):
|
||||
"""Test 404 returned for nonexistent check_id."""
|
||||
response = authenticated_client.get(
|
||||
|
||||
@@ -4,7 +4,6 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
@@ -12,7 +11,6 @@ from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import sentry_sdk
|
||||
|
||||
from allauth.socialaccount.models import SocialAccount, SocialApp
|
||||
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
|
||||
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
|
||||
@@ -76,6 +74,7 @@ from rest_framework.exceptions import (
|
||||
)
|
||||
from rest_framework.generics import GenericAPIView, get_object_or_404
|
||||
from rest_framework.permissions import SAFE_METHODS
|
||||
from rest_framework_json_api import filters as jsonapi_filters
|
||||
from rest_framework_json_api.views import RelationshipView, Response
|
||||
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
|
||||
from tasks.beat import schedule_provider_scan
|
||||
@@ -100,7 +99,6 @@ from api.attack_paths import database as graph_database
|
||||
from api.attack_paths import get_queries_for_provider, get_query_by_id
|
||||
from api.attack_paths import views_helpers as attack_paths_views_helpers
|
||||
from api.base_views import BaseRLSViewSet, BaseTenantViewset, BaseUserViewset
|
||||
from api.renderers import APIJSONRenderer, PlainTextRenderer
|
||||
from api.compliance import (
|
||||
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE,
|
||||
get_compliance_frameworks,
|
||||
@@ -199,6 +197,7 @@ from api.models import (
|
||||
)
|
||||
from api.pagination import ComplianceOverviewPagination
|
||||
from api.rbac.permissions import Permissions, get_providers, get_role
|
||||
from api.renderers import APIJSONRenderer, PlainTextRenderer
|
||||
from api.rls import Tenant
|
||||
from api.utils import (
|
||||
CustomOAuth2Client,
|
||||
@@ -6777,13 +6776,29 @@ class FindingGroupViewSet(BaseRLSViewSet):
|
||||
queryset = FindingGroupDailySummary.objects.all()
|
||||
serializer_class = FindingGroupSerializer
|
||||
filterset_class = FindingGroupSummaryFilter
|
||||
filter_backends = [
|
||||
jsonapi_filters.QueryParameterValidationFilter,
|
||||
jsonapi_filters.OrderingFilter,
|
||||
CustomDjangoFilterBackend,
|
||||
]
|
||||
http_method_names = ["get"]
|
||||
required_permissions = []
|
||||
|
||||
def get_filterset_class(self):
|
||||
"""Return appropriate filter based on action."""
|
||||
"""Return the filterset class used for schema generation and the list action.
|
||||
|
||||
Note: The resources and latest_resources actions do not use this method
|
||||
at runtime. They manually instantiate FindingGroupFilter /
|
||||
LatestFindingGroupFilter against a Finding queryset (see
|
||||
_get_finding_queryset). The class returned here for those actions only
|
||||
affects the OpenAPI schema generated by drf-spectacular.
|
||||
"""
|
||||
if self.action == "latest":
|
||||
return LatestFindingGroupSummaryFilter
|
||||
if self.action == "resources":
|
||||
return FindingGroupFilter
|
||||
if self.action == "latest_resources":
|
||||
return LatestFindingGroupFilter
|
||||
return FindingGroupSummaryFilter
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -7237,6 +7252,7 @@ class FindingGroupViewSet(BaseRLSViewSet):
|
||||
and timing information including how long they have been failing.
|
||||
""",
|
||||
tags=["Finding Groups"],
|
||||
filters=True,
|
||||
)
|
||||
@action(detail=True, methods=["get"], url_path="resources")
|
||||
def resources(self, request, pk=None):
|
||||
@@ -7311,6 +7327,7 @@ class FindingGroupViewSet(BaseRLSViewSet):
|
||||
and timing information. No date filters required.
|
||||
""",
|
||||
tags=["Finding Groups"],
|
||||
filters=True,
|
||||
)
|
||||
@action(
|
||||
detail=False,
|
||||
|
||||
Reference in New Issue
Block a user