Skip to content

Commit 2f1d961

Browse files
reimport: optionally restart sla on reactivation
1 parent 5eff6e4 commit 2f1d961

File tree

5 files changed

+133
-7
lines changed

5 files changed

+133
-7
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.1.8 on 2025-07-23 06:48
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('dojo', '0238_finding_fix_available'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='sla_configuration',
15+
name='restart_sla_on_reactivation',
16+
field=models.BooleanField(default=False, help_text='When enabled, findings that were previously mitigated but are reactivated durign reimport will have their SLA period restarted.', verbose_name='Restart SLA when findings are reactivated'),
17+
),
18+
]

dojo/forms.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2793,10 +2793,12 @@ def __init__(self, *args, **kwargs):
27932793
self.fields["low"].disabled = True
27942794
self.fields["enforce_low"].disabled = True
27952795
self.fields["low"].widget.attrs["message"] = msg
2796+
self.fields["restart_sla_on_reactivation"].disabled = True
2797+
self.fields["restart_sla_on_reactivation"].widget.attrs["message"] = msg
27962798

27972799
class Meta:
27982800
model = SLA_Configuration
2799-
fields = ["name", "description", "critical", "enforce_critical", "high", "enforce_high", "medium", "enforce_medium", "low", "enforce_low"]
2801+
fields = ["name", "description", "critical", "enforce_critical", "high", "enforce_high", "medium", "enforce_medium", "low", "enforce_low", "restart_sla_on_reactivation"]
28002802

28012803

28022804
class DeleteSLAConfigForm(forms.ModelForm):

dojo/importers/default_reimporter.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,10 @@ def process_matched_mitigated_finding(
498498
component_version = getattr(unsaved_finding, "component_version", None)
499499
existing_finding.component_name = existing_finding.component_name or component_name
500500
existing_finding.component_version = existing_finding.component_version or component_version
501+
if existing_finding.get_sla_configuration().restart_sla_on_reactivation:
502+
# restart the sla start date to the current date, finding.save() will set new sla_expiration_date
503+
existing_finding.sla_start_date = self.now
504+
501505
existing_finding.save(dedupe_option=False)
502506
# don't dedupe before endpoints are added
503507
existing_finding.save(dedupe_option=False)

dojo/models.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,10 @@ class SLA_Configuration(models.Model):
10331033
default=True,
10341034
verbose_name=_("Enforce Low Finding SLA Days"),
10351035
help_text=_("When enabled, low findings will be assigned an SLA expiration date based on the low finding SLA days within this SLA configuration."))
1036+
restart_sla_on_reactivation = models.BooleanField(
1037+
default=False,
1038+
verbose_name=_("Restart SLA when findings are reactivated"),
1039+
help_text=_("When enabled, findings that were previously mitigated but are reactivated durign reimport will have their SLA period restarted."))
10361040
async_updating = models.BooleanField(
10371041
default=False,
10381042
help_text=_("Findings under this SLA configuration are asynchronously being updated"))
@@ -3104,8 +3108,11 @@ def get_sla_start_date(self):
31043108
return self.sla_start_date
31053109
return self.date
31063110

3111+
def get_sla_configuration(self):
3112+
return self.test.engagement.product.sla_configuration
3113+
31073114
def get_sla_period(self):
3108-
sla_configuration = self.test.engagement.product.sla_configuration
3115+
sla_configuration = self.get_sla_configuration()
31093116
sla_period = getattr(sla_configuration, self.severity.lower(), None)
31103117
enforce_period = getattr(sla_configuration, str("enforce_" + self.severity.lower()), None)
31113118
return sla_period, enforce_period

unittests/test_import_reimport.py

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import datetime
2-
31
# from unittest import skip
42
import logging
3+
import zoneinfo
4+
from datetime import datetime
55
from pathlib import Path
6+
from unittest.mock import patch
67

8+
from dateutil.relativedelta import relativedelta
79
from django.test import override_settings
810
from django.test.client import Client
911
from django.urls import reverse
@@ -777,13 +779,20 @@ def test_import_0_reimport_1_active_not_verified(self):
777779
# - active findings count should be 4
778780
# - total findings count should be 5
779781
# - zap1 active, zap4 inactive
780-
def test_import_0_reimport_1_active_verified_reimport_0_active_verified(self):
782+
# - zap1 is reactivated but should not have a new sla start date and expiration date
783+
def test_import_0_reimport_1_active_verified_reimport_0_active_verified_sla_no_restart(self):
781784
logger.debug("reimporting updated zap xml report, 1 new finding and 1 no longer present, verified=True and then 0 again")
782785

783786
import0 = self.import_scan_with_params(self.zap_sample0_filename)
784787

785788
test_id = import0["test"]
786789
findings = self.get_test_findings_api(test_id)
790+
791+
for finding in findings["results"]:
792+
if "Zap1" in finding["title"]:
793+
zap1_initial_sla_start_date = finding["sla_start_date"]
794+
zap1_initial_sla_expiration_date = finding["sla_expiration_date"]
795+
787796
self.log_finding_summary_json_api(findings)
788797

789798
self.db_finding_count()
@@ -820,6 +829,8 @@ def test_import_0_reimport_1_active_verified_reimport_0_active_verified(self):
820829
for finding in findings["results"]:
821830
if "Zap1" in finding["title"]:
822831
self.assertTrue(finding["active"])
832+
self.assertEqual(finding["sla_start_date"], zap1_initial_sla_start_date)
833+
self.assertEqual(finding["sla_expiration_date"], zap1_initial_sla_expiration_date)
823834
zap1_ok = True
824835
if "Zap4" in finding["title"]:
825836
self.assertFalse(finding["active"])
@@ -845,6 +856,90 @@ def test_import_0_reimport_1_active_verified_reimport_0_active_verified(self):
845856
# zap4 was created and then closed -> only 1 note
846857
self.assertEqual(notes_count_before + 2 + 1, self.db_notes_count())
847858

859+
# import 0 and then reimport 1 with zap4 as extra finding, zap1 closed and then reimport 0 again
860+
# - active findings count should be 4
861+
# - total findings count should be 5
862+
# - zap1 active, zap4 inactive
863+
# - zap1 is reactivated and should have a new sla start date and expiration date since we enabled that flag in the test case
864+
@patch("django.utils.timezone.now")
865+
def test_import_0_reimport_1_active_verified_reimport_0_active_verified_sla_restart(self, mock_now):
866+
fake_now = datetime(2025, 7, 1, tzinfo=zoneinfo.ZoneInfo("UTC"))
867+
mock_now.return_value = fake_now
868+
logger.debug("reimporting updated zap xml report, 1 new finding and 1 no longer present, verified=True and then 0 again")
869+
870+
import0 = self.import_scan_with_params(self.zap_sample0_filename)
871+
872+
test_id = import0["test"]
873+
findings = self.get_test_findings_api(test_id)
874+
875+
for finding in findings["results"]:
876+
if "Zap1" in finding["title"]:
877+
finding["sla_start_date"]
878+
finding["sla_expiration_date"]
879+
zap1 = Finding.objects.get(id=finding["id"])
880+
sla_configuration = zap1.get_sla_configuration()
881+
sla_configuration.restart_sla_on_reactivation = True
882+
sla_configuration.save()
883+
884+
self.log_finding_summary_json_api(findings)
885+
886+
self.db_finding_count()
887+
endpoint_count_before = self.db_endpoint_count()
888+
endpoint_status_count_before_active = self.db_endpoint_status_count(mitigated=False)
889+
endpoint_status_count_before_mitigated = self.db_endpoint_status_count(mitigated=True)
890+
notes_count_before = self.db_notes_count()
891+
892+
reimport1 = self.reimport_scan_with_params(test_id, self.zap_sample1_filename)
893+
894+
# zap1 should be closed 2 endpoint statuses less, but 2 extra for zap4
895+
self.assertEqual(endpoint_status_count_before_active - 3 + 2, self.db_endpoint_status_count(mitigated=False))
896+
self.assertEqual(endpoint_status_count_before_mitigated + 2, self.db_endpoint_status_count(mitigated=True))
897+
898+
endpoint_status_count_before_active = self.db_endpoint_status_count(mitigated=False)
899+
endpoint_status_count_before_mitigated = self.db_endpoint_status_count(mitigated=True)
900+
901+
with assertTestImportModelsCreated(self, reimports=1, affected_findings=2, closed=1, reactivated=1, untouched=3):
902+
self.reimport_scan_with_params(test_id, self.zap_sample0_filename)
903+
904+
test_id = reimport1["test"]
905+
self.assertEqual(test_id, test_id)
906+
907+
self.get_test_api(test_id)
908+
findings = self.get_test_findings_api(test_id)
909+
self.log_finding_summary_json_api(findings)
910+
911+
# active findings must be equal to those in both reports
912+
findings = self.get_test_findings_api(test_id)
913+
self.assert_finding_count_json(4 + 1, findings)
914+
915+
for finding in findings["results"]:
916+
if "Zap1" in finding["title"]:
917+
self.assertTrue(finding["active"])
918+
self.assertEqual(finding["sla_start_date"], fake_now.date().isoformat())
919+
sla_days = finding["severity"].lower()
920+
sla_config = Finding.objects.get(id=finding["id"]).test.engagement.product.sla_configuration
921+
sla_expiration_date = fake_now.date() + relativedelta(days=getattr(sla_config, sla_days))
922+
self.assertEqual(finding["sla_expiration_date"], sla_expiration_date.isoformat())
923+
else:
924+
self.assertIsNone(finding["sla_start_date"])
925+
926+
# verified findings must be equal to those in report 0
927+
findings = self.get_test_findings_api(test_id, verified=True)
928+
self.assert_finding_count_json(0, findings)
929+
930+
findings = self.get_test_findings_api(test_id, verified=False)
931+
self.assert_finding_count_json(5, findings)
932+
933+
self.assertEqual(endpoint_count_before, self.db_endpoint_count())
934+
935+
# zap4 should be closed again so 2 mitigated eps, zap1 should be open again so 3 active extra
936+
self.assertEqual(endpoint_status_count_before_active + 3 - 2, self.db_endpoint_status_count(mitigated=False))
937+
self.assertEqual(endpoint_status_count_before_mitigated - 3 + 2, self.db_endpoint_status_count(mitigated=True))
938+
939+
# zap1 was closed and then opened -> 2 notes
940+
# zap4 was created and then closed -> only 1 note
941+
self.assertEqual(notes_count_before + 2 + 1, self.db_notes_count())
942+
848943
# import 0 and then reimport 2 with an extra endpoint for zap1
849944
# - extra endpoint should be present in db
850945
# - reimport doesn't look at endpoints to match against existing findings
@@ -1522,8 +1617,8 @@ def test_import_reimport_vulnerability_ids(self):
15221617
engagement=test.engagement,
15231618
test_type=test_type,
15241619
scan_type=self.anchore_grype_scan_type,
1525-
target_start=datetime.datetime.now(datetime.UTC),
1526-
target_end=datetime.datetime.now(datetime.UTC),
1620+
target_start=datetime.now(timezone.get_current_timezone()),
1621+
target_end=datetime.now(timezone.get_current_timezone()),
15271622
)
15281623
reimport_test.save()
15291624

0 commit comments

Comments
 (0)