Skip to content

Commit

Permalink
new notifications type: SLA breach combined (per product)
Browse files Browse the repository at this point in the history
This commit introduces a new type of notifications: SLA breach combined.

The main difference is that notification is produced per product.
Original SLA breach notifications are generated for each applicable
findings. This may result in hundreds of messages (e-mail, slack or
teams messages) for large products. Such alerts are hardly manageable
and in the end not of much use.

With SLA breach combined notifications being enabled a user receives a
message per product with a list of findings which breach their SLA. It
can be summarized in the following manner:

subject: <SLA breach kind> <product type> <product>

body: <product summary> <list of findings>
  • Loading branch information
pna-nca committed Sep 27, 2023
1 parent d977dd5 commit 0217ed7
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 20 deletions.
3 changes: 3 additions & 0 deletions dojo/api_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2957,6 +2957,9 @@ class NotificationsSerializer(serializers.ModelSerializer):
sla_breach = MultipleChoiceField(
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION
)
sla_breach_combined = MultipleChoiceField(
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION
)
risk_acceptance_expiration = MultipleChoiceField(
choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION
)
Expand Down
19 changes: 19 additions & 0 deletions dojo/db_migrations/0191_notifications_sla_breach_combined.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.1.10 on 2023-09-12 11:29

from django.db import migrations
import multiselectfield.db.fields


class Migration(migrations.Migration):

dependencies = [
('dojo', '0190_system_settings_experimental_fp_history'),
]

operations = [
migrations.AddField(
model_name='notifications',
name='sla_breach_combined',
field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('alert', 'alert')], default=('alert', 'alert'), help_text='Get notified of (upcoming) SLA breaches (a message per project)', max_length=24, verbose_name='SLA breach (combined)'),
),
]
3 changes: 2 additions & 1 deletion dojo/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2592,11 +2592,12 @@ def __init__(self, *args, **kwargs):
self.initial['test_added'] = ''
self.initial['scan_added'] = ''
self.initial['sla_breach'] = ''
self.initial['sla_breach_combined'] = ''
self.initial['risk_acceptance_expiration'] = ''

class Meta:
model = Notifications
fields = ['engagement_added', 'close_engagement', 'test_added', 'scan_added', 'sla_breach', 'risk_acceptance_expiration']
fields = ['engagement_added', 'close_engagement', 'test_added', 'scan_added', 'sla_breach', 'sla_breach_combined', 'risk_acceptance_expiration']


class AjaxChoiceField(forms.ChoiceField):
Expand Down
4 changes: 4 additions & 0 deletions dojo/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3763,6 +3763,9 @@ class Notifications(models.Model):
sla_breach = MultiSelectField(choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION, blank=True,
verbose_name=_('SLA breach'),
help_text=_('Get notified of (upcoming) SLA breaches'))
sla_breach_combined = MultiSelectField(choices=NOTIFICATION_CHOICES, default=DEFAULT_NOTIFICATION, blank=True,
verbose_name=_('SLA breach (combined)'),
help_text=_('Get notified of (upcoming) SLA breaches (a message per project)'))
risk_acceptance_expiration = MultiSelectField(choices=NOTIFICATION_CHOICES, default='alert', blank=True,
verbose_name=_('Risk Acceptance Expiration'),
help_text=_('Get notified of (upcoming) Risk Acceptance expiries'))
Expand Down Expand Up @@ -3805,6 +3808,7 @@ def merge_notifications_list(cls, notifications_list):
result.review_requested = merge_sets_safe(result.review_requested, notifications.review_requested)
result.other = merge_sets_safe(result.other, notifications.other)
result.sla_breach = merge_sets_safe(result.sla_breach, notifications.sla_breach)
result.sla_breach_combined = merge_sets_safe(result.sla_breach_combined, notifications.sla_breach_combined)
result.risk_acceptance_expiration = merge_sets_safe(result.risk_acceptance_expiration, notifications.risk_acceptance_expiration)

return result
Expand Down
72 changes: 72 additions & 0 deletions dojo/templates/notifications/mail/sla_breach_combined.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{% load i18n %}
{% load navigation_tags %}
{% load display_tags %}
<html>
<body>
{% autoescape on %}
<p>{% trans "Hello" %} {{ user.get_full_name }},</p>
<p>
{% trans "Product summary" %}:
<ul>
<li>{% trans "name" %}: {{ product.name }}</li>
<li>{% trans "product type" %}: {{ product.prod_type }}</li>
<li>{% trans "team manager" %}: {{ product.team_manager }}</li>
<li>{% trans "product manager" %}: {{ product.product_manager }}</li>
<li>{% trans "technical contact" %}: {{ product.technical_contact }}</li>
</ul>
</p>
<p>
{% if breach_kind == 'breached' %}
{% blocktranslate trimmed %}
These security findings have breached their SLA:
{% endblocktranslate %}
{% elif breach_kind == 'prebreach' %}
{% blocktranslate trimmed %}
These security findings are about to breach their SLA:
{% endblocktranslate %}
{% elif breach_kind == 'breaching' %}
{% blocktranslate trimmed %}
These security findings breaching their SLA today:
{% endblocktranslate %}
{% else %}
This should not happen, check 'breach_kind' and 'kind' properties value in the source code.
{% endif %}
<br />
<ul>
{% for f in findings %}
{% url 'view_finding' f.id as finding_url %}
<li>
<a href="{{ finding_url|full_url }}">"{{ f.title }}"</a> ({{ f.severity }} {% trans "severity" %}), {% trans "SLA age" %}: {{ f.sla_age }}
</li>
{% endfor %}
</ul>
<br />
{% trans "Please refer to your SLA documentation for further guidance" %}
</p>
{% trans "Kind regards" %},
</br>
{% if system_settings.team_name %}
{{ system_settings.team_name }}
{% else %}
Defect Dojo
{% endif %}
<br />
<p>
{% url 'notifications' as notification_url %}
{% trans "You can manage your notification settings here" %}: <a href="{{ notification_url|full_url }}">{{ notification_url|full_url }}</a>
</p>
{% if system_settings.disclaimer and system_settings.disclaimer.strip %}
<br />
<div style="background-color:#DADCE2; border:1px #003333; padding:.8em; ">
<span style="font-size:16pt;
font-family: 'Cambria','times new roman','garamond',serif;
color:#ff0000">{% trans "Disclaimer" %}</span>
<br />
<p style="font-size:11pt;
line-height:10pt;
font-family: 'Cambria','times roman',serif">{{ system_settings.disclaimer }}</p>
</div>
{% endif %}
{% endautoescape %}
</body>
</html>
108 changes: 89 additions & 19 deletions dojo/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from django.conf import settings
from django.core.mail import send_mail
from django.core.paginator import Paginator
from django.urls import get_resolver, reverse
from django.urls import get_resolver, reverse, get_script_prefix
from django.db.models import Q, Sum, Case, When, IntegerField, Value, Count
from django.utils import timezone
from django.utils.translation import gettext as _
Expand Down Expand Up @@ -1921,19 +1921,89 @@ def sla_compute_and_notify(*args, **kwargs):
"""
import dojo.jira_link.helper as jira_helper

def _notify(finding, title):
if not finding.test.engagement.product.disable_sla_breach_notifications:
create_notification(
event='sla_breach',
title=title,
finding=finding,
url=reverse('view_finding', args=(finding.id,)),
sla_age=sla_age
)
class NotificationEntry:
def __init__(self, finding=None, jira_issue=None, do_jira_sla_comment=False):
self.finding = finding
self.jira_issue = jira_issue
self.do_jira_sla_comment = do_jira_sla_comment

def _add_notification(finding, kind):
# jira_issue, do_jira_sla_comment are taken from the context
# kind can be one of: breached, prebreach, breaching
if finding.test.engagement.product.disable_sla_breach_notifications:
return

notification = NotificationEntry(finding=finding,
jira_issue=jira_issue,
do_jira_sla_comment=do_jira_sla_comment)

if do_jira_sla_comment:
logger.info("Creating JIRA comment to notify of SLA breach information.")
jira_helper.add_simple_jira_comment(jira_instance, jira_issue, title)
pt = finding.test.engagement.product.prod_type.name
p = finding.test.engagement.product.name

if pt in combined_notifications:
if p in combined_notifications[pt]:
if kind in combined_notifications[pt][p]:
combined_notifications[pt][p][kind].append(notification)
else:
combined_notifications[pt][p][kind] = [notification]
else:
combined_notifications[pt][p] = {kind: [notification]}
else:
combined_notifications[pt] = {p: {kind: [notification]}}

def _notification_title_for_finding(finding, kind, sla_age):
title = "Finding %s - " % (finding.id)
if kind == 'breached':
abs_sla_age = abs(sla_age)
period = "day"
if abs_sla_age > 1:
period = "days"
title += "SLA breached by %d %s! Overdue notice" % (abs_sla_age, period)
elif kind == 'prebreach':
title += "SLA pre-breach warning - %d day(s) left" % (sla_age)
elif kind == 'breaching':
title += "SLA is breaching today"

return title

def _create_notifications():
for pt in combined_notifications:
for p in combined_notifications[pt]:
for kind in combined_notifications[pt][p]:
# creating notifications on per-finding basis

# we need this list for combined notification feature as we
# can not supply references to local objects as
# create_notification() arguments
findings_list = []

for n in combined_notifications[pt][p][kind]:
title = _notification_title_for_finding(n.finding, kind, n.finding.sla_days_remaining())

create_notification(
event='sla_breach',
title=title,
finding=n.finding,
url=reverse('view_finding', args=(n.finding.id,)),
)

if n.do_jira_sla_comment:
logger.info("Creating JIRA comment to notify of SLA breach information.")
jira_helper.add_simple_jira_comment(jira_instance, n.jira_issue, title)

findings_list.append(n.finding)

# producing a "combined" SLA breach notification
title_combined = "SLA alert (%s): product type '%s', product '%s'" % (kind, pt, p)
product = combined_notifications[pt][p][kind][0].finding.test.engagement.product
create_notification(
event='sla_breach_combined',
title=title_combined,
product=product,
findings=findings_list,
breach_kind=kind,
base_url=get_script_prefix(),
)

# exit early on flags
system_settings = System_Settings.objects.get()
Expand All @@ -1943,6 +2013,8 @@ def _notify(finding, title):

jira_issue = None
jira_instance = None
# notifications list per product per product type
combined_notifications = {}
try:
if system_settings.enable_finding_sla:
logger.info("About to process findings for SLA notifications.")
Expand Down Expand Up @@ -2031,23 +2103,21 @@ def _notify(finding, title):
logger.info("Finding {} has breached by {} days.".format(finding.id, abs(sla_age)))
abs_sla_age = abs(sla_age)
if not system_settings.enable_notify_sla_exponential_backoff or abs_sla_age == 1 or (abs_sla_age & (abs_sla_age - 1) == 0):
period = "day"
if abs_sla_age > 1:
period = "days"
_notify(finding, 'Finding {} - SLA breached by {} {}! Overdue notice'.format(finding.id, abs_sla_age, period))
_add_notification(finding, 'breached')
else:
logger.info("Skipping notification as exponential backoff is enabled and the SLA is not a power of two")
# The finding is within the pre-breach period
elif (sla_age > 0) and (sla_age <= settings.SLA_NOTIFY_PRE_BREACH):
pre_breach_count += 1
logger.info("Security SLA pre-breach warning for finding ID {}. Days remaining: {}".format(finding.id, sla_age))
_notify(finding, 'Finding {} - SLA pre-breach warning - {} day(s) left'.format(finding.id, sla_age))
_add_notification(finding, 'prebreach')
# The finding breaches the SLA today
elif (sla_age == 0):
at_breach_count += 1
logger.info("Security SLA breach warning. Finding ID {} breaching today ({})".format(finding.id, sla_age))
_notify(finding, "Finding {} - SLA is breaching today".format(finding.id))
_add_notification(finding, 'breaching')

_create_notifications()
logger.info("SLA run results: Pre-breach: {}, at-breach: {}, post-breach: {}, post-breach-no-notify: {}, with-jira: {}, TOTAL: {}".format(
pre_breach_count,
at_breach_count,
Expand Down

0 comments on commit 0217ed7

Please sign in to comment.