From 4c54de092fa028222c3c2f07a868595946a89c0f Mon Sep 17 00:00:00 2001 From: Prowler Bot Date: Fri, 17 Jan 2025 14:16:51 +0100 Subject: [PATCH] feat(findings): Add resource_tag filters for findings endpoint (#6587) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Víctor Fernández Poyatos --- api/src/backend/api/filters.py | 37 +++++++ api/src/backend/api/specs/v1.yaml | 135 ++++++++++++++++++++++++ api/src/backend/api/tests/test_views.py | 13 ++- api/src/backend/api/v1/views.py | 9 +- 4 files changed, 191 insertions(+), 3 deletions(-) diff --git a/api/src/backend/api/filters.py b/api/src/backend/api/filters.py index 87483581d7..1042c3aa86 100644 --- a/api/src/backend/api/filters.py +++ b/api/src/backend/api/filters.py @@ -319,6 +319,27 @@ class FindingFilter(FilterSet): field_name="resources__type", lookup_expr="icontains" ) + resource_tag_key = CharFilter(field_name="resources__tags__key") + resource_tag_key__in = CharInFilter( + field_name="resources__tags__key", lookup_expr="in" + ) + resource_tag_key__icontains = CharFilter( + field_name="resources__tags__key", lookup_expr="icontains" + ) + resource_tag_value = CharFilter(field_name="resources__tags__value") + resource_tag_value__in = CharInFilter( + field_name="resources__tags__value", lookup_expr="in" + ) + resource_tag_value__icontains = CharFilter( + field_name="resources__tags__value", lookup_expr="icontains" + ) + resource_tags = CharInFilter( + method="filter_resource_tag", + lookup_expr="in", + help_text="Filter by resource tags `key:value` pairs.\nMultiple values may be " + "separated by commas.", + ) + scan = UUIDFilter(method="filter_scan_id") scan__in = UUIDInFilter(method="filter_scan_id_in") @@ -353,6 +374,12 @@ class FindingFilter(FilterSet): }, } + @property + def qs(self): + # Force distinct results to prevent duplicates with many-to-many relationships + parent_qs = super().qs + return parent_qs.distinct() + # Convert filter values to UUIDv7 values for use with partitioning def filter_scan_id(self, queryset, name, value): try: @@ -426,6 +453,16 @@ class FindingFilter(FilterSet): return queryset.filter(id__lte=end).filter(inserted_at__lte=value) + def filter_resource_tag(self, queryset, name, value): + overall_query = Q() + for key_value_pair in value: + tag_key, tag_value = key_value_pair.split(":", 1) + overall_query |= Q( + resources__tags__key__icontains=tag_key, + resources__tags__value__icontains=tag_value, + ) + return queryset.filter(overall_query).distinct() + @staticmethod def maybe_date_to_datetime(value): dt = value diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 966f09a6ec..6cccee8404 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -477,6 +477,51 @@ paths: description: Multiple values may be separated by commas. explode: false style: form + - in: query + name: filter[resource_tag_key] + schema: + type: string + - in: query + name: filter[resource_tag_key__icontains] + schema: + type: string + - in: query + name: filter[resource_tag_key__in] + schema: + type: array + items: + type: string + description: Multiple values may be separated by commas. + explode: false + style: form + - in: query + name: filter[resource_tag_value] + schema: + type: string + - in: query + name: filter[resource_tag_value__icontains] + schema: + type: string + - in: query + name: filter[resource_tag_value__in] + schema: + type: array + items: + type: string + description: Multiple values may be separated by commas. + explode: false + style: form + - in: query + name: filter[resource_tags] + schema: + type: array + items: + type: string + description: |- + Filter by resource tags `key:value` pairs. + Multiple values may be separated by commas. + explode: false + style: form - in: query name: filter[resource_type] schema: @@ -983,6 +1028,51 @@ paths: description: Multiple values may be separated by commas. explode: false style: form + - in: query + name: filter[resource_tag_key] + schema: + type: string + - in: query + name: filter[resource_tag_key__icontains] + schema: + type: string + - in: query + name: filter[resource_tag_key__in] + schema: + type: array + items: + type: string + description: Multiple values may be separated by commas. + explode: false + style: form + - in: query + name: filter[resource_tag_value] + schema: + type: string + - in: query + name: filter[resource_tag_value__icontains] + schema: + type: string + - in: query + name: filter[resource_tag_value__in] + schema: + type: array + items: + type: string + description: Multiple values may be separated by commas. + explode: false + style: form + - in: query + name: filter[resource_tags] + schema: + type: array + items: + type: string + description: |- + Filter by resource tags `key:value` pairs. + Multiple values may be separated by commas. + explode: false + style: form - in: query name: filter[resource_type] schema: @@ -1410,6 +1500,51 @@ paths: description: Multiple values may be separated by commas. explode: false style: form + - in: query + name: filter[resource_tag_key] + schema: + type: string + - in: query + name: filter[resource_tag_key__icontains] + schema: + type: string + - in: query + name: filter[resource_tag_key__in] + schema: + type: array + items: + type: string + description: Multiple values may be separated by commas. + explode: false + style: form + - in: query + name: filter[resource_tag_value] + schema: + type: string + - in: query + name: filter[resource_tag_value__icontains] + schema: + type: string + - in: query + name: filter[resource_tag_value__in] + schema: + type: array + items: + type: string + description: Multiple values may be separated by commas. + explode: false + style: form + - in: query + name: filter[resource_tags] + schema: + type: array + items: + type: string + description: |- + Filter by resource tags `key:value` pairs. + Multiple values may be separated by commas. + explode: false + style: form - in: query name: filter[resource_type] schema: diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index e6f19f9241..c175334f15 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -2444,6 +2444,15 @@ class TestFindingViewSet: ("search", "ec2", 2), # full text search on finding tags ("search", "value2", 2), + ("resource_tag_key", "key", 2), + ("resource_tag_key__in", "key,key2", 2), + ("resource_tag_key__icontains", "key", 2), + ("resource_tag_value", "value", 2), + ("resource_tag_value__in", "value,value2", 2), + ("resource_tag_value__icontains", "value", 2), + ("resource_tags", "key:value", 2), + ("resource_tags", "not:exists", 0), + ("resource_tags", "not:exists,key:value", 2), ] ), ) @@ -2592,7 +2601,7 @@ class TestFindingViewSet: expected_services = {"ec2", "s3"} expected_regions = {"eu-west-1", "us-east-1"} - expected_tags = {"key": "value", "key2": "value2"} + expected_tags = {"key": ["value"], "key2": ["value2"]} expected_resource_types = {"prowler-test"} assert data["data"]["type"] == "findings-metadata" @@ -2619,7 +2628,7 @@ class TestFindingViewSet: expected_services = {"s3"} expected_regions = {"eu-west-1"} - expected_tags = {"key": "value", "key2": "value2"} + expected_tags = {"key": ["value"], "key2": ["value2"]} expected_resource_types = {"prowler-test"} assert data["data"]["type"] == "findings-metadata" diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index d166a8d15a..67728a65dc 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -1414,7 +1414,14 @@ class FindingViewSet(BaseRLSViewSet): if result["tags"] is None: result["tags"] = [] - result["tags"] = {t["key"]: t["value"] for t in result["tags"]} + tags_dict = {} + for t in result["tags"]: + key, value = t["key"], t["value"] + if key not in tags_dict: + tags_dict[key] = [] + tags_dict[key].append(value) + + result["tags"] = tags_dict serializer = self.get_serializer( data=result,