Skip to content

Commit

Permalink
Optionally Enforce SLA Remediation Days (#10179)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
blakeaowens authored May 14, 2024
1 parent 800ee35 commit 1b86b66
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 30 deletions.
4 changes: 2 additions & 2 deletions dojo/api_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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'),
),
]
6 changes: 5 additions & 1 deletion dojo/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
73 changes: 55 additions & 18 deletions dojo/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand All @@ -903,23 +931,27 @@ 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)

# if the initial sla config exists and async finding update is not running
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):
Expand Down Expand Up @@ -2963,17 +2995,22 @@ 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()
if not system_settings.enable_finding_sla:
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:
Expand Down
8 changes: 4 additions & 4 deletions dojo/templates/dojo/sla_config.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,16 @@ <h3 class="has-filters">
{% if conf.description %}{{ conf.description }}{% endif %}
</td>
<td>
{% if conf.critical %}{{ conf.critical }}{% endif %}
{% if conf.critical and conf.enforce_critical %}{{ conf.critical }}{% endif %}
</td>
<td>
{% if conf.high %}{{ conf.high }}{% endif %}
{% if conf.high and conf.enforce_high %}{{ conf.high }}{% endif %}
</td>
<td>
{% if conf.medium %}{{ conf.medium }}{% endif %}
{% if conf.medium and conf.enforce_medium %}{{ conf.medium }}{% endif %}
</td>
<td>
{% if conf.low %}{{ conf.low }}{% endif %}
{% if conf.low and conf.enforce_low %}{{ conf.low }}{% endif %}
</td>
</tr>
{% endfor %}
Expand Down
32 changes: 28 additions & 4 deletions dojo/templates/dojo/view_product_details.html
Original file line number Diff line number Diff line change
Expand Up @@ -505,19 +505,43 @@ <h3 class="panel-title"><span class="fa-solid fa-calendar-check fa-fw" aria-hidd
<tbody>
<tr>
<td style="width: 160px;"><strong>Critical</strong></td>
<td>{{ sla.critical }} days to remediate</td>
<td>
{% if sla.enforce_critical %}
{{ sla.critical }} days to remediate
{% else %}
<em class="text-muted">Not Enforced</em>
{% endif %}
</td>
</tr>
<tr>
<td style="width: 160px;"><strong>High</strong></td>
<td>{{ sla.high }} days to remediate</td>
<td>
{% if sla.enforce_high %}
{{ sla.high }} days to remediate
{% else %}
<em class="text-muted">Not Enforced</em>
{% endif %}
</td>
</tr>
<tr>
<td style="width: 160px;"><strong>Medium</strong></td>
<td>{{ sla.medium }} days to remediate</td>
<td>
{% if sla.enforce_medium %}
{{ sla.medium }} days to remediate
{% else %}
<em class="text-muted">Not Enforced</em>
{% endif %}
</td>
</tr>
<tr>
<td style="width: 160px;"><strong>Low</strong></td>
<td>{{ sla.low }} days to remediate</td>
<td>
{% if sla.enforce_low %}
{{ sla.low }} days to remediate
{% else %}
<em class="text-muted">Not Enforced</em>
{% endif %}
</td>
</tr>
</tbody>
</table>
Expand Down
5 changes: 4 additions & 1 deletion dojo/templatetags/display_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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") + ')'
Expand Down
33 changes: 33 additions & 0 deletions unittests/test_finding_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit 1b86b66

Please sign in to comment.