Skip to content

Commit 7cf5b09

Browse files
API: prevent duplicate saves of taggable entities or when pushing to JIRA (#12607)
* prevent duplicate saves * readd TagSerializer * rerecord JIRAImportAndPushTestApi.test_groups_create_edit_update_finding * remove superfluous push_all_issues check * add unit tests for tags on products * remove duplicate patch_product_api method * Remove TaggitSerializer * rename sub_tag * cleanup logging * remove _pop_tags from FindingTemplate Create * adjust vulnerability id code * adjust vulnerability id code * adjust vulnerability id code * cleanup logging Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> * fix unsaved_vulnerability_ids error * add tests for vulnerability_ids * cleanup logging --------- Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com>
1 parent 385b96d commit 7cf5b09

10 files changed

+1237
-955
lines changed

dojo/api_v2/exception_handler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def custom_exception_handler(exc, context):
4646
# There is no standard error response, so we assume an unexpected
4747
# exception. It is logged but no details are given to the user,
4848
# to avoid leaking internal technical information.
49-
logger.error(exc)
49+
logger.error(exc, exc_info=True) # noqa: LOG014
5050
response = Response()
5151
response.status_code = HTTP_500_INTERNAL_SERVER_ERROR
5252
response.data = {}
@@ -67,6 +67,6 @@ def custom_exception_handler(exc, context):
6767
else:
6868
# HTTP status code 500 or higher are technical errors.
6969
# They get logged and we don't change the response.
70-
logger.error(exc)
70+
logger.error(exc, exc_info=True) # noqa: LOG014
7171

7272
return response

dojo/api_v2/serializers.py

Lines changed: 50 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ def to_internal_value(self, data):
237237
tag_validator(sub, exception_class=RestFrameworkValidationError)
238238
data_safe.extend(substrings)
239239

240-
return tagulous.utils.render_tags(data_safe)
240+
logger.debug(f"result after rendering tags: {data_safe}")
241+
return data_safe
241242

242243
def to_representation(self, value):
243244
if not isinstance(value, list):
@@ -254,44 +255,6 @@ def to_representation(self, value):
254255
return value
255256

256257

257-
class TaggitSerializer(serializers.Serializer):
258-
def create(self, validated_data):
259-
to_be_tagged, validated_data = self._pop_tags(validated_data)
260-
261-
tag_object = super().create(validated_data)
262-
263-
return self._save_tags(tag_object, to_be_tagged)
264-
265-
def update(self, instance, validated_data):
266-
to_be_tagged, validated_data = self._pop_tags(validated_data)
267-
268-
tag_object = super().update(
269-
instance, validated_data,
270-
)
271-
272-
return self._save_tags(tag_object, to_be_tagged)
273-
274-
def _save_tags(self, tag_object, tags):
275-
for key in list(tags.keys()):
276-
tag_values = tags.get(key)
277-
# tag_object.tags = ", ".join(tag_values)
278-
tag_object.tags = tag_values
279-
tag_object.save()
280-
281-
return tag_object
282-
283-
def _pop_tags(self, validated_data):
284-
to_be_tagged = {}
285-
286-
for key in list(self.fields.keys()):
287-
field = self.fields[key]
288-
if isinstance(field, TagListSerializerField):
289-
if key in validated_data:
290-
to_be_tagged[key] = validated_data.pop(key)
291-
292-
return (to_be_tagged, validated_data)
293-
294-
295258
class RequestResponseDict(collections.UserList):
296259
def __init__(self, *args, **kwargs):
297260
pretty_print = kwargs.pop("pretty_print", True)
@@ -1094,7 +1057,7 @@ class Meta:
10941057
fields = "__all__"
10951058

10961059

1097-
class EngagementSerializer(TaggitSerializer, serializers.ModelSerializer):
1060+
class EngagementSerializer(serializers.ModelSerializer):
10981061
tags = TagListSerializerField(required=False)
10991062

11001063
class Meta:
@@ -1151,7 +1114,7 @@ class Meta:
11511114
fields = "__all__"
11521115

11531116

1154-
class AppAnalysisSerializer(TaggitSerializer, serializers.ModelSerializer):
1117+
class AppAnalysisSerializer(serializers.ModelSerializer):
11551118
tags = TagListSerializerField(required=False)
11561119

11571120
class Meta:
@@ -1246,7 +1209,7 @@ def update(self, instance, validated_data):
12461209
raise
12471210

12481211

1249-
class EndpointSerializer(TaggitSerializer, serializers.ModelSerializer):
1212+
class EndpointSerializer(serializers.ModelSerializer):
12501213
tags = TagListSerializerField(required=False)
12511214

12521215
class Meta:
@@ -1440,7 +1403,7 @@ class Meta:
14401403
fields = ("id", "name", "test", "jira_issue")
14411404

14421405

1443-
class TestSerializer(TaggitSerializer, serializers.ModelSerializer):
1406+
class TestSerializer(serializers.ModelSerializer):
14441407
tags = TagListSerializerField(required=False)
14451408
test_type_name = serializers.ReadOnlyField()
14461409
finding_groups = FindingGroupSerializer(
@@ -1459,7 +1422,7 @@ def build_relational_field(self, field_name, relation_info):
14591422
return super().build_relational_field(field_name, relation_info)
14601423

14611424

1462-
class TestCreateSerializer(TaggitSerializer, serializers.ModelSerializer):
1425+
class TestCreateSerializer(serializers.ModelSerializer):
14631426
engagement = serializers.PrimaryKeyRelatedField(
14641427
queryset=Engagement.objects.all(),
14651428
)
@@ -1476,7 +1439,7 @@ class Meta:
14761439
exclude = ("inherited_tags",)
14771440

14781441

1479-
class TestTypeSerializer(TaggitSerializer, serializers.ModelSerializer):
1442+
class TestTypeSerializer(serializers.ModelSerializer):
14801443
tags = TagListSerializerField(required=False)
14811444

14821445
class Meta:
@@ -1702,7 +1665,7 @@ class Meta:
17021665
fields = ["vulnerability_id"]
17031666

17041667

1705-
class FindingSerializer(TaggitSerializer, serializers.ModelSerializer):
1668+
class FindingSerializer(serializers.ModelSerializer):
17061669
tags = TagListSerializerField(required=False)
17071670
request_response = serializers.SerializerMethodField()
17081671
accepted_risks = RiskAcceptanceSerializer(
@@ -1771,41 +1734,32 @@ def process_risk_acceptance(self, data):
17711734

17721735
# Overriding this to push add Push to JIRA functionality
17731736
def update(self, instance, validated_data):
1774-
# remove tags from validated data and store them seperately
1775-
to_be_tagged, validated_data = self._pop_tags(validated_data)
1776-
1777-
# pop push_to_jira so it won't get send to the model as a field
1778-
# TODO: JIRA can we remove this is_push_all_issues, already checked in
1779-
# apiv2 viewset?
1780-
push_to_jira = validated_data.pop(
1781-
"push_to_jira",
1782-
) or jira_helper.is_push_all_issues(instance)
1737+
# push_all_issues already checked in api views.py
1738+
push_to_jira = validated_data.pop("push_to_jira")
17831739

17841740
# Save vulnerability ids and pop them
1785-
if "vulnerability_id_set" in validated_data:
1786-
vulnerability_id_set = validated_data.pop("vulnerability_id_set")
1787-
vulnerability_ids = []
1788-
if vulnerability_id_set:
1789-
vulnerability_ids.extend(vulnerability_id["vulnerability_id"] for vulnerability_id in vulnerability_id_set)
1790-
save_vulnerability_ids(instance, vulnerability_ids)
1741+
parsed_vulnerability_ids = []
1742+
if (vulnerability_ids := validated_data.pop("vulnerability_id_set", None)):
1743+
logger.debug("VULNERABILITY_ID_SET: %s", vulnerability_ids)
1744+
parsed_vulnerability_ids.extend(vulnerability_id["vulnerability_id"] for vulnerability_id in vulnerability_ids)
1745+
logger.debug("SETTING CVE FROM VULNERABILITY_ID_SET: %s", parsed_vulnerability_ids[0])
1746+
validated_data["cve"] = parsed_vulnerability_ids[0]
17911747

1792-
instance = super(TaggitSerializer, self).update(
1793-
instance, validated_data,
1794-
)
17951748
# Save the reporter on the finding
17961749
if reporter_id := validated_data.get("reporter"):
17971750
instance.reporter = reporter_id
17981751

1799-
# If we need to push to JIRA, an extra save call is needed.
1800-
# Also if we need to update the mitigation date of the finding.
1801-
# TODO: try to combine create and save, but for now I'm just fixing a
1802-
# bug and don't want to change to much
1752+
instance = super().update(
1753+
instance, validated_data,
1754+
)
1755+
1756+
if parsed_vulnerability_ids:
1757+
save_vulnerability_ids(instance, parsed_vulnerability_ids)
1758+
18031759
if push_to_jira:
1804-
instance.save(push_to_jira=push_to_jira)
1760+
jira_helper.push_to_jira(instance)
18051761

1806-
# not sure why we are returning a tag_object, but don't want to change
1807-
# too much now as we're just fixing a bug
1808-
return self._save_tags(instance, to_be_tagged)
1762+
return instance
18091763

18101764
def validate(self, data):
18111765
if self.context["request"].method == "PATCH":
@@ -1876,7 +1830,7 @@ def get_request_response(self, obj):
18761830
return serialized_burps.data
18771831

18781832

1879-
class FindingCreateSerializer(TaggitSerializer, serializers.ModelSerializer):
1833+
class FindingCreateSerializer(serializers.ModelSerializer):
18801834
notes = serializers.PrimaryKeyRelatedField(
18811835
read_only=True, allow_null=True, required=False, many=True,
18821836
)
@@ -1908,21 +1862,24 @@ class Meta:
19081862

19091863
# Overriding this to push add Push to JIRA functionality
19101864
def create(self, validated_data):
1911-
# Pop off of some fields that should not be sent to the model at this time
1912-
to_be_tagged, validated_data = self._pop_tags(validated_data)
1865+
logger.debug(f"Creating finding with validated data: {validated_data}")
19131866
push_to_jira = validated_data.pop("push_to_jira", False)
19141867
notes = validated_data.pop("notes", None)
19151868
found_by = validated_data.pop("found_by", None)
19161869
reviewers = validated_data.pop("reviewers", None)
19171870
# Process the vulnerability IDs specially
19181871
parsed_vulnerability_ids = []
19191872
if (vulnerability_ids := validated_data.pop("vulnerability_id_set", None)):
1873+
logger.debug("VULNERABILITY_ID_SET: %s", vulnerability_ids)
19201874
parsed_vulnerability_ids.extend(vulnerability_id["vulnerability_id"] for vulnerability_id in vulnerability_ids)
1875+
logger.debug("SETTING CVE FROM VULNERABILITY_ID_SET: %s", parsed_vulnerability_ids[0])
19211876
validated_data["cve"] = parsed_vulnerability_ids[0]
1922-
# Create a findings in memory so that we have access to unsaved_vulnerability_ids
1923-
new_finding = Finding(**validated_data)
1924-
new_finding.unsaved_vulnerability_ids = parsed_vulnerability_ids
1925-
new_finding.save()
1877+
1878+
new_finding = super().create(
1879+
validated_data)
1880+
1881+
logger.debug(f"New finding CVE: {new_finding.cve}")
1882+
19261883
# Deal with all of the many to many things
19271884
if notes:
19281885
new_finding.notes.set(notes)
@@ -1932,18 +1889,14 @@ def create(self, validated_data):
19321889
new_finding.reviewers.set(reviewers)
19331890
if parsed_vulnerability_ids:
19341891
save_vulnerability_ids(new_finding, parsed_vulnerability_ids)
1935-
# TODO: JIRA can we remove this is_push_all_issues, already checked in
1936-
# apiv2 viewset?
1937-
push_to_jira = push_to_jira or jira_helper.is_push_all_issues(
1938-
new_finding,
1939-
)
1940-
# If we need to push to JIRA, an extra save call is needed.
1941-
# TODO: try to combine create and save, but for now I'm just fixing a
1942-
# bug and don't want to change to much
1943-
if push_to_jira or new_finding:
1944-
new_finding.save(push_to_jira=push_to_jira)
1945-
# This final call will save the finding again and return it
1946-
return self._save_tags(new_finding, to_be_tagged)
1892+
# can we avoid this extra save? the cve has already been set above in validated_data. but there are no tests for this
1893+
# on finding update nothing is done # with vulnerability_ids?
1894+
# new_finding.save()
1895+
1896+
if push_to_jira:
1897+
jira_helper.push_to_jira(new_finding)
1898+
1899+
return new_finding
19471900

19481901
def validate(self, data):
19491902
if "reporter" not in data:
@@ -1989,7 +1942,7 @@ class Meta:
19891942
fields = ["vulnerability_id"]
19901943

19911944

1992-
class FindingTemplateSerializer(TaggitSerializer, serializers.ModelSerializer):
1945+
class FindingTemplateSerializer(serializers.ModelSerializer):
19931946
tags = TagListSerializerField(required=False)
19941947
vulnerability_ids = VulnerabilityIdTemplateSerializer(
19951948
source="vulnerability_id_template_set", many=True, required=False,
@@ -2000,7 +1953,6 @@ class Meta:
20001953
exclude = ("cve",)
20011954

20021955
def create(self, validated_data):
2003-
to_be_tagged, validated_data = self._pop_tags(validated_data)
20041956

20051957
# Save vulnerability ids and pop them
20061958
if "vulnerability_id_template_set" in validated_data:
@@ -2010,7 +1962,7 @@ def create(self, validated_data):
20101962
else:
20111963
vulnerability_id_set = None
20121964

2013-
new_finding_template = super(TaggitSerializer, self).create(
1965+
new_finding_template = super().create(
20141966
validated_data,
20151967
)
20161968

@@ -2022,7 +1974,6 @@ def create(self, validated_data):
20221974
)
20231975
new_finding_template.save()
20241976

2025-
self._save_tags(new_finding_template, to_be_tagged)
20261977
return new_finding_template
20271978

20281979
def update(self, instance, validated_data):
@@ -2036,7 +1987,7 @@ def update(self, instance, validated_data):
20361987
vulnerability_ids.extend(vulnerability_id["vulnerability_id"] for vulnerability_id in vulnerability_id_set)
20371988
save_vulnerability_ids_template(instance, vulnerability_ids)
20381989

2039-
return super(TaggitSerializer, self).update(instance, validated_data)
1990+
return super().update(instance, validated_data)
20401991

20411992

20421993
class CredentialSerializer(serializers.ModelSerializer):
@@ -2080,7 +2031,7 @@ def validate_severity(self, value: str) -> str:
20802031
return value
20812032

20822033

2083-
class ProductSerializer(TaggitSerializer, serializers.ModelSerializer):
2034+
class ProductSerializer(serializers.ModelSerializer):
20842035
findings_count = serializers.SerializerMethodField()
20852036
findings_list = serializers.SerializerMethodField()
20862037

@@ -2411,7 +2362,7 @@ def save(self, *, push_to_jira=False):
24112362
self.process_scan(data, context)
24122363

24132364

2414-
class ReImportScanSerializer(TaggitSerializer, CommonImportScanSerializer):
2365+
class ReImportScanSerializer(CommonImportScanSerializer):
24152366

24162367
help_do_not_reactivate = "Select if the import should ignore active findings from the report, useful for triage-less scanners. Will keep existing findings closed, without reactivating them. For more information check the docs."
24172368
do_not_reactivate = serializers.BooleanField(
@@ -2791,7 +2742,7 @@ class TagSerializer(serializers.Serializer):
27912742
tags = TagListSerializerField(required=True)
27922743

27932744

2794-
class SystemSettingsSerializer(TaggitSerializer, serializers.ModelSerializer):
2745+
class SystemSettingsSerializer(serializers.ModelSerializer):
27952746
class Meta:
27962747
model = System_Settings
27972748
fields = "__all__"

dojo/api_v2/views.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -989,13 +989,13 @@ def tags(self, request, pk=None):
989989
all_tags = serializers.TagSerializer({"tags": all_tags}).data[
990990
"tags"
991991
]
992+
for tag in new_tags.validated_data["tags"]:
993+
for sub_tag in tagulous.utils.parse_tags(tag):
994+
if sub_tag not in all_tags:
995+
all_tags.append(sub_tag)
992996

993-
for tag in tagulous.utils.parse_tags(
994-
new_tags.validated_data["tags"],
995-
):
996-
if tag not in all_tags:
997-
all_tags.append(tag)
998997
new_tags = tagulous.utils.render_tags(all_tags)
998+
999999
finding.tags = new_tags
10001000
finding.save()
10011001
else:
@@ -1238,19 +1238,18 @@ def remove_tags(self, request, pk=None):
12381238
]
12391239

12401240
# serializer turns it into a string, but we need a list
1241-
del_tags = tagulous.utils.parse_tags(
1242-
delete_tags.validated_data["tags"],
1243-
)
1241+
del_tags = delete_tags.validated_data["tags"]
12441242
if len(del_tags) < 1:
12451243
return Response(
12461244
{"error": "Empty Tag List Not Allowed"},
12471245
status=status.HTTP_400_BAD_REQUEST,
12481246
)
1247+
12491248
for tag in del_tags:
12501249
if tag not in all_tags:
12511250
return Response(
12521251
{
1253-
"error": f"'{tag}' is not a valid tag in list",
1252+
"error": f"'{tag}' is not a valid tag in list '{all_tags}'",
12541253
},
12551254
status=status.HTTP_400_BAD_REQUEST,
12561255
)
@@ -2508,7 +2507,7 @@ def perform_create(self, serializer):
25082507
jira_driver = engagement or (product or None)
25092508
if jira_project := (jira_helper.get_jira_project(jira_driver) if jira_driver else None):
25102509
push_to_jira = push_to_jira or jira_project.push_all_issues
2511-
logger.debug(f"push_to_jira: {push_to_jira}")
2510+
# logger.debug(f"push_to_jira: {push_to_jira}")
25122511
serializer.save(push_to_jira=push_to_jira)
25132512

25142513
def get_queryset(self):

dojo/jira_link/helper.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,9 @@ def push_to_jira(obj, *args, **kwargs):
667667
raise ValueError(msg)
668668

669669
if isinstance(obj, Finding):
670+
if obj.has_finding_group:
671+
logger.debug("pushing finding group for %s to JIRA", obj)
672+
return push_finding_group_to_jira(obj.finding_group, *args, **kwargs)
670673
return push_finding_to_jira(obj, *args, **kwargs)
671674

672675
if isinstance(obj, Finding_Group):

dojo/models.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2678,7 +2678,6 @@ def __str__(self):
26782678
def save(self, dedupe_option=True, rules_option=True, product_grading_option=True, # noqa: FBT002
26792679
issue_updater_option=True, push_to_jira=False, user=None, *args, **kwargs): # noqa: FBT002 - this is bit hard to fix nice have this universally fixed
26802680
logger.debug("Start saving finding of id " + str(self.id) + " dedupe_option:" + str(dedupe_option) + " (self.pk is %s)", "None" if self.pk is None else "not None")
2681-
26822681
from dojo.finding import helper as finding_helper
26832682

26842683
# if not isinstance(self.date, (datetime, date)):

0 commit comments

Comments
 (0)