diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index 29bb2d03cba..c0d30d8a978 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -1264,6 +1264,7 @@ def update(self, instance, validated_data): class EndpointSerializer(serializers.ModelSerializer): tags = TagListSerializerField(required=False) + active_finding_count = serializers.IntegerField(read_only=True) class Meta: model = Endpoint diff --git a/dojo/api_v2/views.py b/dojo/api_v2/views.py index c106d667e77..8290d932eef 100644 --- a/dojo/api_v2/views.py +++ b/dojo/api_v2/views.py @@ -12,6 +12,8 @@ from django.contrib.auth.models import Permission from django.core.exceptions import ValidationError from django.db import IntegrityError +from django.db.models import OuterRef, Value +from django.db.models.functions import Coalesce from django.db.models.query import QuerySet as DjangoQuerySet from django.http import FileResponse, HttpResponse from django.shortcuts import get_object_or_404 @@ -166,6 +168,7 @@ get_authorized_product_type_members, get_authorized_product_types, ) +from dojo.query_utils import build_count_subquery from dojo.reports.views import ( prefetch_related_findings_for_report, report_url_resolver, @@ -345,7 +348,13 @@ class EndPointViewSet( ) def get_queryset(self): - return get_authorized_endpoints(Permissions.Location_View).distinct() + active_finding_subquery = build_count_subquery( + Finding.objects.filter(endpoints=OuterRef("pk"), active=True), + group_field="endpoints", + ) + return get_authorized_endpoints(Permissions.Location_View).annotate( + active_finding_count=Coalesce(active_finding_subquery, Value(0)), + ).distinct() @extend_schema( request=serializers.ReportGenerateOptionSerializer, diff --git a/dojo/endpoint/views.py b/dojo/endpoint/views.py index caa48f02757..addd0762c4c 100644 --- a/dojo/endpoint/views.py +++ b/dojo/endpoint/views.py @@ -59,7 +59,13 @@ def process_endpoints_view(request, *, host_view=False, vulnerable=False): else: endpoints = Endpoint.objects.all() - endpoints = endpoints.prefetch_related("product", "product__tags", "tags").distinct() + active_finding_subquery = build_count_subquery( + Finding.objects.filter(endpoints=OuterRef("pk"), active=True), + group_field="endpoints", + ) + endpoints = endpoints.prefetch_related("product", "product__tags", "tags").annotate( + active_finding_count=Coalesce(active_finding_subquery, Value(0)), + ).distinct() endpoints = get_authorized_endpoints_for_queryset(Permissions.Location_View, endpoints, request.user) filter_string_matching = get_system_setting("filter_string_matching", False) filter_class = EndpointFilterWithoutObjectLookups if filter_string_matching else EndpointFilter diff --git a/dojo/filters.py b/dojo/filters.py index 3c38ce7246f..610900237dc 100644 --- a/dojo/filters.py +++ b/dojo/filters.py @@ -2866,7 +2866,11 @@ class EndpointFilterHelper(FilterSet): ("product", "product"), ("host", "host"), ("id", "id"), + ("active_finding_count", "active_finding_count"), ), + field_labels={ + "active_finding_count": "Active Findings Count", + }, ) @@ -3108,7 +3112,11 @@ class ApiEndpointFilter(DojoFilter): ("host", "host"), ("product", "product"), ("id", "id"), + ("active_finding_count", "active_finding_count"), ), + field_labels={ + "active_finding_count": "Active Findings Count", + }, ) class Meta: diff --git a/dojo/location/api/endpoint_compat.py b/dojo/location/api/endpoint_compat.py index 1f75a3bfcf5..964da4c0d4d 100644 --- a/dojo/location/api/endpoint_compat.py +++ b/dojo/location/api/endpoint_compat.py @@ -6,6 +6,8 @@ """ import datetime +from django.db.models import OuterRef, Value +from django.db.models.functions import Coalesce from django_filters import BooleanFilter, CharFilter, NumberFilter from django_filters.rest_framework import DjangoFilterBackend, FilterSet from drf_spectacular.utils import extend_schema @@ -32,6 +34,7 @@ from dojo.location.models import LocationFindingReference, LocationProductReference from dojo.location.queries import get_authorized_location_finding_reference, get_authorized_location_product_reference from dojo.location.status import FindingLocationStatus +from dojo.query_utils import build_count_subquery from dojo.url.models import URL ########## @@ -101,7 +104,11 @@ class V3EndpointCompatibleFilterSet(FilterSet): ("location__url__host", "host"), ("product__id", "product"), ("id", "id"), + ("active_finding_count", "active_finding_count"), ), + field_labels={ + "active_finding_count": "Active Findings Count", + }, ) @@ -118,6 +125,7 @@ class V3EndpointCompatibleSerializer(ModelSerializer): fragment = CharField(source="location.url.fragment") tags = TagListSerializerField(source="location.tags") location_id = IntegerField(source="location.id") + active_finding_count = IntegerField(read_only=True) class Meta: model = LocationProductReference @@ -141,7 +149,18 @@ class V3EndpointCompatibleViewSet(PrefetchListMixin, PrefetchRetrieveMixin, view def get_queryset(self): """Get authorized URLs using Endpoint authorization logic.""" - return get_authorized_location_product_reference(Permissions.Location_View).filter(location__location_type=URL.LOCATION_TYPE).distinct() + active_finding_subquery = build_count_subquery( + LocationFindingReference.objects.filter( + location=OuterRef("location"), + status=FindingLocationStatus.Active, + ), + group_field="location", + ) + return get_authorized_location_product_reference(Permissions.Location_View).filter( + location__location_type=URL.LOCATION_TYPE, + ).annotate( + active_finding_count=Coalesce(active_finding_subquery, Value(0)), + ).distinct() @extend_schema( request=serializers.ReportGenerateOptionSerializer, diff --git a/dojo/templates/dojo/endpoints.html b/dojo/templates/dojo/endpoints.html index 82af13baa3a..5bda216c888 100644 --- a/dojo/templates/dojo/endpoints.html +++ b/dojo/templates/dojo/endpoints.html @@ -87,7 +87,7 @@