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 @@