From 679fd9cf956ffff56a5cf2cf89a73bdfff954a24 Mon Sep 17 00:00:00 2001 From: Sam Vader Date: Mon, 5 Oct 2026 15:20:32 -0500 Subject: [PATCH 01/10] feat(deprecations): declare deprecations once, with a release calendar Tool Types, Tool Configurations and API Scan Configurations each carried their own copy of the removal version and date. A declaration now names a minor release, and RELEASE_DATES (copied from the release milestones) gives its day. overdue_deprecations() makes a missed removal fail the test on the release bump. --- dojo/deprecations.py | 98 ++++++++++++++++++++++++++++++++++ unittests/test_deprecations.py | 97 +++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 dojo/deprecations.py create mode 100644 unittests/test_deprecations.py diff --git a/dojo/deprecations.py b/dojo/deprecations.py new file mode 100644 index 00000000000..a3d862c51b8 --- /dev/null +++ b/dojo/deprecations.py @@ -0,0 +1,98 @@ +import re +from dataclasses import dataclass +from datetime import date + +from django.utils import translation +from django.utils.translation import gettext_lazy as _ + +# Copied from the release milestones. A deprecation names its removal release, and its +# day always comes from here. +RELEASE_DATES = { + "3.3.0": date(2026, 9, 8), + "3.4.0": date(2026, 10, 5), + "3.5.0": date(2026, 11, 2), + "3.6.0": date(2026, 12, 7), +} + +_MINOR_RELEASE = re.compile(r"\d+\.\d+\.0") + + +@dataclass(frozen=True) +class Deprecation: + key: str + title: str + removal_version: str + notice_url: str + action: str = "" + removed: bool = False + + @property + def removal_date(self) -> date: + return RELEASE_DATES[self.removal_version] + + @property + def removal_label(self) -> str: + return f"{self.removal_version} ({self.removal_date:%B %Y})" + + def message(self) -> str: + # Keep the whole sentence in English: no catalog holds it. + with translation.override(None): + return ( + f"{self.title} are deprecated and will be removed in DefectDojo {self.removal_label}. " + "Please plan to migrate away from this feature." + ) + + +_DEPRECATIONS: dict[str, Deprecation] = {} + + +def _release(version: str) -> tuple[int, ...]: + return tuple(int(part) for part in version.split("-", maxsplit=1)[0].split(".")[:3]) + + +def register_deprecation(entry: Deprecation, *, override: bool = False) -> None: + if not _MINOR_RELEASE.fullmatch(entry.removal_version): + msg = f"{entry.key}: a feature is removed in a minor release (X.Y.0), not {entry.removal_version}" + raise ValueError(msg) + if entry.removal_version not in RELEASE_DATES: + msg = f"{entry.key}: add {entry.removal_version} to RELEASE_DATES before naming it" + raise ValueError(msg) + if entry.key in _DEPRECATIONS and not override: + return + _DEPRECATIONS[entry.key] = entry + + +def get_deprecation(key: str) -> Deprecation | None: + return _DEPRECATIONS.get(key) + + +def active_deprecations() -> list[Deprecation]: + return [entry for entry in _DEPRECATIONS.values() if not entry.removed] + + +def overdue_deprecations(version: str) -> list[Deprecation]: + current = _release(version) + return [entry for entry in active_deprecations() if _release(entry.removal_version) < current] + + +_UPGRADING_3_2 = "https://docs.defectdojo.com/releases/os_upgrading/3.2/" + +register_deprecation( + Deprecation(key="tool_type", title=_("Tool Types"), removal_version="3.5.0", notice_url=_UPGRADING_3_2), +) +register_deprecation( + Deprecation( + key="tool_configuration", + title=_("Tool Configurations"), + removal_version="3.5.0", + notice_url=_UPGRADING_3_2, + ), +) +register_deprecation( + Deprecation( + key="api_scan_configuration", + title=_("API Scan Configurations"), + removal_version="3.5.0", + notice_url=_UPGRADING_3_2, + ), +) diff --git a/unittests/test_deprecations.py b/unittests/test_deprecations.py new file mode 100644 index 00000000000..0907cff6ef4 --- /dev/null +++ b/unittests/test_deprecations.py @@ -0,0 +1,97 @@ +from datetime import date +from unittest.mock import patch + +from django.test import SimpleTestCase +from django.utils import translation + +import dojo +from dojo.deprecations import ( + Deprecation, + active_deprecations, + get_deprecation, + overdue_deprecations, + register_deprecation, +) + +UPGRADING_3_2 = "https://docs.defectdojo.com/releases/os_upgrading/3.2/" + + +def widgets(**overrides): + fields = {"key": "widgets", "title": "Widgets", "removal_version": "3.5.0", "notice_url": "https://example.test"} + fields.update(overrides) + return Deprecation(**fields) + + +class TestDeprecationSchedule(SimpleTestCase): + def test_the_date_comes_from_the_release_calendar(self): + self.assertEqual(date(2026, 11, 2), widgets().removal_date) + + def test_people_read_the_version_and_the_month(self): + self.assertEqual("3.5.0 (November 2026)", widgets().removal_label) + + def test_a_patch_release_is_refused(self): + with self.assertRaisesRegex(ValueError, "minor release"): + register_deprecation(widgets(key="widgets_patch", removal_version="3.5.100")) + + def test_a_release_missing_from_the_calendar_is_refused(self): + with self.assertRaisesRegex(ValueError, "RELEASE_DATES"): + register_deprecation(widgets(key="widgets_far", removal_version="9.9.0")) + + def test_a_second_registration_does_not_replace_the_first_unless_asked(self): + first, second = widgets(), widgets(title="Gadgets") + with patch.dict("dojo.deprecations._DEPRECATIONS", {}, clear=True): + register_deprecation(first) + register_deprecation(second) + self.assertIs(first, get_deprecation("widgets")) + register_deprecation(second, override=True) + self.assertIs(second, get_deprecation("widgets")) + + def test_a_removed_feature_is_not_active(self): + gone = widgets(key="gone", removal_version="3.3.0", removed=True) + with patch.dict("dojo.deprecations._DEPRECATIONS", {"gone": gone}, clear=True): + self.assertEqual([], active_deprecations()) + + +class TestOverdue(SimpleTestCase): + def test_a_declaration_past_its_release_is_overdue(self): + old = widgets(removal_version="3.4.0") + with patch.dict("dojo.deprecations._DEPRECATIONS", {"widgets": old}, clear=True): + self.assertEqual([old], overdue_deprecations("3.5.0-dev")) + self.assertEqual([], overdue_deprecations("3.4.0-dev")) + self.assertEqual([], overdue_deprecations("3.4.0")) + + def test_a_removed_declaration_is_never_overdue(self): + gone = widgets(removal_version="3.3.0", removed=True) + with patch.dict("dojo.deprecations._DEPRECATIONS", {"gone": gone}, clear=True): + self.assertEqual([], overdue_deprecations("3.6.0-dev")) + + def test_nothing_declared_is_overdue_on_this_release_line(self): + overdue = [entry.key for entry in overdue_deprecations(dojo.__version__)] + self.assertEqual( + [], + overdue, + f"These removals are past their release on {dojo.__version__}. For each key, remove the feature and " + "delete its declaration, set removed=True while a migration path remains, or move removal_version to " + "a later release in dojo/deprecations.py.", + ) + + +class TestOpenSourceDeclarations(SimpleTestCase): + def test_the_pull_parser_features_go_in_3_5_0(self): + for key in ("tool_type", "tool_configuration", "api_scan_configuration"): + with self.subTest(key=key): + entry = get_deprecation(key) + self.assertEqual("3.5.0", entry.removal_version) + self.assertEqual(UPGRADING_3_2, entry.notice_url) + + def test_the_classic_message_uses_one_sentence_and_one_date_form(self): + self.assertEqual( + "Tool Types are deprecated and will be removed in DefectDojo 3.5.0 (November 2026). " + "Please plan to migrate away from this feature.", + get_deprecation("tool_type").message(), + ) + + def test_the_classic_message_never_mixes_two_languages(self): + with translation.override("de"): + message = get_deprecation("tool_type").message() + self.assertTrue(message.startswith("Tool Types are deprecated"), message) From c39aa87dd4221c2d327450d81a17aa6eab632b5c Mon Sep 17 00:00:00 2001 From: Sam Vader Date: Mon, 5 Oct 2026 15:22:32 -0500 Subject: [PATCH 02/10] refactor(deprecations): @deprecated_view takes a declaration key The 10 Classic UI call sites repeated "3.5.0" and "November 2026" by hand. They now name a declaration, and the message comes from it. The sentence becomes the one the Pro banner already shows: plural feature name, no "v" before the version. --- dojo/decorators.py | 24 ++++++++------- dojo/product/ui/views.py | 8 ++--- dojo/tool_config/ui/views.py | 6 ++-- dojo/tool_type/ui/views.py | 6 ++-- unittests/test_deprecations.py | 54 +++++++++++++++++++++++++++++++++- 5 files changed, 77 insertions(+), 21 deletions(-) diff --git a/dojo/decorators.py b/dojo/decorators.py index 83bbaad08e7..2dd03206a96 100644 --- a/dojo/decorators.py +++ b/dojo/decorators.py @@ -4,11 +4,13 @@ from django.conf import settings from django.contrib import messages +from django.core.exceptions import ImproperlyConfigured from django.http import Http404 from django_ratelimit import UNSAFE from django_ratelimit.core import is_ratelimited from django_ratelimit.exceptions import Ratelimited +from dojo.deprecations import get_deprecation from dojo.models import Dojo_User logger = logging.getLogger(__name__) @@ -168,26 +170,28 @@ def _wrapped(request, *args, **kw): return decorator -def deprecated_view(feature_name, removal_version="X.Y.Z", removal_date="some time in the future"): +def deprecated_view(key): """ - Decorator that adds a deprecation warning message to a view. + Show the Classic UI warning that ``dojo.deprecations`` declares for ``key``. Only adds the message on GET requests to avoid duplicate warnings when POST requests redirect. """ + if get_deprecation(key) is None: + msg = f"{key!r} has no declaration in dojo/deprecations.py" + raise ImproperlyConfigured(msg) + def decorator(func): @wraps(func) def _wrapped(request, *args, **kwargs): - if request.method == "GET": - messages.add_message( - request, - messages.WARNING, - f"{feature_name} is deprecated and will be removed in DefectDojo v{removal_version} " - f"({removal_date}). Please plan to migrate away from this feature.", - extra_tags="alert-warning", - ) + notice = get_deprecation(key) + if request.method == "GET" and notice is not None: + messages.add_message(request, messages.WARNING, notice.message(), extra_tags="alert-warning") return func(request, *args, **kwargs) + + _wrapped.deprecation = key return _wrapped + return decorator diff --git a/dojo/product/ui/views.py b/dojo/product/ui/views.py index a3ed2285b78..0db365358c3 100644 --- a/dojo/product/ui/views.py +++ b/dojo/product/ui/views.py @@ -1727,7 +1727,7 @@ def delete_product_authorized_user(request, pid, user_id): return HttpResponseRedirect(reverse("view_product", args=(pid,))) -@deprecated_view("API Scan Configuration", removal_version="3.5.0", removal_date="November 2026") +@deprecated_view("api_scan_configuration") def add_api_scan_configuration(request, pid): product = get_object_or_404(Product, id=pid) if request.method == "POST": @@ -1771,7 +1771,7 @@ def add_api_scan_configuration(request, pid): }) -@deprecated_view("API Scan Configuration", removal_version="3.5.0", removal_date="November 2026") +@deprecated_view("api_scan_configuration") def view_api_scan_configurations(request, pid): product_api_scan_configurations = Product_API_Scan_Configuration.objects.filter(product=pid) @@ -1785,7 +1785,7 @@ def view_api_scan_configurations(request, pid): }) -@deprecated_view("API Scan Configuration", removal_version="3.5.0", removal_date="November 2026") +@deprecated_view("api_scan_configuration") def edit_api_scan_configuration(request, pid, pascid): product_api_scan_configuration = get_object_or_404(Product_API_Scan_Configuration, id=pascid) @@ -1831,7 +1831,7 @@ def edit_api_scan_configuration(request, pid, pascid): }) -@deprecated_view("API Scan Configuration", removal_version="3.5.0", removal_date="November 2026") +@deprecated_view("api_scan_configuration") def delete_api_scan_configuration(request, pid, pascid): product_api_scan_configuration = get_object_or_404(Product_API_Scan_Configuration, id=pascid) diff --git a/dojo/tool_config/ui/views.py b/dojo/tool_config/ui/views.py index d960bf1c607..d3c10985178 100644 --- a/dojo/tool_config/ui/views.py +++ b/dojo/tool_config/ui/views.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) -@deprecated_view("Tool Configuration", removal_version="3.5.0", removal_date="November 2026") +@deprecated_view("tool_configuration") def new_tool_config(request): if request.method == "POST": tform = ToolConfigForm(request.POST) @@ -51,7 +51,7 @@ def new_tool_config(request): {"tform": tform}) -@deprecated_view("Tool Configuration", removal_version="3.5.0", removal_date="November 2026") +@deprecated_view("tool_configuration") def edit_tool_config(request, ttid): tool_config = Tool_Configuration.objects.get(pk=ttid) # Read before the form binds, which overwrites the instance in place. @@ -100,7 +100,7 @@ def edit_tool_config(request, ttid): }) -@deprecated_view("Tool Configuration", removal_version="3.5.0", removal_date="November 2026") +@deprecated_view("tool_configuration") def tool_config(request): confs = Tool_Configuration.objects.all().order_by("name") add_breadcrumb(title="Tool Configuration List", top_level=not len(request.GET), request=request) diff --git a/dojo/tool_type/ui/views.py b/dojo/tool_type/ui/views.py index d8a6acfaa2b..59fed703c61 100644 --- a/dojo/tool_type/ui/views.py +++ b/dojo/tool_type/ui/views.py @@ -15,7 +15,7 @@ logger = logging.getLogger(__name__) -@deprecated_view("Tool Type", removal_version="3.5.0", removal_date="November 2026") +@deprecated_view("tool_type") def new_tool_type(request): if request.method == "POST": tform = ToolTypeForm(request.POST, instance=Tool_Type()) @@ -35,7 +35,7 @@ def new_tool_type(request): return render(request, "dojo/new_tool_type.html", {"tform": tform}) -@deprecated_view("Tool Type", removal_version="3.5.0", removal_date="November 2026") +@deprecated_view("tool_type") def edit_tool_type(request, ttid): tool_type = Tool_Type.objects.get(pk=ttid) if request.method == "POST": @@ -55,7 +55,7 @@ def edit_tool_type(request, ttid): return render(request, "dojo/edit_tool_type.html", {"tform": tform}) -@deprecated_view("Tool Type", removal_version="3.5.0", removal_date="November 2026") +@deprecated_view("tool_type") def tool_type(request): confs = Tool_Type.objects.all().order_by("name") add_breadcrumb(title=_("Tool Type List"), top_level=not len(request.GET), request=request) diff --git a/unittests/test_deprecations.py b/unittests/test_deprecations.py index 0907cff6ef4..3a54a2ec149 100644 --- a/unittests/test_deprecations.py +++ b/unittests/test_deprecations.py @@ -1,10 +1,16 @@ from datetime import date from unittest.mock import patch -from django.test import SimpleTestCase +from django.contrib import messages +from django.contrib.messages import get_messages +from django.contrib.messages.storage.fallback import FallbackStorage +from django.core.exceptions import ImproperlyConfigured +from django.http import HttpResponse +from django.test import RequestFactory, SimpleTestCase from django.utils import translation import dojo +from dojo.decorators import deprecated_view from dojo.deprecations import ( Deprecation, active_deprecations, @@ -12,6 +18,9 @@ overdue_deprecations, register_deprecation, ) +from dojo.product.ui import views as product_views +from dojo.tool_config.ui import views as tool_config_views +from dojo.tool_type.ui import views as tool_type_views UPGRADING_3_2 = "https://docs.defectdojo.com/releases/os_upgrading/3.2/" @@ -95,3 +104,46 @@ def test_the_classic_message_never_mixes_two_languages(self): with translation.override("de"): message = get_deprecation("tool_type").message() self.assertTrue(message.startswith("Tool Types are deprecated"), message) + + +def ok_view(request): + return HttpResponse("ok") + + +class TestDeprecatedView(SimpleTestCase): + def shown(self, method): + request = getattr(RequestFactory(), method)("/tool_type") + request.session = {} + request._messages = FallbackStorage(request) + deprecated_view("tool_type")(ok_view)(request) + return [(message.level, message.message, message.extra_tags) for message in get_messages(request)] + + def test_a_get_shows_the_declared_warning(self): + self.assertEqual( + [(messages.WARNING, get_deprecation("tool_type").message(), "alert-warning")], + self.shown("get"), + ) + + def test_a_post_shows_nothing_so_the_redirect_does_not_repeat_it(self): + self.assertEqual([], self.shown("post")) + + def test_an_undeclared_key_fails_at_import(self): + with self.assertRaises(ImproperlyConfigured): + deprecated_view("no_such_feature") + + def test_every_deprecated_classic_view_names_its_declaration(self): + expected = { + tool_type_views.new_tool_type: "tool_type", + tool_type_views.edit_tool_type: "tool_type", + tool_type_views.tool_type: "tool_type", + tool_config_views.new_tool_config: "tool_configuration", + tool_config_views.edit_tool_config: "tool_configuration", + tool_config_views.tool_config: "tool_configuration", + product_views.add_api_scan_configuration: "api_scan_configuration", + product_views.view_api_scan_configurations: "api_scan_configuration", + product_views.edit_api_scan_configuration: "api_scan_configuration", + product_views.delete_api_scan_configuration: "api_scan_configuration", + } + for view, key in expected.items(): + with self.subTest(view=view.__name__): + self.assertEqual(key, getattr(view, "deprecation", None)) From 80c6129231db1a2e4aae6400c766ea4ddc21bf4a Mon Sep 17 00:00:00 2001 From: Sam Vader Date: Mon, 5 Oct 2026 15:28:19 -0500 Subject: [PATCH 03/10] refactor(deprecations): API deprecation headers come from the declaration The four deprecated viewsets typed datetime(2026, 11, 1) by hand, one day off the 3.5.0 milestone. X-End-Of-Life-Date is now the release day from RELEASE_DATES. A scan test fails if any other file in dojo/ names a removal release. --- dojo/api_v2/views.py | 14 ++++----- dojo/asset/api/views.py | 5 +--- dojo/product/api/views.py | 5 +--- dojo/tool_config/api/views.py | 5 +--- dojo/tool_type/api/views.py | 5 +--- unittests/test_deprecations.py | 49 ++++++++++++++++++++++++++++++++ unittests/test_rest_framework.py | 8 +++--- 7 files changed, 64 insertions(+), 27 deletions(-) diff --git a/dojo/api_v2/views.py b/dojo/api_v2/views.py index def7d4b239a..7fa1b30da4f 100644 --- a/dojo/api_v2/views.py +++ b/dojo/api_v2/views.py @@ -1,5 +1,5 @@ import logging -from datetime import datetime +from datetime import datetime, time import pghistory from dateutil.relativedelta import relativedelta @@ -37,6 +37,7 @@ ) from dojo.authorization import api_permissions as permissions from dojo.authorization.authorization import user_has_permission_or_403 +from dojo.deprecations import get_deprecation from dojo.endpoint.ui.views import get_endpoint_ids from dojo.engagement.queries import get_authorized_engagements from dojo.filters import ( @@ -166,14 +167,13 @@ class PrefetchDojoModelViewSet( class DeprecationNoticeMixin: - deprecated: bool | None = None - end_of_life_date: datetime | None = None + deprecation: str = "" def finalize_response(self, request, response, *args, **kwargs): - if self.deprecated is not None: - response["X-Deprecated"] = self.deprecated - if self.end_of_life_date is not None: - response["X-End-Of-Life-Date"] = self.end_of_life_date.isoformat() + notice = get_deprecation(self.deprecation) + if notice is not None: + response["X-Deprecated"] = True + response["X-End-Of-Life-Date"] = datetime.combine(notice.removal_date, time.min).isoformat() return super().finalize_response(request, response, *args, **kwargs) diff --git a/dojo/asset/api/views.py b/dojo/asset/api/views.py index fe5cae13013..48df2a5cf1f 100644 --- a/dojo/asset/api/views.py +++ b/dojo/asset/api/views.py @@ -1,4 +1,3 @@ -from datetime import datetime from functools import partial from django.db.models import OuterRef, Value @@ -34,14 +33,12 @@ # Authorization: object-based -# Deprecated in 3.2.0, removal planned for 3.5.0 (serves the API-based pull parsers). @extend_schema_view(**schema_with_prefetch()) class AssetAPIScanConfigurationViewSet( DeprecationNoticeMixin, PrefetchDojoModelViewSet, ): - deprecated = True - end_of_life_date = datetime(2026, 11, 1) + deprecation = "api_scan_configuration" serializer_class = serializers.AssetAPIScanConfigurationSerializer queryset = Product_API_Scan_Configuration.objects.none() filter_backends = (DjangoFilterBackend,) diff --git a/dojo/product/api/views.py b/dojo/product/api/views.py index e4a4900ef63..8d52dc59bd1 100644 --- a/dojo/product/api/views.py +++ b/dojo/product/api/views.py @@ -1,4 +1,3 @@ -from datetime import datetime from functools import partial from django.db.models import OuterRef, Value @@ -35,14 +34,12 @@ # Authorization: object-based -# Deprecated in 3.2.0, removal planned for 3.5.0 (serves the API-based pull parsers). @extend_schema_view(**schema_with_prefetch()) class ProductAPIScanConfigurationViewSet( DeprecationNoticeMixin, PrefetchDojoModelViewSet, ): - deprecated = True - end_of_life_date = datetime(2026, 11, 1) + deprecation = "api_scan_configuration" serializer_class = ProductAPIScanConfigurationSerializer queryset = Product_API_Scan_Configuration.objects.none() filter_backends = (DjangoFilterBackend,) diff --git a/dojo/tool_config/api/views.py b/dojo/tool_config/api/views.py index eb4985b0ad8..959a971d8bf 100644 --- a/dojo/tool_config/api/views.py +++ b/dojo/tool_config/api/views.py @@ -1,5 +1,4 @@ import logging -from datetime import datetime from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema_view @@ -13,14 +12,12 @@ # Authorization: configurations -# Deprecated in 3.2.0, removal planned for 3.5.0 (serves the API-based pull parsers). @extend_schema_view(**schema_with_prefetch()) class ToolConfigurationsViewSet( DeprecationNoticeMixin, PrefetchDojoModelViewSet, ): - deprecated = True - end_of_life_date = datetime(2026, 11, 1) + deprecation = "tool_configuration" serializer_class = ToolConfigurationSerializer queryset = Tool_Configuration.objects.none() filter_backends = (DjangoFilterBackend,) diff --git a/dojo/tool_type/api/views.py b/dojo/tool_type/api/views.py index fba2bbe887e..1d2615e046f 100644 --- a/dojo/tool_type/api/views.py +++ b/dojo/tool_type/api/views.py @@ -1,5 +1,4 @@ import logging -from datetime import datetime from django_filters.rest_framework import DjangoFilterBackend @@ -12,13 +11,11 @@ # Authorization: configuration -# Deprecated in 3.2.0, removal planned for 3.5.0 (serves the API-based pull parsers). class ToolTypesViewSet( DeprecationNoticeMixin, DojoModelViewSet, ): - deprecated = True - end_of_life_date = datetime(2026, 11, 1) + deprecation = "tool_type" serializer_class = ToolTypeSerializer queryset = Tool_Type.objects.none() filter_backends = (DjangoFilterBackend,) diff --git a/unittests/test_deprecations.py b/unittests/test_deprecations.py index 3a54a2ec149..107e95b5d83 100644 --- a/unittests/test_deprecations.py +++ b/unittests/test_deprecations.py @@ -1,4 +1,6 @@ +import re from datetime import date +from pathlib import Path from unittest.mock import patch from django.contrib import messages @@ -8,8 +10,12 @@ from django.http import HttpResponse from django.test import RequestFactory, SimpleTestCase from django.utils import translation +from rest_framework.response import Response +from rest_framework.test import APIRequestFactory +from rest_framework.views import APIView import dojo +from dojo.api_v2.views import DeprecationNoticeMixin from dojo.decorators import deprecated_view from dojo.deprecations import ( Deprecation, @@ -147,3 +153,46 @@ def test_every_deprecated_classic_view_names_its_declaration(self): for view, key in expected.items(): with self.subTest(view=view.__name__): self.assertEqual(key, getattr(view, "deprecation", None)) + + +class ToolTypeProbe(DeprecationNoticeMixin, APIView): + authentication_classes = () + permission_classes = () + deprecation = "tool_type" + + def get(self, request): + return Response({}) + + +class UndeclaredProbe(ToolTypeProbe): + deprecation = "no_such_feature" + + +class TestDeprecationHeaders(SimpleTestCase): + def test_a_declared_feature_sends_both_headers_from_the_calendar(self): + response = ToolTypeProbe.as_view()(APIRequestFactory().get("/")) + self.assertEqual("True", response["X-Deprecated"]) + self.assertEqual("2026-11-02T00:00:00", response["X-End-Of-Life-Date"]) + + def test_an_undeclared_key_sends_no_header(self): + response = UndeclaredProbe.as_view()(APIRequestFactory().get("/")) + self.assertFalse(response.has_header("X-Deprecated")) + self.assertFalse(response.has_header("X-End-Of-Life-Date")) + + +HAND_TYPED_SCHEDULE = re.compile( + r"removal_version=|removal_date=|end_of_life_date\s*=|will be removed (in|by) (DefectDojo )?v?\d|removal planned for \d", +) + + +class TestNoHandTypedSchedule(SimpleTestCase): + def test_only_the_registry_names_a_removal_release(self): + root = Path(dojo.__file__).parent + offenders = [] + for path in sorted([*root.rglob("*.py"), *root.rglob("*.html")]): + if path.name == "deprecations.py" or "db_migrations" in path.parts: + continue + for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + if HAND_TYPED_SCHEDULE.search(line): + offenders.append(f"{path.relative_to(root.parent)}:{number}") + self.assertEqual([], offenders, "Declare the deprecation in dojo/deprecations.py and read it by key.") diff --git a/unittests/test_rest_framework.py b/unittests/test_rest_framework.py index 5573745a951..5802219c64d 100644 --- a/unittests/test_rest_framework.py +++ b/unittests/test_rest_framework.py @@ -2756,7 +2756,7 @@ def test_deprecation_notice_header(self): self.assertTrue(response.has_header("X-Deprecated")) self.assertEqual("True", str(response["X-Deprecated"])) self.assertTrue(response.has_header("X-End-Of-Life-Date")) - self.assertTrue(str(response["X-End-Of-Life-Date"]).startswith("2026-11-01")) + self.assertTrue(str(response["X-End-Of-Life-Date"]).startswith("2026-11-02")) @versioned_fixtures @@ -2789,7 +2789,7 @@ def test_deprecation_notice_header(self): self.assertTrue(response.has_header("X-Deprecated")) self.assertEqual("True", str(response["X-Deprecated"])) self.assertTrue(response.has_header("X-End-Of-Life-Date")) - self.assertTrue(str(response["X-End-Of-Life-Date"]).startswith("2026-11-01")) + self.assertTrue(str(response["X-End-Of-Life-Date"]).startswith("2026-11-02")) @versioned_fixtures @@ -2917,7 +2917,7 @@ def test_deprecation_notice_header(self): self.assertTrue(response.has_header("X-Deprecated")) self.assertEqual("True", str(response["X-Deprecated"])) self.assertTrue(response.has_header("X-End-Of-Life-Date")) - self.assertTrue(str(response["X-End-Of-Life-Date"]).startswith("2026-11-01")) + self.assertTrue(str(response["X-End-Of-Life-Date"]).startswith("2026-11-02")) @versioned_fixtures @@ -2972,7 +2972,7 @@ def test_deprecation_notice_header(self): self.assertTrue(response.has_header("X-Deprecated")) self.assertEqual("True", str(response["X-Deprecated"])) self.assertTrue(response.has_header("X-End-Of-Life-Date")) - self.assertTrue(str(response["X-End-Of-Life-Date"]).startswith("2026-11-01")) + self.assertTrue(str(response["X-End-Of-Life-Date"]).startswith("2026-11-02")) @versioned_fixtures From ea89728e72fe8c49313ae9fd7fc74a51f55169e1 Mon Sep 17 00:00:00 2001 From: Sam Vader Date: Mon, 5 Oct 2026 15:31:06 -0500 Subject: [PATCH 04/10] docs(pro): deprecation banner, release months, Endpoints in 3.6.0 --- .../locations/PRO__migrating_from_endpoints.md | 2 +- docs/content/navigation/PRO__menu_badges.md | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) 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 c73a040ec83..41acfcf2030 100644 --- a/docs/content/asset_modelling/locations/PRO__migrating_from_endpoints.md +++ b/docs/content/asset_modelling/locations/PRO__migrating_from_endpoints.md @@ -9,7 +9,7 @@ When you enable Locations on an existing DefectDojo Pro instance, the data alrea Note that migration is **one-way**. There is no automated rollback path that re-creates Endpoints from Locations. -> **Endpoints are deprecated.** As of **3.2.201**, Endpoints are deprecated in favour of Locations and are scheduled for **removal in 3.4.0**. Until then the Endpoints UI and the read-only Endpoint API stay available, and the **DEPRECATED** badges shown on the Endpoints menu, the Endpoint list pages, and a Finding's endpoint tables link here. Enable Locations and run the migration below before 3.4.0. +> **Endpoints are deprecated.** As of **3.2.201**, Endpoints are deprecated in favour of Locations and are scheduled for **removal in 3.6.0 (December 2026)**. Until then the Endpoints UI and the read-only Endpoint API stay available, and the **DEPRECATED** badges shown on the Endpoints menu, the Endpoint list pages, and a Finding's endpoint tables link here. Enable Locations and run the migration below before 3.6.0. ## Running the migration from the Feature Flags page diff --git a/docs/content/navigation/PRO__menu_badges.md b/docs/content/navigation/PRO__menu_badges.md index 5fc0449e345..34ef249839a 100644 --- a/docs/content/navigation/PRO__menu_badges.md +++ b/docs/content/navigation/PRO__menu_badges.md @@ -32,7 +32,7 @@ The distinction is deliberate, because the two states call for different respons **`DEPRECATED`** means a removal has been announced. Hovering the badge tells you the release it goes away in, and clicking it opens the deprecation notice: -> \ is deprecated and will be removed by \. Click for the deprecation notice. +> \ is deprecated and will be removed by \ (\). Click for the deprecation notice. **`LEGACY`** means the feature has been superseded but no removal has been scheduled. There is deliberately no date in the hover text, because inventing one would be worse than saying nothing. Instead it names the replacement and links to its documentation: @@ -42,6 +42,10 @@ A `LEGACY` feature keeps working and keeps getting fixes. It just will not gain Both badges are links, because a tooltip closes the moment your pointer leaves it and so cannot hold a clickable link. Clicking either badge opens its notice in a new tab; it does not navigate the menu entry underneath. +## Pages of a deprecated feature carry a banner + +Every page that belongs to a `DEPRECATED` feature opens with a warning banner. The banner carries the same red badge, names the release the feature goes away in, and says what to do. It reads the same announcement as the sidebar badge, so the two always agree. + ## What currently carries a badge **`SOON`** @@ -55,10 +59,9 @@ Both badges are links, because a tooltip closes the moment your pointer leaves i **`DEPRECATED`** -* **Settings > Configuration > Tool Types** -* **Settings > Configuration > Tool Configurations** - -Both are removed in **3.5.0**, along with the API-based (pull) parsers they exist to configure. The [3.2 upgrade notes](/releases/os_upgrading/3.2/) explain what to migrate to and by when. +* **Settings > Configuration > Tool Types** and **Tool Configurations**, removed in **3.5.0 (November 2026)** along with the API-based (pull) parsers they exist to configure. The [3.2 upgrade notes](/releases/os_upgrading/3.2/) explain what to migrate to and by when. +* **API Scan Configurations** on an Asset, removed in **3.5.0 (November 2026)** for the same reason. +* **Endpoints** (the Endpoints menu, the Endpoint and Host list pages, and a Finding's endpoint tables), removed in **3.6.0 (December 2026)** in favour of Locations. See [Migrating from Endpoints](/asset_modelling/locations/pro__migrating_from_endpoints/). ![DEPRECATED badges under Settings > Configuration](images/menu_badge_deprecated.png) From 0cbbf6f3ad1e84cc6f1df0e1cb14c8b4cd92c721 Mon Sep 17 00:00:00 2001 From: Sam Vader Date: Mon, 5 Oct 2026 15:32:45 -0500 Subject: [PATCH 05/10] test(deprecations): drop schedule comments the declaration now owns --- unittests/test_rest_framework.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/unittests/test_rest_framework.py b/unittests/test_rest_framework.py index 5802219c64d..b400a07a803 100644 --- a/unittests/test_rest_framework.py +++ b/unittests/test_rest_framework.py @@ -2750,7 +2750,6 @@ def __init__(self, *args, **kwargs): BaseClass.RESTEndpointTest.__init__(self, *args, **kwargs) def test_deprecation_notice_header(self): - # Deprecated in 3.2.0, removal planned for 3.5.0 (serves the API-based pull parsers). response = self.client.get(self.url, format="json") self.assertEqual(200, response.status_code, response.content[:1000]) self.assertTrue(response.has_header("X-Deprecated")) @@ -2783,7 +2782,6 @@ def __init__(self, *args, **kwargs): BaseClass.RESTEndpointTest.__init__(self, *args, **kwargs) def test_deprecation_notice_header(self): - # Deprecated in 3.2.0, removal planned for 3.5.0 (serves the API-based pull parsers). response = self.client.get(self.url, format="json") self.assertEqual(200, response.status_code, response.content[:1000]) self.assertTrue(response.has_header("X-Deprecated")) @@ -2966,7 +2964,6 @@ def __init__(self, *args, **kwargs): BaseClass.RESTEndpointTest.__init__(self, *args, **kwargs) def test_deprecation_notice_header(self): - # Deprecated in 3.2.0, removal planned for 3.5.0 (serves the API-based pull parsers). response = self.client.get(self.url, format="json") self.assertEqual(200, response.status_code, response.content[:1000]) self.assertTrue(response.has_header("X-Deprecated")) From 18d8ecd04e6509130e9fd4ca653b22af1e4e8180 Mon Sep 17 00:00:00 2001 From: Sam Vader Date: Mon, 5 Oct 2026 15:33:53 -0500 Subject: [PATCH 06/10] test(deprecations): drop the last schedule comment in the header tests --- unittests/test_rest_framework.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/unittests/test_rest_framework.py b/unittests/test_rest_framework.py index b400a07a803..78dc8028d65 100644 --- a/unittests/test_rest_framework.py +++ b/unittests/test_rest_framework.py @@ -2907,9 +2907,9 @@ def __init__(self, *args, **kwargs): BaseClass.RESTEndpointTest.__init__(self, *args, **kwargs) def test_deprecation_notice_header(self): - # Deprecated in 3.2.0, removal planned for 3.5.0. The DeprecationNoticeMixin - # must run in finalize_response, which only happens if it precedes the base - # viewset in the MRO (see dojo/api_v2/views.py:DeprecationNoticeMixin). + # The DeprecationNoticeMixin must run in finalize_response, which only + # happens if it precedes the base viewset in the MRO (see + # dojo/api_v2/views.py:DeprecationNoticeMixin). response = self.client.get(self.url, format="json") self.assertEqual(200, response.status_code, response.content[:1000]) self.assertTrue(response.has_header("X-Deprecated")) From 93b3fe67151d59550d2dfbffe78f16a5ecab24fe Mon Sep 17 00:00:00 2001 From: Sam Vader Date: Mon, 5 Oct 2026 18:07:17 -0500 Subject: [PATCH 07/10] test(deprecations): catch a typo'd deprecation key across every viewset register_deprecation never validated the key a DeprecationNoticeMixin subclass names, so a typo sends no X-Deprecated header and fails silently. Walk every subclass after dojo.urls loads them all, and assert each non-empty key resolves to a real declaration. --- unittests/test_deprecations.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/unittests/test_deprecations.py b/unittests/test_deprecations.py index 107e95b5d83..1996b94f6d9 100644 --- a/unittests/test_deprecations.py +++ b/unittests/test_deprecations.py @@ -15,6 +15,7 @@ from rest_framework.views import APIView import dojo +import dojo.urls # noqa: F401 -- loads every viewset module, so every DeprecationNoticeMixin subclass exists from dojo.api_v2.views import DeprecationNoticeMixin from dojo.decorators import deprecated_view from dojo.deprecations import ( @@ -185,6 +186,24 @@ def test_an_undeclared_key_sends_no_header(self): ) +def mixin_subclasses(cls): + for sub in cls.__subclasses__(): + yield sub + yield from mixin_subclasses(sub) + + +class TestEveryDeprecationKeyIsDeclared(SimpleTestCase): + def test_a_mixin_subclass_never_names_an_undeclared_key(self): + offenders = [] + for cls in mixin_subclasses(DeprecationNoticeMixin): + if cls.__module__.startswith("unittests."): + continue + key = getattr(cls, "deprecation", "") + if key and get_deprecation(key) is None: + offenders.append(f"{cls.__module__}.{cls.__qualname__}: deprecation={key!r}") + self.assertEqual([], offenders, "A typo here sends no deprecation header and fails silently.") + + class TestNoHandTypedSchedule(SimpleTestCase): def test_only_the_registry_names_a_removal_release(self): root = Path(dojo.__file__).parent From 3ade7d6d2eb220ee013894f8e95f0c67064fcd6a Mon Sep 17 00:00:00 2001 From: Sam Vader Date: Mon, 5 Oct 2026 18:07:32 -0500 Subject: [PATCH 08/10] docs(upgrading): correct the Endpoints removal notice and the 3.5.0 header date The Endpoints migration page claimed the read-only Endpoint API goes away in 3.6.0 with the UI; only the UI pages do, and the badges link to the Feature Flags page, not to this doc. The 3.5.0 upgrade page named the old end-of-life header value and had no record of the fix. --- .../PRO__migrating_from_endpoints.md | 2 +- docs/content/releases/os_upgrading/3.5.md | 23 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) 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 fbeb48410af..defdc2fd7e5 100644 --- a/docs/content/asset_modelling/locations/PRO__migrating_from_endpoints.md +++ b/docs/content/asset_modelling/locations/PRO__migrating_from_endpoints.md @@ -9,7 +9,7 @@ When you enable Locations on an existing DefectDojo Pro instance, the data alrea Note that migration is **one-way**. There is no automated rollback path that re-creates Endpoints from Locations. -> **Endpoints are deprecated.** As of **3.2.201**, Endpoints are deprecated in favour of Locations and are scheduled for **removal in 3.6.0 (December 2026)**. Until then the Endpoints UI and the read-only Endpoint API stay available, and the **DEPRECATED** badges shown on the Endpoints menu, the Endpoint list pages, and a Finding's endpoint tables link here. Enable Locations and run the migration below before 3.6.0. +> **Endpoints are deprecated.** As of **3.2.201**, Endpoints are deprecated in favour of Locations. The Endpoints pages go away in **3.6.0 (December 2026)**. The read-only Endpoint API (`/api/v2/endpoints/`, `/api/v2/endpoint_status/`) stays until its own deprecation announcement. The Endpoints menu, the Endpoint list pages, and a Finding's endpoint tables carry a deprecation banner. Enable Locations and run the migration below before 3.6.0. ## Running the migration from the Feature Flags page diff --git a/docs/content/releases/os_upgrading/3.5.md b/docs/content/releases/os_upgrading/3.5.md index a65adf278f0..b99328da8a1 100644 --- a/docs/content/releases/os_upgrading/3.5.md +++ b/docs/content/releases/os_upgrading/3.5.md @@ -2,6 +2,25 @@ title: 'Upgrading to DefectDojo Version 3.5.x' toc_hide: true weight: -20261005 -description: No special instructions. +description: The deprecated Tool Type, Tool Configuration, and API Scan Configuration API endpoints send a corrected end-of-life date and a single-sentence Classic UI warning. --- -There are no special instructions for upgrading to 3.5.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/3.5.0) for the contents of the release. +Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/3.5.0) for the contents of the release. + +## Deprecated API endpoints: corrected end-of-life date + +The deprecated Tool Type, Tool Configuration, and API Scan Configuration API endpoints send a +corrected end-of-life date. They now send `X-End-Of-Life-Date: 2026-11-02T00:00:00`, the actual +3.5.0 release day. The earlier value was `2026-11-01T00:00:00`, one day off. + +The affected endpoints: + +- `/api/v2/tool_types/` +- `/api/v2/tool_configurations/` +- `/api/v2/product_api_scan_configurations/` + +Their Classic UI warning is now one sentence: ` are deprecated and will be removed in +DefectDojo 3.5.0 (November 2026). Please plan to migrate away from this feature.` + +### What you need to do + +Nothing. If your automation reads `X-End-Of-Life-Date`, it now receives the correct day. From bf075a53fe5145ddc940ca2bf6aec752b464572d Mon Sep 17 00:00:00 2001 From: Sam Vader Date: Wed, 7 Oct 2026 15:09:28 -0500 Subject: [PATCH 09/10] fix(deprecations): move the upgrade note to 3.4.100 and harden the registry The note sat in 3.5.md, but the change ships in the 3.4.100 weekly patch, and 3.5.0 is the release that removes the three features. 3.3.200.md is the precedent for a patch release page. _release() now reads a release candidate or a v prefix instead of raising. A declaration kept with removed=True no longer adds the Classic UI warning or the API headers. --- docs/content/releases/os_upgrading/3.4.100.md | 25 +++++++++++++++++ docs/content/releases/os_upgrading/3.5.md | 23 ++------------- dojo/api_v2/views.py | 2 +- dojo/decorators.py | 2 +- dojo/deprecations.py | 9 +++++- unittests/test_deprecations.py | 28 +++++++++++++++++++ 6 files changed, 65 insertions(+), 24 deletions(-) create mode 100644 docs/content/releases/os_upgrading/3.4.100.md diff --git a/docs/content/releases/os_upgrading/3.4.100.md b/docs/content/releases/os_upgrading/3.4.100.md new file mode 100644 index 00000000000..3615a41cd04 --- /dev/null +++ b/docs/content/releases/os_upgrading/3.4.100.md @@ -0,0 +1,25 @@ +--- +title: 'Upgrading to DefectDojo Version 3.4.100' +toc_hide: true +weight: -20261013 +description: The deprecated Tool Type, Tool Configuration, and API Scan Configuration API endpoints send a corrected end-of-life date and a single-sentence Classic UI warning. +--- + +## Deprecated API endpoints: corrected end-of-life date + +The deprecated Tool Type, Tool Configuration, and API Scan Configuration API endpoints send a +corrected end-of-life date. They now send `X-End-Of-Life-Date: 2026-11-02T00:00:00`, the actual +3.5.0 release day. The earlier value was `2026-11-01T00:00:00`, one day off. + +The affected endpoints: + +- `/api/v2/tool_types/` +- `/api/v2/tool_configurations/` +- `/api/v2/product_api_scan_configurations/` + +Their Classic UI warning is now one sentence: ` are deprecated and will be removed in +DefectDojo 3.5.0 (November 2026). Please plan to migrate away from this feature.` + +### What you need to do + +Nothing. If your automation reads `X-End-Of-Life-Date`, it now receives the correct day. diff --git a/docs/content/releases/os_upgrading/3.5.md b/docs/content/releases/os_upgrading/3.5.md index b99328da8a1..a65adf278f0 100644 --- a/docs/content/releases/os_upgrading/3.5.md +++ b/docs/content/releases/os_upgrading/3.5.md @@ -2,25 +2,6 @@ title: 'Upgrading to DefectDojo Version 3.5.x' toc_hide: true weight: -20261005 -description: The deprecated Tool Type, Tool Configuration, and API Scan Configuration API endpoints send a corrected end-of-life date and a single-sentence Classic UI warning. +description: No special instructions. --- -Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/3.5.0) for the contents of the release. - -## Deprecated API endpoints: corrected end-of-life date - -The deprecated Tool Type, Tool Configuration, and API Scan Configuration API endpoints send a -corrected end-of-life date. They now send `X-End-Of-Life-Date: 2026-11-02T00:00:00`, the actual -3.5.0 release day. The earlier value was `2026-11-01T00:00:00`, one day off. - -The affected endpoints: - -- `/api/v2/tool_types/` -- `/api/v2/tool_configurations/` -- `/api/v2/product_api_scan_configurations/` - -Their Classic UI warning is now one sentence: ` are deprecated and will be removed in -DefectDojo 3.5.0 (November 2026). Please plan to migrate away from this feature.` - -### What you need to do - -Nothing. If your automation reads `X-End-Of-Life-Date`, it now receives the correct day. +There are no special instructions for upgrading to 3.5.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/3.5.0) for the contents of the release. diff --git a/dojo/api_v2/views.py b/dojo/api_v2/views.py index 7fa1b30da4f..eb0ff0f4421 100644 --- a/dojo/api_v2/views.py +++ b/dojo/api_v2/views.py @@ -171,7 +171,7 @@ class DeprecationNoticeMixin: def finalize_response(self, request, response, *args, **kwargs): notice = get_deprecation(self.deprecation) - if notice is not None: + if notice is not None and not notice.removed: response["X-Deprecated"] = True response["X-End-Of-Life-Date"] = datetime.combine(notice.removal_date, time.min).isoformat() return super().finalize_response(request, response, *args, **kwargs) diff --git a/dojo/decorators.py b/dojo/decorators.py index 09b2392212d..31050436479 100644 --- a/dojo/decorators.py +++ b/dojo/decorators.py @@ -186,7 +186,7 @@ def decorator(func): @wraps(func) def _wrapped(request, *args, **kwargs): notice = get_deprecation(key) - if request.method == "GET" and notice is not None: + if request.method == "GET" and notice is not None and not notice.removed: messages.add_message(request, messages.WARNING, notice.message(), extra_tags="alert-warning") return func(request, *args, **kwargs) diff --git a/dojo/deprecations.py b/dojo/deprecations.py index a3d862c51b8..46ba3753176 100644 --- a/dojo/deprecations.py +++ b/dojo/deprecations.py @@ -46,8 +46,15 @@ def message(self) -> str: _DEPRECATIONS: dict[str, Deprecation] = {} +_VERSION = re.compile(r"v?(\d+)\.(\d+)\.(\d+)") + + def _release(version: str) -> tuple[int, ...]: - return tuple(int(part) for part in version.split("-", maxsplit=1)[0].split(".")[:3]) + match = _VERSION.match(version) + if match is None: + msg = f"{version!r} is not an X.Y.Z version" + raise ValueError(msg) + return tuple(int(part) for part in match.groups()) def register_deprecation(entry: Deprecation, *, override: bool = False) -> None: diff --git a/unittests/test_deprecations.py b/unittests/test_deprecations.py index 1996b94f6d9..d58297beae2 100644 --- a/unittests/test_deprecations.py +++ b/unittests/test_deprecations.py @@ -81,6 +81,14 @@ def test_a_removed_declaration_is_never_overdue(self): with patch.dict("dojo.deprecations._DEPRECATIONS", {"gone": gone}, clear=True): self.assertEqual([], overdue_deprecations("3.6.0-dev")) + def test_a_release_candidate_or_a_v_prefix_still_parses(self): + old = widgets(removal_version="3.4.0") + with patch.dict("dojo.deprecations._DEPRECATIONS", {"widgets": old}, clear=True): + self.assertEqual([old], overdue_deprecations("3.5.0rc1")) + self.assertEqual([old], overdue_deprecations("v3.5.0")) + with self.assertRaisesRegex(ValueError, "X.Y.Z"): + overdue_deprecations("latest") + def test_nothing_declared_is_overdue_on_this_release_line(self): overdue = [entry.key for entry in overdue_deprecations(dojo.__version__)] self.assertEqual( @@ -134,6 +142,15 @@ def test_a_get_shows_the_declared_warning(self): def test_a_post_shows_nothing_so_the_redirect_does_not_repeat_it(self): self.assertEqual([], self.shown("post")) + def test_a_removed_declaration_shows_nothing(self): + gone = widgets(key="gone", removal_version="3.3.0", removed=True) + with patch.dict("dojo.deprecations._DEPRECATIONS", {"gone": gone}, clear=True): + request = RequestFactory().get("/gone") + request.session = {} + request._messages = FallbackStorage(request) + deprecated_view("gone")(ok_view)(request) + self.assertEqual([], list(get_messages(request))) + def test_an_undeclared_key_fails_at_import(self): with self.assertRaises(ImproperlyConfigured): deprecated_view("no_such_feature") @@ -169,6 +186,10 @@ class UndeclaredProbe(ToolTypeProbe): deprecation = "no_such_feature" +class RemovedProbe(ToolTypeProbe): + deprecation = "gone" + + class TestDeprecationHeaders(SimpleTestCase): def test_a_declared_feature_sends_both_headers_from_the_calendar(self): response = ToolTypeProbe.as_view()(APIRequestFactory().get("/")) @@ -180,6 +201,13 @@ def test_an_undeclared_key_sends_no_header(self): self.assertFalse(response.has_header("X-Deprecated")) self.assertFalse(response.has_header("X-End-Of-Life-Date")) + def test_a_removed_declaration_sends_no_header(self): + gone = widgets(key="gone", removal_version="3.3.0", removed=True) + with patch.dict("dojo.deprecations._DEPRECATIONS", {"gone": gone}, clear=True): + response = RemovedProbe.as_view()(APIRequestFactory().get("/")) + self.assertFalse(response.has_header("X-Deprecated")) + self.assertFalse(response.has_header("X-End-Of-Life-Date")) + HAND_TYPED_SCHEDULE = re.compile( r"removal_version=|removal_date=|end_of_life_date\s*=|will be removed (in|by) (DefectDojo )?v?\d|removal planned for \d", From 4bdf572dae6b5a7e3fb184684ba766554bd5f84c Mon Sep 17 00:00:00 2001 From: Sam Vader Date: Wed, 7 Oct 2026 15:23:45 -0500 Subject: [PATCH 10/10] docs(upgrading): list the asset API scan configuration endpoint and close the 3.4.100 page like its siblings --- docs/content/releases/os_upgrading/3.4.100.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/content/releases/os_upgrading/3.4.100.md b/docs/content/releases/os_upgrading/3.4.100.md index 3615a41cd04..b00c1046d0e 100644 --- a/docs/content/releases/os_upgrading/3.4.100.md +++ b/docs/content/releases/os_upgrading/3.4.100.md @@ -16,6 +16,7 @@ The affected endpoints: - `/api/v2/tool_types/` - `/api/v2/tool_configurations/` - `/api/v2/product_api_scan_configurations/` +- `/api/v2/asset_api_scan_configurations/` Their Classic UI warning is now one sentence: ` are deprecated and will be removed in DefectDojo 3.5.0 (November 2026). Please plan to migrate away from this feature.` @@ -23,3 +24,5 @@ DefectDojo 3.5.0 (November 2026). Please plan to migrate away from this feature. ### What you need to do Nothing. If your automation reads `X-End-Of-Life-Date`, it now receives the correct day. + +For more information, check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/3.4.100).