Skip to content

Commit 7122e55

Browse files
finding template refactor (#13946)
* finding_templates: make tags work again * finding_templates: reinstate unit tests * finding_templates: remove automated matching logic * finding tempaltes: more auhtoirzation tests to apply template * fixture cleanup * upgrade notes * finding_template: add cvss validation * increase memory hugo * finding_template: align fields with finding model * finding_templates: update api schema * finding_templates: centralize logic * squash migrations * squash migrations * revert git hub pages changes * fix user interface test * update upgrade notes * fix test * move to 2.54 * fix test * move to 2.54 * Bumping hugo version due to memory issue --------- Co-authored-by: Ross Esposito <rossespo@gmail.com>
1 parent 0ffcacc commit 7122e55

22 files changed

Lines changed: 1299 additions & 302 deletions

.github/workflows/gh-pages.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,12 @@ jobs:
4646

4747
- name: Install dependencies
4848
run: cd docs && npm ci
49-
49+
5050
- name: Build production website
5151
env:
5252
HUGO_ENVIRONMENT: production
5353
HUGO_ENV: production
5454
run: cd docs && hugo --minify --gc --config config/production/hugo.toml
55-
5655
- name: Deploy
5756
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
5857
if: github.repository == 'DefectDojo/django-DefectDojo' # Deploy docs only in core repo, not in forks - it would just fail in fork

docs/content/en/open_source/upgrading/2.54.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: 'Upgrading to DefectDojo Version 2.54.x'
33
toc_hide: true
44
weight: -20250804
5-
description: Removal of django-auditlog & Dropped support for DD_PARSER_EXCLUDE & Reimport performance improvements
5+
description: Removal of django-auditlog & Dropped support for DD_PARSER_EXCLUDE & Reimport performance improvements & Removal of Finding Template Matching
66
---
77

88
## Breaking Change: Removal of django-auditlog
@@ -55,5 +55,9 @@ DefectDojo 2.54.x includes performance improvements for reimporting scan results
5555

5656
No action is required after upgrading. (Optional tuning knobs exist via `DD_IMPORT_REIMPORT_MATCH_BATCH_SIZE` and `DD_IMPORT_REIMPORT_DEDUPE_BATCH_SIZE`.)
5757

58+
## Finding Template enhancements and removal of CWE matching
59+
60+
As communicated in the [2025Q1 community update](https://github.com/DefectDojo/django-DefectDojo/discussions/12153) the automated matching of Finding Templates based on `CWE` and/or `title` has now been removed.
61+
5862
There are other instructions for upgrading to 2.54.x. Check the Release Notes for the contents of the release: `https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.54.0`
5963
Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.54.0) for the contents of the release.

dojo/api_v2/serializers.py

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from dojo.authorization.roles_permissions import Permissions
3030
from dojo.endpoint.utils import endpoint_filter, endpoint_meta_import
3131
from dojo.finding.helper import (
32+
save_endpoints_template,
3233
save_vulnerability_ids,
3334
save_vulnerability_ids_template,
3435
)
@@ -111,7 +112,6 @@
111112
User,
112113
UserContactInfo,
113114
Vulnerability_Id,
114-
Vulnerability_Id_Template,
115115
get_current_date,
116116
)
117117
from dojo.notifications.helper import create_notification
@@ -2029,57 +2029,80 @@ def validate_severity(self, value: str) -> str:
20292029
return value
20302030

20312031

2032-
class VulnerabilityIdTemplateSerializer(serializers.ModelSerializer):
2033-
class Meta:
2034-
model = Vulnerability_Id_Template
2035-
fields = ["vulnerability_id"]
2036-
2037-
20382032
class FindingTemplateSerializer(serializers.ModelSerializer):
20392033
tags = TagListSerializerField(required=False)
2040-
vulnerability_ids = VulnerabilityIdTemplateSerializer(
2041-
source="vulnerability_id_template_set", many=True, required=False,
2042-
)
2034+
vulnerability_ids = serializers.SerializerMethodField()
2035+
endpoints = serializers.SerializerMethodField()
20432036

20442037
class Meta:
20452038
model = Finding_Template
2046-
exclude = ("cve",)
2039+
exclude = ("cve", "vulnerability_ids_text")
2040+
2041+
@extend_schema_field(serializers.ListField(child=serializers.CharField()))
2042+
def get_vulnerability_ids(self, obj):
2043+
"""Return vulnerability IDs as a list of strings."""
2044+
return obj.vulnerability_ids
2045+
2046+
@extend_schema_field(serializers.ListField(child=serializers.CharField()))
2047+
def get_endpoints(self, obj):
2048+
"""Return endpoints as a list of URL strings."""
2049+
return obj.endpoints if hasattr(obj, "endpoints") else []
20472050

20482051
def create(self, validated_data):
20492052

2050-
# Save vulnerability ids and pop them
2051-
if "vulnerability_id_template_set" in validated_data:
2052-
vulnerability_id_set = validated_data.pop(
2053-
"vulnerability_id_template_set",
2054-
)
2055-
else:
2056-
vulnerability_id_set = None
2053+
# Handle vulnerability_ids if provided as list
2054+
vulnerability_ids = None
2055+
if "vulnerability_ids" in self.initial_data:
2056+
vulnerability_ids = self.initial_data.get("vulnerability_ids", [])
2057+
if isinstance(vulnerability_ids, str):
2058+
# If it's a string, split by newlines
2059+
vulnerability_ids = [vid.strip() for vid in vulnerability_ids.split("\n") if vid.strip()]
2060+
elif not isinstance(vulnerability_ids, list):
2061+
vulnerability_ids = []
2062+
2063+
# Handle endpoints if provided as list
2064+
endpoint_urls = None
2065+
if "endpoints" in self.initial_data:
2066+
endpoint_urls = self.initial_data.get("endpoints", [])
2067+
if isinstance(endpoint_urls, str):
2068+
# If it's a string, split by newlines
2069+
endpoint_urls = [url.strip() for url in endpoint_urls.split("\n") if url.strip()]
2070+
elif not isinstance(endpoint_urls, list):
2071+
endpoint_urls = []
20572072

20582073
new_finding_template = super().create(
20592074
validated_data,
20602075
)
20612076

2062-
if vulnerability_id_set:
2063-
vulnerability_ids = [vulnerability_id["vulnerability_id"] for vulnerability_id in vulnerability_id_set]
2064-
validated_data["cve"] = vulnerability_ids[0]
2065-
save_vulnerability_ids_template(
2066-
new_finding_template, vulnerability_ids,
2067-
)
2068-
new_finding_template.save()
2077+
# Save vulnerability IDs using helper
2078+
if vulnerability_ids:
2079+
save_vulnerability_ids_template(new_finding_template, vulnerability_ids)
2080+
2081+
# Save endpoints using helper
2082+
if endpoint_urls:
2083+
save_endpoints_template(new_finding_template, endpoint_urls)
20692084

20702085
return new_finding_template
20712086

20722087
def update(self, instance, validated_data):
2073-
# Save vulnerability ids and pop them
2074-
if "vulnerability_id_template_set" in validated_data:
2075-
vulnerability_id_set = validated_data.pop(
2076-
"vulnerability_id_template_set",
2077-
)
2078-
vulnerability_ids = []
2079-
if vulnerability_id_set:
2080-
vulnerability_ids.extend(vulnerability_id["vulnerability_id"] for vulnerability_id in vulnerability_id_set)
2088+
# Handle vulnerability_ids if provided
2089+
if "vulnerability_ids" in self.initial_data:
2090+
vulnerability_ids = self.initial_data.get("vulnerability_ids", [])
2091+
if isinstance(vulnerability_ids, str):
2092+
vulnerability_ids = [vid.strip() for vid in vulnerability_ids.split("\n") if vid.strip()]
2093+
elif not isinstance(vulnerability_ids, list):
2094+
vulnerability_ids = []
20812095
save_vulnerability_ids_template(instance, vulnerability_ids)
20822096

2097+
# Handle endpoints if provided
2098+
if "endpoints" in self.initial_data:
2099+
endpoint_urls = self.initial_data.get("endpoints", [])
2100+
if isinstance(endpoint_urls, str):
2101+
endpoint_urls = [url.strip() for url in endpoint_urls.split("\n") if url.strip()]
2102+
elif not isinstance(endpoint_urls, list):
2103+
endpoint_urls = []
2104+
save_endpoints_template(instance, endpoint_urls)
2105+
20832106
return super().update(instance, validated_data)
20842107

20852108

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Generated by Django 5.2.9 on 2025-12-21 07:29
2+
3+
import dojo.validators
4+
import pgtrigger.compiler
5+
import pgtrigger.migrations
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('dojo', '0251_usercontactinfo_reset_timestamps'),
13+
]
14+
15+
operations = [
16+
pgtrigger.migrations.RemoveTrigger(
17+
model_name='finding_template',
18+
name='insert_insert',
19+
),
20+
pgtrigger.migrations.RemoveTrigger(
21+
model_name='finding_template',
22+
name='update_update',
23+
),
24+
pgtrigger.migrations.RemoveTrigger(
25+
model_name='finding_template',
26+
name='delete_delete',
27+
),
28+
migrations.RemoveField(
29+
model_name='system_settings',
30+
name='enable_template_match',
31+
),
32+
migrations.RemoveField(
33+
model_name='finding_template',
34+
name='template_match',
35+
),
36+
migrations.RemoveField(
37+
model_name='finding_template',
38+
name='template_match_title',
39+
),
40+
migrations.RemoveField(
41+
model_name='finding_templateevent',
42+
name='template_match',
43+
),
44+
migrations.RemoveField(
45+
model_name='finding_templateevent',
46+
name='template_match_title',
47+
),
48+
migrations.AddField(
49+
model_name='finding_template',
50+
name='cvssv4',
51+
field=models.TextField(help_text='Common Vulnerability Scoring System version 4 (CVSS4) score associated with this finding.', max_length=255, null=True, validators=[dojo.validators.cvss4_validator], verbose_name='CVSS4 vector'),
52+
),
53+
migrations.AddField(
54+
model_name='finding_template',
55+
name='component_name',
56+
field=models.CharField(blank=True, help_text='Affected component name (e.g., library name)', max_length=500, null=True),
57+
),
58+
migrations.AddField(
59+
model_name='finding_template',
60+
name='component_version',
61+
field=models.CharField(blank=True, help_text='Affected component version', max_length=100, null=True),
62+
),
63+
migrations.AddField(
64+
model_name='finding_template',
65+
name='cvssv3_score',
66+
field=models.FloatField(blank=True, help_text='CVSSv3 score', null=True),
67+
),
68+
migrations.AddField(
69+
model_name='finding_template',
70+
name='cvssv4_score',
71+
field=models.FloatField(blank=True, help_text='CVSSv4 score', null=True),
72+
),
73+
migrations.AddField(
74+
model_name='finding_template',
75+
name='effort_for_fixing',
76+
field=models.CharField(blank=True, help_text='Effort estimate for fixing (e.g., Low/Medium/High)', max_length=99, null=True),
77+
),
78+
migrations.AddField(
79+
model_name='finding_template',
80+
name='fix_available',
81+
field=models.BooleanField(blank=True, help_text='Indicates if a fix is available for this vulnerability type', null=True),
82+
),
83+
migrations.AddField(
84+
model_name='finding_template',
85+
name='fix_version',
86+
field=models.CharField(blank=True, help_text='Version where fix is available', max_length=100, null=True),
87+
),
88+
migrations.AddField(
89+
model_name='finding_template',
90+
name='notes',
91+
field=models.TextField(blank=True, help_text='Note content to add when applying this template', null=True),
92+
),
93+
migrations.AddField(
94+
model_name='finding_template',
95+
name='planned_remediation_version',
96+
field=models.CharField(blank=True, help_text='Target version for remediation', max_length=99, null=True),
97+
),
98+
migrations.AddField(
99+
model_name='finding_template',
100+
name='severity_justification',
101+
field=models.TextField(blank=True, help_text='Explanation of why this severity level is appropriate', null=True),
102+
),
103+
migrations.AddField(
104+
model_name='finding_template',
105+
name='steps_to_reproduce',
106+
field=models.TextField(blank=True, help_text='Standard reproduction steps for this vulnerability type', null=True),
107+
),
108+
migrations.AddField(
109+
model_name='finding_template',
110+
name='vulnerability_ids_text',
111+
field=models.TextField(blank=True, help_text='Vulnerability IDs (one per line)', null=True),
112+
),
113+
migrations.AddField(
114+
model_name='finding_template',
115+
name='endpoints_text',
116+
field=models.TextField(blank=True, help_text='Endpoint URLs (one per line)', null=True),
117+
),
118+
migrations.AddField(
119+
model_name='finding_templateevent',
120+
name='component_name',
121+
field=models.CharField(blank=True, help_text='Affected component name (e.g., library name)', max_length=500, null=True),
122+
),
123+
migrations.AddField(
124+
model_name='finding_templateevent',
125+
name='component_version',
126+
field=models.CharField(blank=True, help_text='Affected component version', max_length=100, null=True),
127+
),
128+
migrations.AddField(
129+
model_name='finding_templateevent',
130+
name='cvssv3_score',
131+
field=models.FloatField(blank=True, help_text='CVSSv3 score', null=True),
132+
),
133+
migrations.AddField(
134+
model_name='finding_templateevent',
135+
name='cvssv4',
136+
field=models.TextField(help_text='Common Vulnerability Scoring System version 4 (CVSS4) score associated with this finding.', max_length=255, null=True, validators=[dojo.validators.cvss4_validator], verbose_name='CVSS4 vector'),
137+
),
138+
migrations.AddField(
139+
model_name='finding_templateevent',
140+
name='cvssv4_score',
141+
field=models.FloatField(blank=True, help_text='CVSSv4 score', null=True),
142+
),
143+
migrations.AddField(
144+
model_name='finding_templateevent',
145+
name='effort_for_fixing',
146+
field=models.CharField(blank=True, help_text='Effort estimate for fixing (e.g., Low/Medium/High)', max_length=99, null=True),
147+
),
148+
migrations.AddField(
149+
model_name='finding_templateevent',
150+
name='fix_available',
151+
field=models.BooleanField(blank=True, help_text='Indicates if a fix is available for this vulnerability type', null=True),
152+
),
153+
migrations.AddField(
154+
model_name='finding_templateevent',
155+
name='fix_version',
156+
field=models.CharField(blank=True, help_text='Version where fix is available', max_length=100, null=True),
157+
),
158+
migrations.AddField(
159+
model_name='finding_templateevent',
160+
name='notes',
161+
field=models.TextField(blank=True, help_text='Note content to add when applying this template', null=True),
162+
),
163+
migrations.AddField(
164+
model_name='finding_templateevent',
165+
name='planned_remediation_version',
166+
field=models.CharField(blank=True, help_text='Target version for remediation', max_length=99, null=True),
167+
),
168+
migrations.AddField(
169+
model_name='finding_templateevent',
170+
name='severity_justification',
171+
field=models.TextField(blank=True, help_text='Explanation of why this severity level is appropriate', null=True),
172+
),
173+
migrations.AddField(
174+
model_name='finding_templateevent',
175+
name='steps_to_reproduce',
176+
field=models.TextField(blank=True, help_text='Standard reproduction steps for this vulnerability type', null=True),
177+
),
178+
migrations.AddField(
179+
model_name='finding_templateevent',
180+
name='vulnerability_ids_text',
181+
field=models.TextField(blank=True, help_text='Vulnerability IDs (one per line)', null=True),
182+
),
183+
migrations.AddField(
184+
model_name='finding_templateevent',
185+
name='endpoints_text',
186+
field=models.TextField(blank=True, help_text='Endpoint URLs (one per line)', null=True),
187+
),
188+
pgtrigger.migrations.AddTrigger(
189+
model_name='finding_template',
190+
trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_finding_templateevent" ("component_name", "component_version", "cve", "cvssv3", "cvssv3_score", "cvssv4", "cvssv4_score", "cwe", "description", "effort_for_fixing", "endpoints_text", "fix_available", "fix_version", "id", "impact", "last_used", "mitigation", "notes", "numerical_severity", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "planned_remediation_version", "refs", "severity", "severity_justification", "steps_to_reproduce", "title", "vulnerability_ids_text") VALUES (NEW."component_name", NEW."component_version", NEW."cve", NEW."cvssv3", NEW."cvssv3_score", NEW."cvssv4", NEW."cvssv4_score", NEW."cwe", NEW."description", NEW."effort_for_fixing", NEW."endpoints_text", NEW."fix_available", NEW."fix_version", NEW."id", NEW."impact", NEW."last_used", NEW."mitigation", NEW."notes", NEW."numerical_severity", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."planned_remediation_version", NEW."refs", NEW."severity", NEW."severity_justification", NEW."steps_to_reproduce", NEW."title", NEW."vulnerability_ids_text"); RETURN NULL;', hash='602d9e872906719a1c95d671a0c9e4ffe5b1b7ec', operation='INSERT', pgid='pgtrigger_insert_insert_59092', table='dojo_finding_template', when='AFTER')),
191+
),
192+
pgtrigger.migrations.AddTrigger(
193+
model_name='finding_template',
194+
trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "dojo_finding_templateevent" ("component_name", "component_version", "cve", "cvssv3", "cvssv3_score", "cvssv4", "cvssv4_score", "cwe", "description", "effort_for_fixing", "endpoints_text", "fix_available", "fix_version", "id", "impact", "last_used", "mitigation", "notes", "numerical_severity", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "planned_remediation_version", "refs", "severity", "severity_justification", "steps_to_reproduce", "title", "vulnerability_ids_text") VALUES (NEW."component_name", NEW."component_version", NEW."cve", NEW."cvssv3", NEW."cvssv3_score", NEW."cvssv4", NEW."cvssv4_score", NEW."cwe", NEW."description", NEW."effort_for_fixing", NEW."endpoints_text", NEW."fix_available", NEW."fix_version", NEW."id", NEW."impact", NEW."last_used", NEW."mitigation", NEW."notes", NEW."numerical_severity", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."planned_remediation_version", NEW."refs", NEW."severity", NEW."severity_justification", NEW."steps_to_reproduce", NEW."title", NEW."vulnerability_ids_text"); RETURN NULL;', hash='644c1afec5497d6e86c4c1c861801819b7363d61', operation='UPDATE', pgid='pgtrigger_update_update_43036', table='dojo_finding_template', when='AFTER')),
195+
),
196+
pgtrigger.migrations.AddTrigger(
197+
model_name='finding_template',
198+
trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_finding_templateevent" ("component_name", "component_version", "cve", "cvssv3", "cvssv3_score", "cvssv4", "cvssv4_score", "cwe", "description", "effort_for_fixing", "endpoints_text", "fix_available", "fix_version", "id", "impact", "last_used", "mitigation", "notes", "numerical_severity", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "planned_remediation_version", "refs", "severity", "severity_justification", "steps_to_reproduce", "title", "vulnerability_ids_text") VALUES (OLD."component_name", OLD."component_version", OLD."cve", OLD."cvssv3", OLD."cvssv3_score", OLD."cvssv4", OLD."cvssv4_score", OLD."cwe", OLD."description", OLD."effort_for_fixing", OLD."endpoints_text", OLD."fix_available", OLD."fix_version", OLD."id", OLD."impact", OLD."last_used", OLD."mitigation", OLD."notes", OLD."numerical_severity", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."planned_remediation_version", OLD."refs", OLD."severity", OLD."severity_justification", OLD."steps_to_reproduce", OLD."title", OLD."vulnerability_ids_text"); RETURN NULL;', hash='925844fc74c390b9d4fc446e0d3f44f556817fd4', operation='DELETE', pgid='pgtrigger_delete_delete_3f3a6', table='dojo_finding_template', when='AFTER')),
199+
)
200+
]

0 commit comments

Comments
 (0)