Skip to content

Per-Project SLA Config #6413

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
Aug 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5cce1de
manually rebased on upstream/dev
37b Jun 8, 2022
0426e47
rebased and cleaned up
37b Jun 14, 2022
8a93eea
Merge branch 'dev' of https://github.com/DefectDojo/django-DefectDojo…
37b Jun 14, 2022
273723a
updated jira test product data to include sla_configuration
37b Jun 15, 2022
5a0526d
accessibility fix
37b Jun 15, 2022
5517e07
pep8 fixes
37b Jun 15, 2022
122ed28
merged latest from dev
37b Jun 28, 2022
bed75cc
merged latest with dev and fixed some tests
37b Jun 28, 2022
14eb5b4
cleaned up imports
37b Jun 28, 2022
782277b
manually rebased on upstream/dev
37b Jun 8, 2022
a626e43
rebased and cleaned up
37b Jun 14, 2022
df325da
pep8 fixes
37b Jun 15, 2022
63121e2
merged latest with dev and fixed some tests
37b Jun 28, 2022
c377386
cleaned up imports
37b Jun 28, 2022
470eefd
rebase changes
37b Jul 6, 2022
fa7f6c0
bug fix
37b Jul 6, 2022
e9af554
bug fix for default SLA configuration
37b Jul 6, 2022
ef4314c
another bug fix for new products
37b Jul 6, 2022
a59ef19
Merge branch 'dev' into sla_fix
37b Jul 14, 2022
4ac168f
permission fixes
37b Jul 27, 2022
5f34f31
testing migration
37b Jul 27, 2022
3f5399d
test
37b Jul 28, 2022
e7bd53b
Merge branch 'dev' into sla_fix
37b Jul 28, 2022
c334038
testing separate migration files
37b Jul 28, 2022
bddd592
testing
37b Jul 28, 2022
cfe8af9
testing
37b Jul 28, 2022
bf03e3b
testing
37b Jul 28, 2022
04826b6
testing
37b Jul 28, 2022
2dd97e8
testing
37b Jul 28, 2022
bc5c5af
migrate existing SLA config in System Settings to Default entry
37b Jul 28, 2022
12aef52
pep8 fixes
37b Jul 28, 2022
4c73b9d
pep8 fixes
37b Jul 28, 2022
a8f56c2
removed platform specifier from docker-compose files
37b Jul 29, 2022
1ac9305
permission fixes
37b Aug 1, 2022
28e8ed5
pep8 fix
37b Aug 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion dojo/api_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from dojo.endpoint.utils import endpoint_filter
from dojo.importers.reimporter.utils import get_or_create_engagement, get_target_engagement_if_exists, get_target_product_by_id_if_exists, \
get_target_product_if_exists, get_target_test_if_exists
from dojo.models import IMPORT_ACTIONS, SEVERITIES, STATS_FIELDS, Dojo_User, Finding_Group, Product, Engagement, Test, Finding, \
from dojo.models import IMPORT_ACTIONS, SEVERITIES, SLA_Configuration, STATS_FIELDS, Dojo_User, Finding_Group, Product, \
Engagement, Test, Finding, \
User, Stub_Finding, Risk_Acceptance, \
Finding_Template, Test_Type, Development_Environment, NoteHistory, \
JIRA_Issue, Tool_Product_Settings, Tool_Configuration, Tool_Type, \
Expand Down Expand Up @@ -2031,6 +2032,12 @@ class Meta:
fields = '__all__'


class SLAConfigurationSerializer(serializers.ModelSerializer):
class Meta:
model = SLA_Configuration
fields = '__all__'


class UserProfileSerializer(serializers.Serializer):
user = UserSerializer(many=False)
user_contact_info = UserContactInfoSerializer(many=False)
Expand Down
15 changes: 14 additions & 1 deletion dojo/api_v2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
import base64
from dojo.engagement.services import close_engagement, reopen_engagement
from dojo.importers.reimporter.utils import get_target_engagement_if_exists, get_target_product_if_exists, get_target_test_if_exists
from dojo.models import Language_Type, Languages, Notifications, Product, Product_Type, Engagement, Test, Test_Import, Test_Type, Finding, \
from dojo.models import Language_Type, Languages, Notifications, Product, Product_Type, Engagement, SLA_Configuration, \
Test, Test_Import, Test_Type, Finding, \
User, Stub_Finding, Finding_Template, Notes, \
JIRA_Issue, Tool_Product_Settings, Tool_Configuration, Tool_Type, \
Endpoint, JIRA_Project, JIRA_Instance, DojoMeta, Development_Environment, \
Expand Down Expand Up @@ -2555,3 +2556,15 @@ class ConfigurationPermissionViewSet(mixins.RetrieveModelMixin,
filter_backends = (DjangoFilterBackend,)
filter_fields = ('id', 'name', 'codename')
permission_classes = (permissions.IsSuperUser, DjangoModelPermissions)


class SLAConfigurationViewset(mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.CreateModelMixin,
viewsets.GenericViewSet):
serializer_class = serializers.SLAConfigurationSerializer
queryset = SLA_Configuration.objects.all()
filter_backends = (DjangoFilterBackend,)
permission_classes = (IsAuthenticated, DjangoModelPermissions)
59 changes: 59 additions & 0 deletions dojo/db_migrations/0165_custom_sla.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Generated by Django 3.2.13 on 2022-05-28 20:06
import logging

from django.db import migrations, models

logger = logging.getLogger(__name__)


# def save_existing_sla(apps, schema_editor):
# system_settings_model = apps.get_model('dojo', 'System_Settings')
#
# try:
# system_settings = system_settings_model.objects.get()
# critical = system_settings.sla_critical,
# high = system_settings.sla_high,
# medium = system_settings.sla_medium,
# low = system_settings.sla_low
# except:
# critical = 7
# high = 30
# medium = 90
# low = 120
#
# SLA_Configuration = apps.get_model('dojo', 'SLA_Configuration')
# SLA_Configuration.objects.create(name='Default',
# description='The Default SLA Configuration. Products not using an explicit SLA Configuration will use this one.',
# critical=critical,
# high=high,
# medium=medium,
# low=low)


class Migration(migrations.Migration):
dependencies = [
('dojo', '0164_remove_system_settings_staff_user_email_pattern'),
]

operations = [
migrations.CreateModel(
name='SLA_Configuration',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='A unique name for the set of SLAs.', max_length=128, unique=True,
verbose_name='Custom SLA Name')),
('description', models.CharField(blank=True, max_length=512, null=True)),
('critical', models.IntegerField(default=7, help_text='number of days to remediate a critical finding.',
verbose_name='Critical Finding SLA Days')),
('high', models.IntegerField(default=30, help_text='number of days to remediate a high finding.',
verbose_name='High Finding SLA Days')),
('medium', models.IntegerField(default=90, help_text='number of days to remediate a medium finding.',
verbose_name='Medium Finding SLA Days')),
('low', models.IntegerField(default=120, help_text='number of days to remediate a low finding.',
verbose_name='Low Finding SLA Days')),
],
options={
'ordering': ['name'],
},
)
]
65 changes: 65 additions & 0 deletions dojo/db_migrations/0166_copy_sla_from_system_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Generated by Django 3.2.14 on 2022-07-28 13:11
import logging

import django.db.models.deletion

from django.db import migrations, models

logger = logging.getLogger(__name__)


def save_existing_sla(apps, schema_editor):
system_settings_model = apps.get_model('dojo', 'System_Settings')

try:
system_settings = system_settings_model.objects.get()
critical = system_settings.sla_critical
high = system_settings.sla_high
medium = system_settings.sla_medium
low = system_settings.sla_low

except:
critical = 7
high = 30
medium = 90
low = 120

sla_config = apps.get_model('dojo', 'SLA_Configuration')
sla_config.objects.create(name='Default',
description='The Default SLA Configuration. Products not using an explicit SLA Configuration will use this one.',
critical=critical,
high=high,
medium=medium,
low=low)


class Migration(migrations.Migration):
dependencies = [
('dojo', '0165_custom_sla'),
]

operations = [
migrations.RunPython(save_existing_sla),
migrations.RemoveField(
model_name='system_settings',
name='sla_critical',
),
migrations.RemoveField(
model_name='system_settings',
name='sla_high',
),
migrations.RemoveField(
model_name='system_settings',
name='sla_low',
),
migrations.RemoveField(
model_name='system_settings',
name='sla_medium',
),
migrations.AddField(
model_name='product',
name='sla_configuration',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.RESTRICT, related_name='sla_config',
to='dojo.sla_configuration'),
),
]
4 changes: 0 additions & 4 deletions dojo/fixtures/defect_dojo_sample_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -7117,10 +7117,6 @@
"engagement_auto_close": false,
"engagement_auto_close_days": 3,
"enable_finding_sla": true,
"sla_critical": 7,
"sla_high": 30,
"sla_medium": 90,
"sla_low": 120,
"allow_anonymous_survey_repsonse": false,
"credentials": "",
"disclaimer": "",
Expand Down
24 changes: 22 additions & 2 deletions dojo/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from dojo.endpoint.utils import endpoint_get_or_create, endpoint_filter, \
validate_endpoints_to_add
from dojo.models import Finding, Finding_Group, Product_Type, Product, Note_Type, \
Check_List, User, Engagement, Test, Test_Type, Notes, Risk_Acceptance, \
Check_List, SLA_Configuration, User, Engagement, Test, Test_Type, Notes, Risk_Acceptance, \
Development_Environment, Dojo_User, Endpoint, Stub_Finding, Finding_Template, \
JIRA_Issue, JIRA_Project, JIRA_Instance, GITHUB_Issue, GITHUB_PKey, GITHUB_Conf, UserContactInfo, Tool_Type, \
Tool_Configuration, Tool_Product_Settings, Cred_User, Cred_Mapping, System_Settings, Notifications, \
Expand Down Expand Up @@ -247,6 +247,11 @@ class ProductForm(forms.ModelForm):
queryset=Product_Type.objects.none(),
required=True)

sla_configuration = forms.ModelChoiceField(label='SLA Configuration',
queryset=SLA_Configuration.objects.all(),
required=True,
initial='Default')

Comment on lines +250 to +254
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This a good place for this, but I fear product maintainers could swap an SLA conf to something a bit more forgiving (thinking the worst of people here)

Possible solution would be to not display this field unless user has owner role (similar to how jira options are toggled based on enablement)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would the value be set during creation? I'm still learning Django's idioms...

product_manager = forms.ModelChoiceField(queryset=Dojo_User.objects.exclude(is_active=False).order_by('first_name', 'last_name'), required=False)
technical_contact = forms.ModelChoiceField(queryset=Dojo_User.objects.exclude(is_active=False).order_by('first_name', 'last_name'), required=False)
team_manager = forms.ModelChoiceField(queryset=Dojo_User.objects.exclude(is_active=False).order_by('first_name', 'last_name'), required=False)
Expand All @@ -257,7 +262,7 @@ def __init__(self, *args, **kwargs):

class Meta:
model = Product
fields = ['name', 'description', 'tags', 'product_manager', 'technical_contact', 'team_manager', 'prod_type', 'regulations',
fields = ['name', 'description', 'tags', 'product_manager', 'technical_contact', 'team_manager', 'prod_type', 'sla_configuration', 'regulations',
'business_criticality', 'platform', 'lifecycle', 'origin', 'user_records', 'revenue', 'external_audience',
'internet_accessible', 'enable_simple_risk_acceptance', 'enable_full_risk_acceptance']

Expand Down Expand Up @@ -2302,6 +2307,21 @@ def clean(self):
return form_data


class SLAConfigForm(forms.ModelForm):
class Meta:
model = SLA_Configuration
fields = ['name', 'description', 'critical', 'high', 'medium', 'low']


class DeleteSLAConfigForm(forms.ModelForm):
id = forms.IntegerField(required=True,
widget=forms.widgets.HiddenInput())

class Meta:
model = SLA_Configuration
fields = ['id']


class DeleteObjectsSettingsForm(forms.ModelForm):
id = forms.IntegerField(required=True,
widget=forms.widgets.HiddenInput())
Expand Down
73 changes: 55 additions & 18 deletions dojo/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,20 +391,6 @@ class System_Settings(models.Model):
verbose_name=_("Enable Finding SLA's"),
help_text=_("Enables Finding SLA's for time to remediate."))

sla_critical = models.IntegerField(default=7,
verbose_name=_('Critical Finding SLA Days'),
help_text=_('# of days to remediate a critical finding.'))

sla_high = models.IntegerField(default=30,
verbose_name=_('High Finding SLA Days'),
help_text=_('# of days to remediate a high finding.'))
sla_medium = models.IntegerField(default=90,
verbose_name=_('Medium Finding SLA Days'),
help_text=_('# of days to remediate a medium finding.'))

sla_low = models.IntegerField(default=120,
verbose_name=_('Low Finding SLA Days'),
help_text=_('# of days to remediate a low finding.'))
allow_anonymous_survey_repsonse = models.BooleanField(
default=False,
blank=False,
Expand Down Expand Up @@ -768,6 +754,47 @@ class Meta:
('finding', 'name'))


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.'))

def clean(self):

sla_days = [self.critical, self.high, self.medium, self.low]

for sla_day in sla_days:
if sla_day < 1:
raise ValidationError('SLA Days must be at least 1')

def __str__(self):
return self.name

class Meta:
ordering = ['name']

def delete(self, *args, **kwargs):
logger.debug('%d sla configuration delete', self.id)

if self.id != 1:
super().delete(*args, **kwargs)
else:
raise ValidationError("Unable to delete default SLA Configuration")

def get_summary(self):
return f'{self.name} - Critical: {self.critical}, High: {self.high}, Medium: {self.medium}, Low: {self.low}'


class Product(models.Model):
WEB_PLATFORM = 'web'
IOT = 'iot'
Expand Down Expand Up @@ -835,6 +862,12 @@ class Product(models.Model):
prod_type = models.ForeignKey(Product_Type, related_name='prod_type',
null=False, blank=False, on_delete=models.CASCADE)
updated = models.DateTimeField(auto_now=True, null=True)
sla_configuration = models.ForeignKey(SLA_Configuration,
related_name='sla_config',
null=False,
blank=False,
default=1,
on_delete=models.RESTRICT)
tid = models.IntegerField(default=0, editable=False)
members = models.ManyToManyField(Dojo_User, through='Product_Member', related_name='product_members', blank=True)
authorization_groups = models.ManyToManyField(Dojo_Group, through='Product_Group', related_name='product_groups', blank=True)
Expand Down Expand Up @@ -2365,7 +2398,7 @@ def get_vulnerability_ids(self):
# (This sometimes reports "None")
def get_endpoints(self):
endpoint_str = ''
if(self.id is None):
if (self.id is None):
if len(self.unsaved_endpoints) > 0:
deduplicationLogger.debug("get_endpoints before the finding was saved")
# convert list of unsaved endpoints to the list of their canonical representation
Expand Down Expand Up @@ -2516,6 +2549,10 @@ def _age(self, start_date):
def age(self):
return self._age(self.date)

def get_sla_periods(self):
sla_configuration = SLA_Configuration.objects.filter(id=self.test.engagement.product.sla_configuration_id).first()
return sla_configuration

def get_sla_start_date(self):
if self.sla_start_date:
return self.sla_start_date
Expand All @@ -2528,9 +2565,8 @@ def sla_age(self):

def sla_days_remaining(self):
sla_calculation = None
severity = self.severity
from dojo.utils import get_system_setting
sla_age = get_system_setting('sla_' + self.severity.lower())
sla_periods = self.get_sla_periods()
sla_age = getattr(sla_periods, self.severity.lower(), None)
if sla_age:
sla_calculation = sla_age - self.sla_age
return sla_calculation
Expand Down Expand Up @@ -4079,6 +4115,7 @@ def enable_disable_auditlog(enable=True):
admin.site.register(Cred_User)
admin.site.register(Cred_Mapping)
admin.site.register(System_Settings, System_SettingsAdmin)
admin.site.register(SLA_Configuration)
admin.site.register(CWE)
admin.site.register(Regulation)
admin.site.register(Global_Role)
Expand Down
12 changes: 7 additions & 5 deletions dojo/product/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
EngagementPresetsForm, DeleteEngagementPresetsForm, ProductNotificationsForm, \
GITHUB_Product_Form, GITHUBFindingForm, AppAnalysisForm, JIRAEngagementForm, Add_Product_MemberForm, \
Edit_Product_MemberForm, Delete_Product_MemberForm, Add_Product_GroupForm, Edit_Product_Group_Form, \
Delete_Product_GroupForm, \
Delete_Product_GroupForm, SLA_Configuration, \
DeleteAppAnalysisForm, Product_API_Scan_ConfigurationForm, DeleteProduct_API_Scan_ConfigurationForm
from dojo.models import Product_Type, Note_Type, Finding, Product, Engagement, Test, GITHUB_PKey, \
Test_Type, System_Settings, Languages, App_Analysis, Benchmark_Type, Benchmark_Product_Summary, Endpoint_Status, \
Expand Down Expand Up @@ -142,9 +142,9 @@ def iso_to_gregorian(iso_year, iso_week, iso_day):

@user_is_authorized(Product, Permissions.Product_View, 'pid')
def view_product(request, pid):
prod_query = Product.objects.all().select_related('product_manager', 'technical_contact', 'team_manager') \
.prefetch_related('members') \
.prefetch_related('prod_type__members')
prod_query = Product.objects.all().select_related('product_manager', 'technical_contact', 'team_manager', 'sla_configuration') \
.prefetch_related('members') \
.prefetch_related('prod_type__members')
prod = get_object_or_404(prod_query, id=pid)
product_members = get_authorized_members_for_product(prod, Permissions.Product_View)
product_type_members = get_authorized_members_for_product_type(prod.prod_type, Permissions.Product_Type_View)
Expand All @@ -158,6 +158,7 @@ def view_product(request, pid):
benchmark_type = Benchmark_Type.objects.filter(enabled=True).order_by('name')
benchmarks = Benchmark_Product_Summary.objects.filter(product=prod, publish=True,
benchmark_type__enabled=True).order_by('benchmark_type__name')
sla = SLA_Configuration.objects.filter(id=prod.sla_configuration_id).first()
benchAndPercent = []
for i in range(0, len(benchmarks)):
benchAndPercent.append([benchmarks[i].benchmark_type, get_level(benchmarks[i])])
Expand Down Expand Up @@ -216,7 +217,8 @@ def view_product(request, pid):
'product_groups': product_groups,
'product_type_groups': product_type_groups,
'personal_notifications_form': personal_notifications_form,
'enabled_notifications': get_enabled_notifications_list()})
'enabled_notifications': get_enabled_notifications_list(),
'sla': sla})


@user_is_authorized(Product, Permissions.Component_View, 'pid')
Expand Down
Empty file added dojo/sla_config/__init__.py
Empty file.
Loading