Skip to content

Commit d38dfdf

Browse files
risk acceptance expiration: keep link with findings (#12737)
* risk acceptance expiration: keep link with findings * push to jira on reinstate/expiry of Risk Acceptance * move save call out of jira helper * fix tests * remve obsolete comment
1 parent 88e9f59 commit d38dfdf

7 files changed

Lines changed: 9764 additions & 1612 deletions

dojo/finding/views.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1410,7 +1410,9 @@ def reopen_finding(request, fid):
14101410
status.save()
14111411
# Clear the risk acceptance, if present
14121412
ra_helper.risk_unaccept(request.user, finding)
1413-
jira_helper.save_and_push_to_jira(finding)
1413+
finding.save(dedupe_option=False, push_to_jira=False)
1414+
if jira_helper.is_push_all_issues(finding) or jira_helper.is_keep_in_sync_with_jira(finding):
1415+
jira_helper.push_to_jira(finding)
14141416

14151417
reopen_external_issue(finding, "re-opened by defectdojo", "github")
14161418

dojo/jira_link/helper.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,17 @@ def _safely_get_obj_status_for_jira(obj: Finding | Finding_Group, *, isenforced:
142142
return status or ["Inactive"]
143143

144144

145+
def is_keep_in_sync_with_jira(finding):
146+
keep_in_sync_enabled = False
147+
# Check if there is a jira issue that needs to be updated
148+
jira_issue_exists = finding.has_jira_issue or (finding.finding_group and finding.finding_group.has_jira_issue)
149+
if jira_issue_exists:
150+
# Determine if any automatic sync should occur
151+
keep_in_sync_enabled = get_jira_instance(finding).finding_jira_sync
152+
153+
return keep_in_sync_enabled
154+
155+
145156
# checks if a finding can be pushed to JIRA
146157
# optionally provides a form with the new data for the finding
147158
# any finding that already has a JIRA issue can be pushed again to JIRA
@@ -1765,7 +1776,14 @@ def process_resolution_from_jira(finding, resolution_id, resolution_name, assign
17651776
jira_instance = get_jira_instance(finding)
17661777

17671778
if resolved:
1768-
if jira_instance and resolution_name in jira_instance.accepted_resolutions and (finding.test.engagement.product.enable_simple_risk_acceptance or finding.test.engagement.enable_full_risk_acceptance):
1779+
if (
1780+
jira_instance
1781+
and resolution_name in jira_instance.accepted_resolutions
1782+
and (
1783+
finding.test.engagement.product.enable_simple_risk_acceptance
1784+
or finding.test.engagement.enable_full_risk_acceptance
1785+
)
1786+
):
17691787
if not finding.risk_accepted:
17701788
logger.debug(f"Marking related finding of {jira_issue.jira_key} as accepted.")
17711789
finding.risk_accepted = True
@@ -1823,26 +1841,6 @@ def process_resolution_from_jira(finding, resolution_id, resolution_name, assign
18231841
return status_changed
18241842

18251843

1826-
def save_and_push_to_jira(finding):
1827-
# Manage the jira status changes
1828-
push_to_jira_decision = False
1829-
# Determine if the finding is in a group. if so, not push to jira yet
1830-
finding_in_group = finding.has_finding_group
1831-
# Check if there is a jira issue that needs to be updated
1832-
jira_issue_exists = finding.has_jira_issue or (finding.finding_group and finding.finding_group.has_jira_issue)
1833-
# Only push if the finding is not in a group
1834-
if jira_issue_exists:
1835-
# Determine if any automatic sync should occur
1836-
push_to_jira_decision = is_push_all_issues(finding) \
1837-
or get_jira_instance(finding).finding_jira_sync
1838-
# Save the finding
1839-
finding.save(push_to_jira=(push_to_jira_decision and not finding_in_group))
1840-
# we only push the group after saving the finding to make sure
1841-
# the updated data of the finding is pushed as part of the group
1842-
if push_to_jira_decision and finding_in_group:
1843-
push_to_jira(finding.finding_group)
1844-
1845-
18461844
def get_finding_group_findings_above_threshold(finding_group):
18471845
"""Get the findings that are above the minimum threshold"""
18481846
jira_minimum_threshold = 0

dojo/risk_acceptance/helper.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,25 @@ def expire_now(risk_acceptance):
2424
for finding in risk_acceptance.accepted_findings.all():
2525
if not finding.active: # not sure why this is important
2626
logger.debug("%i:%s: unaccepting/reactivating finding.", finding.id, finding)
27+
finding.active = True
28+
finding.risk_accepted = False
2729

2830
# Update any endpoint statuses on each of the findings
2931
update_endpoint_statuses(finding, accept_risk=False)
30-
risk_unaccept(None, finding, post_comments=False) # comments will be posted at end
3132

3233
if risk_acceptance.restart_sla_expired:
3334
finding.sla_start_date = timezone.now().date()
34-
finding.save(dedupe_option=False) # resave if changed after risk_unaccept
35+
# this method both saves and pushed to JIRA (no other post processing)
36+
finding.save(dedupe_option=False)
37+
if jira_helper.is_push_all_issues(finding) or jira_helper.is_keep_in_sync_with_jira(finding):
38+
logger.info("pushing finding to JIRA after expiration of risk acceptance")
39+
jira_helper.push_to_jira(finding)
3540

3641
reactivated_findings.append(finding)
3742
else:
3843
logger.debug("%i:%s already active, no changes made.", finding.id, finding)
3944

45+
# best effort JIRA integration, no status changes, just a comment
4046
post_jira_comments(risk_acceptance, risk_acceptance.accepted_findings.all(), expiration_message_creator)
4147

4248
risk_acceptance.expiration_date = timezone.now()
@@ -68,12 +74,16 @@ def reinstate(risk_acceptance, old_expiration_date):
6874
finding.risk_accepted = True
6975
# Update any endpoint statuses on each of the findings
7076
update_endpoint_statuses(finding, accept_risk=True)
77+
# this method both saves and pushed to JIRA (no other post processing)
7178
finding.save(dedupe_option=False)
79+
if jira_helper.is_push_all_issues(finding) or jira_helper.is_keep_in_sync_with_jira(finding):
80+
logger.info("pushing finding to JIRA after reinstating risk acceptance")
81+
jira_helper.push_to_jira(finding)
7282
reinstated_findings.append(finding)
7383
else:
7484
logger.debug("%i:%s: already inactive, not making any changes", finding.id, finding)
7585

76-
# best effort JIRA integration, no status changes
86+
# best effort JIRA integration, no status changes, just a comment
7787
post_jira_comments(risk_acceptance, risk_acceptance.accepted_findings.all(), reinstation_message_creator)
7888

7989
risk_acceptance.expiration_date_handled = None
@@ -108,7 +118,12 @@ def remove_finding_from_risk_acceptance(user: Dojo_User, risk_acceptance: Risk_A
108118
finding.risk_accepted = False
109119
# Update any endpoint statuses on each of the findings
110120
update_endpoint_statuses(finding, accept_risk=False)
121+
# this method both saves and pushed to JIRA (no other post processing)
111122
finding.save(dedupe_option=False)
123+
if jira_helper.is_push_all_issues(finding) or jira_helper.is_keep_in_sync_with_jira(finding):
124+
logger.info("pushing finding to JIRA after removal from risk acceptance")
125+
jira_helper.push_to_jira(finding)
126+
112127
# best effort jira integration, no status changes
113128
post_jira_comments(risk_acceptance, [finding], unaccepted_message_creator)
114129
# Add a note to reflect that the finding was removed from the risk acceptance
@@ -132,7 +147,13 @@ def add_findings_to_risk_acceptance(user: Dojo_User, risk_acceptance: Risk_Accep
132147
finding.save(dedupe_option=False)
133148
# Update any endpoint statuses on each of the findings
134149
update_endpoint_statuses(finding, accept_risk=True)
150+
135151
risk_acceptance.accepted_findings.add(finding)
152+
153+
if jira_helper.is_push_all_issues(finding) or jira_helper.is_keep_in_sync_with_jira(finding):
154+
logger.info("pushing finding to JIRA after adding to risk acceptance")
155+
jira_helper.push_to_jira(finding)
156+
136157
# Add a note to reflect that the finding was removed from the risk acceptance
137158
if user is not None:
138159
finding.notes.add(Notes.objects.create(
@@ -314,6 +335,9 @@ def simple_risk_accept(user: Dojo_User, finding: Finding, *, perform_save=True)
314335
finding.save(dedupe_option=False)
315336
# post_jira_comment might reload from database so see unaccepted finding. but the comment
316337
# only contains some text so that's ok
338+
if jira_helper.is_push_all_issues(finding) or jira_helper.is_keep_in_sync_with_jira(finding):
339+
jira_helper.push_to_jira(finding)
340+
317341
post_jira_comment(finding, accepted_message_creator)
318342
# Add a note to reflect that the finding was removed from the risk acceptance
319343
if user is not None:
@@ -344,7 +368,8 @@ def risk_unaccept(user: Dojo_User, finding: Finding, *, perform_save=True, post_
344368
post_jira_comment(finding, unaccepted_message_creator)
345369

346370
# Update the JIRA obect for this finding
347-
jira_helper.save_and_push_to_jira(finding)
371+
if jira_helper.is_push_all_issues(finding) or jira_helper.is_keep_in_sync_with_jira(finding):
372+
jira_helper.push_to_jira(finding)
348373

349374
# Add a note to reflect that the finding was removed from the risk acceptance
350375
if user is not None:

dojo/templates/dojo/view_eng.html

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ <h4> Risk Acceptance
437437
Never
438438
{% endif %}
439439
</td>
440-
<td>{{ risk_acceptance.accepted_findings_count }}</td>
440+
<td><a href="{% url 'view_risk_acceptance' eng.id risk_acceptance.id %}">{{ risk_acceptance.accepted_findings_count }}</a></td>
441441
{% if risk_acceptance.filename %}
442442
<td><a href="{% url 'download_risk_acceptance' eng.id risk_acceptance.id %}">Yes</a>
443443
&nbsp;<i style="position:absolute;" class="fa has-popover fa-info-circle" title="Uploaded proof" data-trigger="hover" data-placement="bottom" data-container="body" data-html="true"
@@ -717,7 +717,7 @@ <h4>Files<span class="pull-right">
717717
<div class="col-md-4">
718718
<div class="panel panel-default-secondary">
719719
<div class="panel-heading">
720-
<h3 class="panel-title"><span class="fa-solid fa-circle-info fa-fw" aria-hidden="true"></span>
720+
<h3 class="panel-title"><span class="fa-solid fa-circle-info fa-fw" aria-hidden="true"></span>
721721
{% if eng.name %}
722722
{{ eng.name }}
723723
{% else %}
@@ -1040,25 +1040,25 @@ <h4><span class="fa-solid fa-key" aria-hidden="true"></span>
10401040
$(document).on('keypress', null, 'e', function () {
10411041
window.location.assign('{% url 'edit_engagement' eng.id %}');
10421042
});
1043-
1043+
10441044
$(document).on('keypress', null, 'a', function () {
10451045
window.location.assign('{% url 'add_tests' eng.id %}');
10461046
});
1047-
1047+
10481048
$(document).on('keypress', null, 'i', function () {
10491049
window.location.assign('{% url 'import_scan_results' eng.id %}');
10501050
});
1051-
1051+
10521052
$("a[data-toggle='collapse']").on('click', function () {
10531053
var i = $($(this).find('i').get(0));
10541054
i.toggleClass('glyphicon-chevron-up').toggleClass('glyphicon-chevron-down');
10551055
});
1056-
1056+
10571057
//Ensures dropdown has proper zindex
10581058
$('.table-responsive').on('show.bs.dropdown', function () {
10591059
$('.table-responsive').css( "overflow", "inherit" );
10601060
});
1061-
1061+
10621062
$('.table-responsive').on('hide.bs.dropdown', function () {
10631063
$('.table-responsive').css( "overflow", "auto" );
10641064
})
@@ -1067,15 +1067,15 @@ <h4><span class="fa-solid fa-key" aria-hidden="true"></span>
10671067
var terms = '';
10681068
if ($.cookie('highlight')) {
10691069
terms = $.cookie('highlight').split(' ');
1070-
1070+
10711071
for (var i = 0; i < terms.length; i++) {
10721072
$('body').highlight(terms[i]);
10731073
}
10741074
}
1075-
1075+
10761076
$('input#simple_search').val(terms);
10771077
}
1078-
1078+
10791079
$('#shareQuestionnaireModal').on('show.bs.modal', function (event) {
10801080
var button = $(event.relatedTarget) // Button that triggered the modal
10811081
var path = button.data('whatever') // Extract info from data-* attributes
@@ -1088,8 +1088,8 @@ <h4><span class="fa-solid fa-key" aria-hidden="true"></span>
10881088
modal.find('p#questionnaireURL').text('Questionnaire URL: ' + host + path)
10891089
})
10901090
});
1091-
1091+
10921092
{% include 'dojo/snippets/risk_acceptance_actions_snippet_js.html' %}
1093-
1093+
10941094
</script>
10951095
{% endblock %}

unittests/test_jira_import_and_pushing_api.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# from unittest import skip
22
import logging
3+
from unittest.mock import patch
34

45
from crum import impersonate
56
from django.urls import reverse
@@ -70,7 +71,6 @@ def setUp(self):
7071
self.testuser = User.objects.get(username="admin")
7172
self.testuser.usercontactinfo.block_execution = True
7273
self.testuser.usercontactinfo.save()
73-
7474
token = Token.objects.get(user=self.testuser)
7575
self.client = APIClient()
7676
self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key)
@@ -321,7 +321,7 @@ def add_risk_acceptance(self, eid, data_risk_accceptance, fid=None):
321321
self.assertEqual(302, response.status_code, response.content[:1000])
322322
return response
323323

324-
def test_import_grouped_reopen_expired_sla(self):
324+
def test_import_grouped_reopen_expired_risk_acceptance(self):
325325
# steps
326326
# import scan, make sure they are in grouped JIRA
327327
# risk acceptance all the grouped findings, make sure they are closed in JIRA
@@ -374,6 +374,59 @@ def test_import_grouped_reopen_expired_sla(self):
374374
# by asserting full cassette is played we know all calls to JIRA have been made as expected
375375
self.assert_cassette_played()
376376

377+
@patch("dojo.decorators.we_want_async", return_value=False)
378+
def test_import_grouped_reopen_expired_risk_acceptance_with_finding_sync(self, mock):
379+
# steps
380+
# import scan, make sure they are in grouped JIRA
381+
# risk acceptance all the grouped findings, make sure they are closed in JIRA
382+
# expire risk acceptance on all grouped findings, make sure they are open in JIRA
383+
JIRA_Instance.objects.update(finding_jira_sync=True)
384+
385+
import0 = self.import_scan_with_params(self.npm_groups_sample_filename, scan_type="NPM Audit Scan", group_by="component_name+component_version", push_to_jira=True, verified=True)
386+
test_id = import0["test"]
387+
self.assert_jira_issue_count_in_test(test_id, 0)
388+
self.assert_jira_group_issue_count_in_test(test_id, 3)
389+
findings = self.get_test_findings_api(test_id)
390+
finding_id = findings["results"][0]["id"]
391+
392+
ra_data = {
393+
"name": "Accept: Unit test",
394+
"accepted_findings": [],
395+
"recommendation": "A",
396+
"recommendation_details": "recommendation 1",
397+
"decision": "A",
398+
"decision_details": "it has been decided!",
399+
"accepted_by": "pointy haired boss",
400+
"owner": 1,
401+
"expiration_date": "2024-12-31",
402+
"reactivate_expired": True,
403+
}
404+
405+
for finding in findings["results"]:
406+
ra_data["accepted_findings"].append(finding["id"])
407+
408+
pre_jira_status = self.get_jira_issue_status(finding_id)
409+
410+
response = self.add_risk_acceptance(1, data_risk_accceptance=ra_data)
411+
self.assertEqual("/engagement/1", response.url)
412+
413+
# we don't do any explicit push to JIRA here as it should happen automatically
414+
415+
post_jira_status = self.get_jira_issue_status(finding_id)
416+
self.assertNotEqual(pre_jira_status, post_jira_status)
417+
418+
pre_jira_status = post_jira_status
419+
ra = Risk_Acceptance.objects.last()
420+
ra_helper.expire_now(ra)
421+
422+
# we don't do any explicit push to JIRA here as it should happen automatically
423+
424+
post_jira_status = self.get_jira_issue_status(finding_id)
425+
self.assertNotEqual(pre_jira_status, post_jira_status)
426+
427+
# by asserting full cassette is played we know all calls to JIRA have been made as expected
428+
self.assert_cassette_played()
429+
377430
def test_import_with_groups_twice_push_to_jira(self):
378431
import0 = self.import_scan_with_params(self.npm_groups_sample_filename, scan_type="NPM Audit Scan", group_by="component_name+component_version", push_to_jira=True, verified=True)
379432
test_id = import0["test"]

0 commit comments

Comments
 (0)