@@ -667,3 +667,124 @@ def test_webhook_issue_updated_extracts_comment(self):
667667 last_note = finding .notes .order_by ("-id" ).first ()
668668 self .assertIsNotNone (last_note )
669669 self .assertEqual (last_note .entry , "(Valentijn Scholten (valentijn)): test2" )
670+
671+ # ---- statusCategory guard on the mitigation path ----
672+ # The webhook payload's issue.fields.resolution used to be the sole
673+ # signal for whether the Jira issue was closed, which caused several
674+ # real-world false-mitigations:
675+ # (a) Jira workflows where the Reopen transition does not clear
676+ # resolution - the issue ends up in status=To-Do with a stale
677+ # resolution value, and every webhook mis-mitigated the finding.
678+ # (b) Ricochets during DefectDojo's own push to Jira: when a finding
679+ # is reopened, the issue.update() and the subsequent transition
680+ # fire separate webhooks, and the first one sees the
681+ # pre-transition (still-resolved) state.
682+ # The fix: a webhook only mitigates when BOTH resolution is non-null
683+ # AND statusCategory.key == "done". These tests lock that in.
684+
685+ def _update_body_with_status (self , * , status_name , status_category_key ):
686+ body = json .loads (self .jira_issue_update_template_string )
687+ # Target the JIRA_Issue linked to finding 5 (jira_id=2 in the fixture)
688+ body ["issue" ]["id" ] = 2
689+ body ["issue" ]["fields" ]["status" ]["name" ] = status_name
690+ body ["issue" ]["fields" ]["status" ]["statusCategory" ]["key" ] = status_category_key
691+ # The template already carries a non-null resolution ("Cancelled").
692+ # That is the interesting state: resolution is set but status may or
693+ # may not actually be "done".
694+ return body
695+
696+ def _reset_finding_to_active (self , finding ):
697+ finding .active = True
698+ finding .is_mitigated = False
699+ finding .mitigated = None
700+ finding .mitigated_by = None
701+ finding .false_p = False
702+ finding .risk_accepted = False
703+ finding .save ()
704+
705+ def test_webhook_update_does_not_mitigate_when_status_category_is_new (self ):
706+ """
707+ Regression: resolution set + statusCategory 'new' must NOT mitigate.
708+
709+ This is the real-world symptom seen when a Jira Reopen transition
710+ forgets to clear the resolution field, or when DefectDojo's own
711+ multi-step push to Jira fires a webhook against the pre-transition
712+ state during a finding reopen.
713+ """
714+ self .system_settings (
715+ enable_jira = True , enable_jira_web_hook = True ,
716+ disable_jira_webhook_secret = False ,
717+ jira_webhook_secret = self .correct_secret ,
718+ )
719+ jira_issue = JIRA_Issue .objects .get (jira_id = 2 )
720+ finding = jira_issue .finding
721+ self ._reset_finding_to_active (finding )
722+
723+ body = self ._update_body_with_status (
724+ status_name = "To Do" , status_category_key = "new" ,
725+ )
726+ response = self .client .post (
727+ reverse ("jira_web_hook_secret" , args = (self .correct_secret , )),
728+ body ,
729+ content_type = "application/json" ,
730+ )
731+ self .assertEqual (200 , response .status_code , response .content [:1000 ])
732+
733+ finding .refresh_from_db ()
734+ self .assertTrue (finding .active )
735+ self .assertFalse (finding .is_mitigated )
736+ self .assertIsNone (finding .mitigated )
737+ self .assertIsNone (finding .mitigated_by )
738+
739+ def test_webhook_update_does_not_mitigate_when_status_category_is_indeterminate (self ):
740+ """Same guard for issues in the 'In Progress' category."""
741+ self .system_settings (
742+ enable_jira = True , enable_jira_web_hook = True ,
743+ disable_jira_webhook_secret = False ,
744+ jira_webhook_secret = self .correct_secret ,
745+ )
746+ jira_issue = JIRA_Issue .objects .get (jira_id = 2 )
747+ finding = jira_issue .finding
748+ self ._reset_finding_to_active (finding )
749+
750+ body = self ._update_body_with_status (
751+ status_name = "In Progress" , status_category_key = "indeterminate" ,
752+ )
753+ response = self .client .post (
754+ reverse ("jira_web_hook_secret" , args = (self .correct_secret , )),
755+ body ,
756+ content_type = "application/json" ,
757+ )
758+ self .assertEqual (200 , response .status_code , response .content [:1000 ])
759+
760+ finding .refresh_from_db ()
761+ self .assertTrue (finding .active )
762+ self .assertFalse (finding .is_mitigated )
763+
764+ def test_webhook_update_mitigates_when_status_category_is_done (self ):
765+ """Happy path: a genuinely-closed Jira issue still mitigates its finding."""
766+ self .system_settings (
767+ enable_jira = True , enable_jira_web_hook = True ,
768+ disable_jira_webhook_secret = False ,
769+ jira_webhook_secret = self .correct_secret ,
770+ )
771+ jira_issue = JIRA_Issue .objects .get (jira_id = 2 )
772+ finding = jira_issue .finding
773+ self ._reset_finding_to_active (finding )
774+
775+ body = self ._update_body_with_status (
776+ status_name = "Done" , status_category_key = "done" ,
777+ )
778+ response = self .client .post (
779+ reverse ("jira_web_hook_secret" , args = (self .correct_secret , )),
780+ body ,
781+ content_type = "application/json" ,
782+ )
783+ self .assertEqual (200 , response .status_code , response .content [:1000 ])
784+
785+ finding .refresh_from_db ()
786+ self .assertFalse (finding .active )
787+ self .assertTrue (finding .is_mitigated )
788+ self .assertIsNotNone (finding .mitigated )
789+ self .assertIsNotNone (finding .mitigated_by )
790+ self .assertEqual (finding .mitigated_by .username , "JIRA" )
0 commit comments