diff --git a/components/package.json b/components/package.json
index c25b207b862..44db1f83489 100644
--- a/components/package.json
+++ b/components/package.json
@@ -1,6 +1,6 @@
{
"name": "defectdojo",
- "version": "2.55.0",
+ "version": "2.55.1",
"license" : "BSD-3-Clause",
"private": true,
"dependencies": {
diff --git a/docs/config/_default/hugo.toml b/docs/config/_default/hugo.toml
index 13367504ca6..0b623542125 100644
--- a/docs/config/_default/hugo.toml
+++ b/docs/config/_default/hugo.toml
@@ -46,9 +46,12 @@ copyRight = "Copyright (c) 2020-2024 Thulite"
priority = 0.5
[caches]
+ [caches.getresource]
+ dir = ":cacheDir/:project"
+ maxAge = "1h"
[caches.getjson]
dir = ":cacheDir/:project"
- maxAge = -1 # "30m"
+ maxAge = "1h"
[taxonomies]
contributor = "contributors"
diff --git a/docs/layouts/_partials/head/custom-head.html b/docs/layouts/_partials/head/custom-head.html
index cadc425ac3a..5f14c4648e0 100644
--- a/docs/layouts/_partials/head/custom-head.html
+++ b/docs/layouts/_partials/head/custom-head.html
@@ -1,6 +1,7 @@
-{{ if site.Params.add_ons.docSearch -}}
+{{ if site.Params.add_ons.docSearch -}}
{{ $options := (dict "targetPath" "/css/main.min.css" "outputStyle" "compressed") }}
{{ $style := resources.Get "scss/app.scss" | css.Sass $options }}
-
+
{{ end -}}
+
\ No newline at end of file
diff --git a/docs/layouts/_partials/seo/robots.html b/docs/layouts/_partials/seo/robots.html
new file mode 100644
index 00000000000..128d19bf8f2
--- /dev/null
+++ b/docs/layouts/_partials/seo/robots.html
@@ -0,0 +1,3 @@
+{{- with .Params.seo.robots }}
+
+{{- end }}
\ No newline at end of file
diff --git a/docs/layouts/robots.txt b/docs/layouts/robots.txt
new file mode 100644
index 00000000000..3cba9e17660
--- /dev/null
+++ b/docs/layouts/robots.txt
@@ -0,0 +1,3 @@
+User-agent: *
+Disallow:
+Sitemap: {{ "/sitemap.xml" | absURL }}
\ No newline at end of file
diff --git a/dojo/__init__.py b/dojo/__init__.py
index fe0ab480aee..9f7285d1e66 100644
--- a/dojo/__init__.py
+++ b/dojo/__init__.py
@@ -4,6 +4,6 @@
# Django starts so that shared_task will use this app.
from .celery import app as celery_app # noqa: F401
-__version__ = "2.55.0"
+__version__ = "2.55.1"
__url__ = "https://github.com/DefectDojo/django-DefectDojo" # noqa: RUF067
__docs__ = "https://documentation.defectdojo.com" # noqa: RUF067
diff --git a/dojo/api_v2/views.py b/dojo/api_v2/views.py
index c1a3b12db6f..3998683c315 100644
--- a/dojo/api_v2/views.py
+++ b/dojo/api_v2/views.py
@@ -1124,6 +1124,9 @@ def notes(self, request, pk=None):
note_type=note_type,
)
note.save()
+ finding.last_reviewed = note.date
+ finding.last_reviewed_by = author
+ finding.save(update_fields=["last_reviewed", "last_reviewed_by", "updated"])
finding.notes.add(note)
# Determine if we need to send any notifications for user mentioned
process_tag_notifications(
diff --git a/dojo/jira_link/views.py b/dojo/jira_link/views.py
index 31841c9bf43..061ad83d83c 100644
--- a/dojo/jira_link/views.py
+++ b/dojo/jira_link/views.py
@@ -285,9 +285,11 @@ def check_for_and_create_comment(parsed_json):
finding.notes.add(new_note)
finding.jira_issue.jira_change = timezone.now()
finding.jira_issue.save()
- # Only update the timestamp, not other fields like 'active' to avoid
+ finding.last_reviewed = new_note.date
+ finding.last_reviewed_by = author
+ # Only update the timestamp fields, not other fields like 'active' to avoid
# race conditions with concurrent webhook events (e.g. issue_updated)
- finding.save(update_fields=["updated"])
+ finding.save(update_fields=["last_reviewed", "last_reviewed_by", "updated"])
return None
@@ -345,11 +347,11 @@ def post(self, request):
# Get the open and close keys
msg = "Unable to find Open/Close ID's (invalid issue key specified?). They will need to be found manually"
try:
+ open_key = close_key = None
issue_id = jform.cleaned_data.get("issue_key")
key_url = jira_server.strip("/") + "/rest/api/latest/issue/" + issue_id + "/transitions?expand=transitions.fields"
response = jira._session.get(key_url).json()
logger.debug("Retrieved JIRA issue successfully")
- open_key = close_key = None
for node in response["transitions"]:
if node["to"]["statusCategory"]["name"] == "To Do":
open_key = open_key or int(node["id"])
diff --git a/dojo/product_type/views.py b/dojo/product_type/views.py
index 28553db8cfc..51c3985ec40 100644
--- a/dojo/product_type/views.py
+++ b/dojo/product_type/views.py
@@ -4,7 +4,7 @@
from django.contrib import messages
from django.contrib.admin.utils import NestedObjects
from django.db import DEFAULT_DB_ALIAS
-from django.db.models import Count, IntegerField, OuterRef, Subquery, Value
+from django.db.models import OuterRef, Value
from django.db.models.functions import Coalesce
from django.db.models.query import QuerySet
from django.http import HttpResponseRedirect
@@ -82,13 +82,10 @@ def prefetch_for_product_type(prod_types):
logger.debug("unable to prefetch because query was already executed")
return prod_types
- prod_subquery = Subquery(
- Product.objects.filter(prod_type_id=OuterRef("pk"))
- .values("prod_type_id")
- .annotate(c=Count("*"))
- .values("c")[:1],
- output_field=IntegerField(),
- )
+ prod_subquery = build_count_subquery(
+ Product.objects.filter(prod_type_id=OuterRef("pk")),
+ group_field="prod_type_id",
+ )
base_findings = Finding.objects.filter(test__engagement__product__prod_type_id=OuterRef("pk"))
count_subquery = partial(build_count_subquery, group_field="test__engagement__product__prod_type_id")
diff --git a/dojo/query_utils.py b/dojo/query_utils.py
index b14c4bc03fd..19e062871bc 100644
--- a/dojo/query_utils.py
+++ b/dojo/query_utils.py
@@ -4,7 +4,11 @@
def build_count_subquery(model_qs: QuerySet, group_field: str) -> Subquery:
"""Return a Subquery that yields one aggregated count per `group_field`."""
+ # Important: slicing (`[:1]`) on an unordered queryset makes Django add an implicit `ORDER BY `.
+ # With aggregation, Django then includes that pk in the GROUP BY, which collapses counts to 1.
+ # Ordering by `group_field` avoids that and keeps the GROUP BY stable.
+ model_qs = model_qs.order_by()
return Subquery(
- model_qs.values(group_field).annotate(c=Count("*")).values("c")[:1], # one row per group_field
+ model_qs.values(group_field).annotate(c=Count("pk")).order_by(group_field).values("c")[:1], # one row per group_field
output_field=IntegerField(),
)
diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml
index 0b582212518..1b77c00b173 100644
--- a/helm/defectdojo/Chart.yaml
+++ b/helm/defectdojo/Chart.yaml
@@ -1,8 +1,8 @@
apiVersion: v2
-appVersion: "2.55.0"
+appVersion: "2.55.1"
description: A Helm chart for Kubernetes to install DefectDojo
name: defectdojo
-version: 1.9.10
+version: 1.9.11
icon: https://defectdojo.com/hubfs/DefectDojo_favicon.png
maintainers:
- name: madchap
@@ -34,4 +34,4 @@ dependencies:
# description: Critical bug
annotations:
artifacthub.io/prerelease: "false"
- artifacthub.io/changes: "- kind: changed\n description: Update valkey Docker tag from 0.13.0 to v0.15.0 (_/defect_/Chart.yaml)\n- kind: changed\n description: chore(deps)_ update valkey _ tag from 0.15.0 to v0.15.1 (_/defect_/chart.yaml)\n- kind: changed\n description: chore(deps)_ update gcr.io/cloudsql__/gce_proxy _ tag from 1.37.11 to v1.37.12 (_/defect_/values.yaml)\n- kind: changed\n description: Update valkey Docker tag from 0.15.1 to v0.15.2 (_/defect_/Chart.yaml)\n- kind: changed\n description: Update valkey Docker tag from 0.15.2 to v0.15.3 (_/defect_/Chart.yaml)\n- kind: changed\n description: Bump DefectDojo to 2.55.0\n"
+ artifacthub.io/changes: "- kind: changed\n description: Bump DefectDojo to 2.55.1\n"
diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md
index 33bc2bbc843..1e471662ca6 100644
--- a/helm/defectdojo/README.md
+++ b/helm/defectdojo/README.md
@@ -511,7 +511,7 @@ The HELM schema will be generated for you.
# General information about chart values
- 
+ 
A Helm chart for Kubernetes to install DefectDojo
diff --git a/requirements.txt b/requirements.txt
index d152e8a32eb..52396d021d7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -19,7 +19,7 @@ django-slack==5.19.0
django-watson==1.6.3
django-permissions-policy==4.28.0
django-prometheus==2.4.1
-Django==5.2.9
+Django==5.2.11
django-single-session==0.2.0
djangorestframework==3.16.1
html2text==2025.4.15
diff --git a/unittests/test_product_type_counts.py b/unittests/test_product_type_counts.py
new file mode 100644
index 00000000000..5bac04f3d1c
--- /dev/null
+++ b/unittests/test_product_type_counts.py
@@ -0,0 +1,19 @@
+from dojo.models import Product, Product_Type
+from dojo.product_type.views import prefetch_for_product_type
+from unittests.dojo_test_case import DojoTestCase, versioned_fixtures
+
+
+@versioned_fixtures
+class TestProductTypeCounts(DojoTestCase):
+ fixtures = ["dojo_testdata.json"]
+
+ def test_prefetch_for_product_type_prod_count_matches_direct_count(self):
+ product_type = Product_Type.objects.create(name="PT count test")
+ Product.objects.create(name="PT product 1", description="test", prod_type=product_type)
+ Product.objects.create(name="PT product 2", description="test", prod_type=product_type)
+
+ annotated = prefetch_for_product_type(Product_Type.objects.filter(id=product_type.id))
+ annotated_count = annotated.values_list("prod_count", flat=True).get()
+
+ direct_count = Product.objects.filter(prod_type_id=product_type.id).count()
+ self.assertEqual(annotated_count, direct_count)
diff --git a/unittests/test_query_utils.py b/unittests/test_query_utils.py
new file mode 100644
index 00000000000..e953efd1df9
--- /dev/null
+++ b/unittests/test_query_utils.py
@@ -0,0 +1,21 @@
+from django.db.models import Count
+
+from dojo.engagement.views import prefetch_for_view_tests
+from dojo.models import Finding, Test
+from unittests.dojo_test_case import DojoTestCase, versioned_fixtures
+
+
+@versioned_fixtures
+class TestQueryUtils(DojoTestCase):
+ fixtures = ["dojo_testdata.json"]
+
+ def test_prefetch_for_view_tests_finding_counts_match_direct_count(self):
+ test = Test.objects.annotate(finding_count=Count("finding")).filter(finding_count__gt=1).first()
+ # If fixtures ever change, ensure we still have a representative test case.
+ self.assertIsNotNone(test)
+
+ annotated = prefetch_for_view_tests(Test.objects.filter(id=test.id))
+ annotated_count = annotated.values_list("count_findings_test_all", flat=True).get()
+
+ direct_count = Finding.objects.filter(test_id=test.id).count()
+ self.assertEqual(annotated_count, direct_count)