From 1b86b668409227c5f73a97e853aa6a8c53b75ee5 Mon Sep 17 00:00:00 2001 From: Blake Owens <76979297+blakeaowens@users.noreply.github.com> Date: Tue, 14 May 2024 12:04:46 -0500 Subject: [PATCH] Optionally Enforce SLA Remediation Days (#10179) * add ability to toggle whether SLA days are enforced per severity * revert changes * add changes back * update view product details * ruff fix * add unit test * fix serializer old vs new comparison --- dojo/api_v2/serializers.py | 4 +- ...configuration_enforce_critical_and_more.py | 53 ++++++++++++++ dojo/forms.py | 6 +- dojo/models.py | 73 ++++++++++++++----- dojo/templates/dojo/sla_config.html | 8 +- dojo/templates/dojo/view_product_details.html | 32 +++++++- dojo/templatetags/display_tags.py | 5 +- unittests/test_finding_model.py | 33 +++++++++ 8 files changed, 184 insertions(+), 30 deletions(-) create mode 100644 dojo/db_migrations/0212_sla_configuration_enforce_critical_and_more.py diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index 74ffe721c3f..2007190de82 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -2957,10 +2957,10 @@ class Meta: def validate(self, data): async_updating = getattr(self.instance, 'async_updating', None) if async_updating: - for field in ['critical', 'high', 'medium', 'low']: + for field in ['critical', 'enforce_critical', 'high', 'enforce_high', 'medium', 'enforce_medium', 'low', 'enforce_low']: old_days = getattr(self.instance, field, None) new_days = data.get(field, None) - if old_days and new_days and (old_days != new_days): + if old_days is not None and new_days is not None and (old_days != new_days): msg = 'Finding SLA expiration dates are currently being calculated. The SLA days for this SLA configuration cannot be changed until the calculation is complete.' raise serializers.ValidationError(msg) return data diff --git a/dojo/db_migrations/0212_sla_configuration_enforce_critical_and_more.py b/dojo/db_migrations/0212_sla_configuration_enforce_critical_and_more.py new file mode 100644 index 00000000000..7b473bde7dd --- /dev/null +++ b/dojo/db_migrations/0212_sla_configuration_enforce_critical_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 4.1.13 on 2024-05-09 08:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dojo', '0211_system_settings_enable_similar_findings'), + ] + + operations = [ + migrations.AddField( + model_name='sla_configuration', + name='enforce_critical', + field=models.BooleanField(default=True, help_text='When enabled, critical findings will be assigned an SLA expiration date based on the critical finding SLA days within this SLA configuration.', verbose_name='Enforce Critical Finding SLA Days'), + ), + migrations.AddField( + model_name='sla_configuration', + name='enforce_high', + field=models.BooleanField(default=True, help_text='When enabled, high findings will be assigned an SLA expiration date based on the high finding SLA days within this SLA configuration.', verbose_name='Enforce High Finding SLA Days'), + ), + migrations.AddField( + model_name='sla_configuration', + name='enforce_low', + field=models.BooleanField(default=True, help_text='When enabled, low findings will be assigned an SLA expiration date based on the low finding SLA days within this SLA configuration.', verbose_name='Enforce Low Finding SLA Days'), + ), + migrations.AddField( + model_name='sla_configuration', + name='enforce_medium', + field=models.BooleanField(default=True, help_text='When enabled, medium findings will be assigned an SLA expiration date based on the medium finding SLA days within this SLA configuration.', verbose_name='Enforce Medium Finding SLA Days'), + ), + migrations.AlterField( + model_name='sla_configuration', + name='critical', + field=models.IntegerField(default=7, help_text='The number of days to remediate a critical finding.', verbose_name='Critical Finding SLA Days'), + ), + migrations.AlterField( + model_name='sla_configuration', + name='high', + field=models.IntegerField(default=30, help_text='The number of days to remediate a high finding.', verbose_name='High Finding SLA Days'), + ), + migrations.AlterField( + model_name='sla_configuration', + name='low', + field=models.IntegerField(default=120, help_text='The number of days to remediate a low finding.', verbose_name='Low Finding SLA Days'), + ), + migrations.AlterField( + model_name='sla_configuration', + name='medium', + field=models.IntegerField(default=90, help_text='The number of days to remediate a medium finding.', verbose_name='Medium Finding SLA Days'), + ), + ] diff --git a/dojo/forms.py b/dojo/forms.py index 09b8c33949b..9d919558478 100644 --- a/dojo/forms.py +++ b/dojo/forms.py @@ -2596,17 +2596,21 @@ def __init__(self, *args, **kwargs): msg = 'Finding SLA expiration dates are currently being recalculated. ' + \ 'This field cannot be changed until the calculation is complete.' self.fields['critical'].disabled = True + self.fields['enforce_critical'].disabled = True self.fields['critical'].widget.attrs['message'] = msg self.fields['high'].disabled = True + self.fields['enforce_high'].disabled = True self.fields['high'].widget.attrs['message'] = msg self.fields['medium'].disabled = True + self.fields['enforce_medium'].disabled = True self.fields['medium'].widget.attrs['message'] = msg self.fields['low'].disabled = True + self.fields['enforce_low'].disabled = True self.fields['low'].widget.attrs['message'] = msg class Meta: model = SLA_Configuration - fields = ['name', 'description', 'critical', 'high', 'medium', 'low'] + fields = ['name', 'description', 'critical', 'enforce_critical', 'high', 'enforce_high', 'medium', 'enforce_medium', 'low', 'enforce_low'] class DeleteSLAConfigForm(forms.ModelForm): diff --git a/dojo/models.py b/dojo/models.py index 6e7c1365786..53149c8389e 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -877,17 +877,45 @@ def clean(self): class SLA_Configuration(models.Model): name = models.CharField(max_length=128, unique=True, blank=False, verbose_name=_('Custom SLA Name'), help_text=_('A unique name for the set of SLAs.')) - description = models.CharField(max_length=512, null=True, blank=True) - critical = models.IntegerField(default=7, verbose_name=_('Critical Finding SLA Days'), - help_text=_('number of days to remediate a critical finding.')) - high = models.IntegerField(default=30, verbose_name=_('High Finding SLA Days'), - help_text=_('number of days to remediate a high finding.')) - medium = models.IntegerField(default=90, verbose_name=_('Medium Finding SLA Days'), - help_text=_('number of days to remediate a medium finding.')) - low = models.IntegerField(default=120, verbose_name=_('Low Finding SLA Days'), - help_text=_('number of days to remediate a low finding.')) - async_updating = models.BooleanField(default=False, - help_text=_('Findings under this SLA configuration are asynchronously being updated')) + description = models.CharField( + max_length=512, + null=True, + blank=True) + critical = models.IntegerField( + default=7, + verbose_name=_('Critical Finding SLA Days'), + help_text=_('The number of days to remediate a critical finding.')) + enforce_critical = models.BooleanField( + default=True, + verbose_name=_('Enforce Critical Finding SLA Days'), + help_text=_('When enabled, critical findings will be assigned an SLA expiration date based on the critical finding SLA days within this SLA configuration.')) + high = models.IntegerField( + default=30, + verbose_name=_('High Finding SLA Days'), + help_text=_('The number of days to remediate a high finding.')) + enforce_high = models.BooleanField( + default=True, + verbose_name=_('Enforce High Finding SLA Days'), + help_text=_('When enabled, high findings will be assigned an SLA expiration date based on the high finding SLA days within this SLA configuration.')) + medium = models.IntegerField( + default=90, + verbose_name=_('Medium Finding SLA Days'), + help_text=_('The number of days to remediate a medium finding.')) + enforce_medium = models.BooleanField( + default=True, + verbose_name=_('Enforce Medium Finding SLA Days'), + help_text=_('When enabled, medium findings will be assigned an SLA expiration date based on the medium finding SLA days within this SLA configuration.')) + low = models.IntegerField( + default=120, + verbose_name=_('Low Finding SLA Days'), + help_text=_('The number of days to remediate a low finding.')) + enforce_low = models.BooleanField( + default=True, + verbose_name=_('Enforce Low Finding SLA Days'), + help_text=_('When enabled, low findings will be assigned an SLA expiration date based on the low finding SLA days within this SLA configuration.')) + async_updating = models.BooleanField( + default=False, + help_text=_('Findings under this SLA configuration are asynchronously being updated')) class Meta: ordering = ['name'] @@ -903,9 +931,13 @@ def save(self, *args, **kwargs): # if initial config exists and async finding update is already running, revert sla config before saving if initial_sla_config and self.async_updating: self.critical = initial_sla_config.critical + self.enforce_critical = initial_sla_config.enforce_critical self.high = initial_sla_config.high + self.enforce_high = initial_sla_config.enforce_high self.medium = initial_sla_config.medium + self.enforce_medium = initial_sla_config.enforce_medium self.low = initial_sla_config.low + self.enforce_low = initial_sla_config.enforce_low super().save(*args, **kwargs) @@ -913,13 +945,13 @@ def save(self, *args, **kwargs): if initial_sla_config is not None and not self.async_updating: # check which sla days fields changed based on severity severities = [] - if initial_sla_config.critical != self.critical: + if (initial_sla_config.critical != self.critical) or (initial_sla_config.enforce_critical != self.enforce_critical): severities.append('Critical') - if initial_sla_config.high != self.high: + if (initial_sla_config.high != self.high) or (initial_sla_config.enforce_high != self.enforce_high): severities.append('High') - if initial_sla_config.medium != self.medium: + if (initial_sla_config.medium != self.medium) or (initial_sla_config.enforce_medium != self.enforce_medium): severities.append('Medium') - if initial_sla_config.low != self.low: + if (initial_sla_config.low != self.low) or (initial_sla_config.enforce_low != self.enforce_low): severities.append('Low') # if severities have changed, update finding sla expiration dates with those severities if len(severities): @@ -2963,7 +2995,9 @@ def get_sla_start_date(self): def get_sla_period(self): sla_configuration = SLA_Configuration.objects.filter(id=self.test.engagement.product.sla_configuration_id).first() - return getattr(sla_configuration, self.severity.lower(), None) + sla_period = getattr(sla_configuration, self.severity.lower(), None) + enforce_period = getattr(sla_configuration, str('enforce_' + self.severity.lower()), None) + return sla_period, enforce_period def set_sla_expiration_date(self): system_settings = System_Settings.objects.get() @@ -2971,9 +3005,12 @@ def set_sla_expiration_date(self): return None days_remaining = None - sla_period = self.get_sla_period() - if sla_period: + sla_period, enforce_period = self.get_sla_period() + if sla_period is not None and enforce_period: days_remaining = sla_period - self.sla_age + else: + self.sla_expiration_date = Finding().sla_expiration_date + return None if days_remaining: if self.mitigated: diff --git a/dojo/templates/dojo/sla_config.html b/dojo/templates/dojo/sla_config.html index c2b1a7debfd..91c8cf530e0 100644 --- a/dojo/templates/dojo/sla_config.html +++ b/dojo/templates/dojo/sla_config.html @@ -61,16 +61,16 @@

{% if conf.description %}{{ conf.description }}{% endif %} - {% if conf.critical %}{{ conf.critical }}{% endif %} + {% if conf.critical and conf.enforce_critical %}{{ conf.critical }}{% endif %} - {% if conf.high %}{{ conf.high }}{% endif %} + {% if conf.high and conf.enforce_high %}{{ conf.high }}{% endif %} - {% if conf.medium %}{{ conf.medium }}{% endif %} + {% if conf.medium and conf.enforce_medium %}{{ conf.medium }}{% endif %} - {% if conf.low %}{{ conf.low }}{% endif %} + {% if conf.low and conf.enforce_low %}{{ conf.low }}{% endif %} {% endfor %} diff --git a/dojo/templates/dojo/view_product_details.html b/dojo/templates/dojo/view_product_details.html index cc216a79d99..e5061e3527a 100644 --- a/dojo/templates/dojo/view_product_details.html +++ b/dojo/templates/dojo/view_product_details.html @@ -505,19 +505,43 @@

Critical - {{ sla.critical }} days to remediate + + {% if sla.enforce_critical %} + {{ sla.critical }} days to remediate + {% else %} + Not Enforced + {% endif %} + High - {{ sla.high }} days to remediate + + {% if sla.enforce_high %} + {{ sla.high }} days to remediate + {% else %} + Not Enforced + {% endif %} + Medium - {{ sla.medium }} days to remediate + + {% if sla.enforce_medium %} + {{ sla.medium }} days to remediate + {% else %} + Not Enforced + {% endif %} + Low - {{ sla.low }} days to remediate + + {% if sla.enforce_low %} + {{ sla.low }} days to remediate + {% else %} + Not Enforced + {% endif %} + diff --git a/dojo/templatetags/display_tags.py b/dojo/templatetags/display_tags.py index 1b45c935115..ed224d1b70b 100644 --- a/dojo/templatetags/display_tags.py +++ b/dojo/templatetags/display_tags.py @@ -269,10 +269,13 @@ def finding_sla(finding): if not get_system_setting('enable_finding_sla'): return "" + sla_age, enforce_sla = finding.get_sla_period() + if not enforce_sla: + return "" + title = "" severity = finding.severity find_sla = finding.sla_days_remaining() - sla_age = finding.get_sla_period() if finding.mitigated: status = "blue" status_text = 'Remediated within SLA for ' + severity.lower() + ' findings (' + str(sla_age) + ' days since ' + finding.get_sla_start_date().strftime("%b %d, %Y") + ')' diff --git a/unittests/test_finding_model.py b/unittests/test_finding_model.py index 9ab69365042..1a2fb4e2a2a 100644 --- a/unittests/test_finding_model.py +++ b/unittests/test_finding_model.py @@ -458,3 +458,36 @@ def test_sla_expiration_date_after_sla_configuration_updated(self): expected_sla_days = getattr(product.sla_configuration, finding.severity.lower(), None) self.assertEqual(finding.sla_expiration_date, datetime.now().date() + timedelta(days=expected_sla_days)) self.assertEqual(finding.sla_days_remaining(), expected_sla_days) + + def test_sla_expiration_date_after_sla_not_enforced(self): + """ + tests if the SLA expiration date is none after the after the SLA configuration on a + product was updated to not enforce all SLA remediation days + """ + user, _ = User.objects.get_or_create(username='admin') + product_type = self.create_product_type('test_product_type') + sla_config = self.create_sla_configuration(name='test_sla_config') + product = self.create_product(name='test_product', prod_type=product_type) + product.sla_configuration = sla_config + product.save() + engagement = self.create_engagement('test_eng', product) + test = self.create_test(engagement=engagement, scan_type='ZAP Scan', title='test_test') + finding = Finding.objects.create( + test=test, + reporter=user, + title='test_finding', + severity='Critical', + date=datetime.now().date()) + + expected_sla_days = getattr(product.sla_configuration, finding.severity.lower(), None) + self.assertEqual(finding.sla_expiration_date, datetime.now().date() + timedelta(days=expected_sla_days)) + self.assertEqual(finding.sla_days_remaining(), expected_sla_days) + + sla_config.enforce_critical = False + sla_config.save() + + finding.set_sla_expiration_date() + + self.assertEqual(finding.sla_expiration_date, None) + self.assertEqual(finding.sla_days_remaining(), None) + self.assertEqual(finding.sla_deadline(), None)