From d70c61e7caa3350833dabeb72931251c4674fb17 Mon Sep 17 00:00:00 2001 From: svader0 Date: Wed, 26 Aug 2026 11:39:26 -0500 Subject: [PATCH] fix(api): return a boolean from endpoint_status mitigated under V3 Locations V3EndpointStatusCompatibleSerializer.get_mitigated was a copy-paste of get_date two methods above it, so it returned obj.created.date() instead of a status check. Every row of GET /api/v2/endpoint_status/ therefore reported mitigated as a date string. The blast radius is wider than the one field. get_mitigated_time and get_mitigated_by both branch on self.get_mitigated(obj), and a date is truthy, so both answered as though every status was mitigated. A customer polling for mitigated statuses got a wrong answer with no error to signal it. The fix matches the three sibling methods right below it, which already compare obj.status against the matching FindingLocationStatus member. Found while auditing the "api/v2 endpoints return 500 on V3-Locations tenants" story. It was left out of that fix because it is a separate bug with a separate blast radius. --- .../PRO__migrating_from_endpoints.md | 1 + dojo/location/api/endpoint_compat.py | 2 +- unittests/test_endpoint_init_v3.py | 53 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/docs/content/asset_modelling/locations/PRO__migrating_from_endpoints.md b/docs/content/asset_modelling/locations/PRO__migrating_from_endpoints.md index 683f4b6a560..9da37cbeb87 100644 --- a/docs/content/asset_modelling/locations/PRO__migrating_from_endpoints.md +++ b/docs/content/asset_modelling/locations/PRO__migrating_from_endpoints.md @@ -97,6 +97,7 @@ A few things behave differently from the original Endpoint API: - **Single status instead of flags.** Locations have one status at a time. If your code relied on a Finding being *both* `mitigated=True` *and* `false_positive=True` simultaneously on an Endpoint_Status, that is no longer representable — the migration picks the highest-priority flag (the order shown in the table above). - **`endpoint` field on Endpoint_Status.** The legacy `endpoint` field is reconstructed by looking up the matching Asset Reference. In rare cases where a Finding's Asset no longer matches its Location's Asset references, this field may be null. +- **`mitigated` returned a date before 3.2.400.** On releases before 3.2.400 the `mitigated` field of `/api/v2/endpoint_status/` returned the record's creation date instead of a boolean. Because a date string is truthy, `mitigated_time` and `mitigated_by` answered as though every status was mitigated. From 3.2.400 the field is a boolean that is true only when the Location status is `Mitigated`. If you poll for mitigated statuses, upgrade before you trust this field. - **Pagination and ordering.** Available ordering fields on the read-compat shim are `host`, `product`, `id`, and `active_finding_count`. If your client orders by another field, switch to one of these or move to the new Locations endpoints. ## Tags and Metadata diff --git a/dojo/location/api/endpoint_compat.py b/dojo/location/api/endpoint_compat.py index ccfc156a9bc..c31d6ceeda9 100644 --- a/dojo/location/api/endpoint_compat.py +++ b/dojo/location/api/endpoint_compat.py @@ -273,7 +273,7 @@ def get_date(self, obj) -> datetime.date | None: return obj.created.date() if obj.created else None def get_mitigated(self, obj) -> bool | None: - return obj.created.date() if obj.created else None + return obj.status == FindingLocationStatus.Mitigated def get_mitigated_time(self, obj) -> datetime.datetime | None: return obj.audit_time if self.get_mitigated(obj) else None diff --git a/unittests/test_endpoint_init_v3.py b/unittests/test_endpoint_init_v3.py index be090b4eb8b..13cbf4d9747 100644 --- a/unittests/test_endpoint_init_v3.py +++ b/unittests/test_endpoint_init_v3.py @@ -31,6 +31,7 @@ from django.urls import reverse from django.utils import timezone from openpyxl import load_workbook +from parameterized import parameterized from rest_framework.authtoken.models import Token from rest_framework.test import APIClient @@ -38,6 +39,7 @@ from dojo.finding.helper import post_process_findings_batch from dojo.github.services import github_body from dojo.jira.helper import jira_description +from dojo.location.api.endpoint_compat import V3EndpointStatusCompatibleSerializer from dojo.location.models import LocationFindingReference, LocationProductReference from dojo.location.status import FindingLocationStatus, ProductLocationStatus from dojo.models import ( @@ -335,3 +337,54 @@ def test_post_process_findings_batch_with_legacy_endpoints_under_v3(self): ) self.assertTrue(Finding.objects.filter(id=tree.finding.id).exists()) + + # ------------------------------------------------------------------ + # endpoint_status compatibility serializer + # ------------------------------------------------------------------ + # Regression: V3EndpointStatusCompatibleSerializer.get_mitigated returned obj.created.date() + # (a copy of get_date) instead of the Mitigated status, so every row reported a truthy date. + @parameterized.expand([ + (FindingLocationStatus.Active, False), + (FindingLocationStatus.Mitigated, True), + (FindingLocationStatus.FalsePositive, False), + (FindingLocationStatus.RiskAccepted, False), + (FindingLocationStatus.OutOfScope, False), + ]) + def test_endpoint_status_mitigated_is_a_bool_tracking_the_status(self, status, expected): + """``mitigated`` must be a bool that is True only for a Mitigated location.""" + tree = self._make_tree(f"mit-{status.value}") + ref = self._add_location(tree, f"loc-{status.value.lower()}.example.com", status=status) + + data = V3EndpointStatusCompatibleSerializer(ref).data + + self.assertIsInstance( + data["mitigated"], bool, + msg=f"expected a bool for status={status.value}, got {type(data['mitigated']).__name__} {data['mitigated']!r}", + ) + self.assertEqual( + data["mitigated"], expected, + msg=f"expected mitigated={expected} for status={status.value}, got {data['mitigated']!r}", + ) + + @parameterized.expand([ + (FindingLocationStatus.Active, False), + (FindingLocationStatus.Mitigated, True), + ]) + def test_endpoint_status_mitigated_time_and_by_follow_mitigated(self, status, expected): + """``mitigated_time``/``mitigated_by`` branch on get_mitigated, so they must follow it.""" + tree = self._make_tree(f"mitby-{status.value}") + ref = self._add_location(tree, f"loc-mitby-{status.value.lower()}.example.com", status=status) + ref.auditor = self.admin + ref.audit_time = timezone.now() + ref.save() + + data = V3EndpointStatusCompatibleSerializer(ref).data + + self.assertEqual( + data["mitigated_time"] is not None, expected, + msg=f"expected mitigated_time set={expected} for status={status.value}, got {data['mitigated_time']!r}", + ) + self.assertEqual( + data["mitigated_by"], self.admin.id if expected else None, + msg=f"expected mitigated_by set={expected} for status={status.value}, got {data['mitigated_by']!r}", + )