Skip to content

Commit

Permalink
Consolidation of notification creation (#8824)
Browse files Browse the repository at this point in the history
* Add basic TestNotificationTriggers

* Clean eng

* Fix product_added

* Correct processing notification for "non-other" sources

* Rename "process_notifications" to "process_tag_notifications"

* Process product_deleted

* process endpoint

* process prod type

* test URLs

* Process others in all notification types

* Fix test typo

* Process tests

* Add processing engagement_deleted

* Fix unit tests

* Process engagement_pre_save

* Move functions to signals.py

* "process" findings

* "process" test

* process finding_group

* Fixes

* Add test_auditlog_on_off

* unittests: move auditloging

* event name consolidation

* Fix renamed engagement

* Fix testing auditlog

* Try exclude test_auditlog_on_off

* Change get_system_setting('enable_auditlog') to settings.ENABLE_AUDITLOG

* Change TODOs

* JIRA: more specific event names

* Fix Ruff

* Fix tests

* Unittest: Set username rendering

Signed-off-by: kiblik <[email protected]>

* Eng: Do not process "Not Started" -> "In Progress"

Signed-off-by: kiblik <[email protected]>

* Fix Eng ruff

* Fix TokenAuthentication

* localization

---------

Signed-off-by: kiblik <[email protected]>
  • Loading branch information
kiblik authored May 18, 2024
1 parent 417ee9e commit 3273c68
Show file tree
Hide file tree
Showing 24 changed files with 685 additions and 196 deletions.
5 changes: 5 additions & 0 deletions dojo/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,13 @@ def ready(self):
# Load any signals here that will be ready for runtime
# Importing the signals file is good enough if using the reciever decorator
import dojo.announcement.signals # noqa: F401
import dojo.endpoint.signals # noqa: F401
import dojo.engagement.signals # noqa: F401
import dojo.finding_group.signals # noqa: F401
import dojo.product.signals # noqa: F401
import dojo.product_type.signals # noqa: F401
import dojo.sla_config.helpers # noqa: F401
import dojo.tags_signals # noqa: F401
import dojo.test.signals # noqa: F401


Expand Down
30 changes: 30 additions & 0 deletions dojo/endpoint/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from auditlog.models import LogEntry
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.urls import reverse
from django.utils.translation import gettext as _

from dojo.models import Endpoint
from dojo.notifications.helper import create_notification


@receiver(post_delete, sender=Endpoint)
def endpoint_post_delete(sender, instance, using, origin, **kwargs):
if instance == origin:
if settings.ENABLE_AUDITLOG:
le = LogEntry.objects.get(
action=LogEntry.Action.DELETE,
content_type=ContentType.objects.get(app_label='dojo', model='endpoint'),
object_id=instance.id
)
description = _('The endpoint "%(name)s" was deleted by %(user)s') % {
'name': str(instance), 'user': le.actor}
else:
description = _('The endpoint "%(name)s" was deleted') % {'name': str(instance)}
create_notification(event='endpoint_deleted', # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
title=_('Deletion of %(name)s') % {'name': str(instance)},
description=description,
url=reverse('endpoint'),
icon="exclamation-triangle")
7 changes: 0 additions & 7 deletions dojo/endpoint/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from dojo.filters import EndpointFilter, EndpointFilterWithoutObjectLookups
from dojo.forms import AddEndpointForm, DeleteEndpointForm, DojoMetaDataForm, EditEndpointForm, ImportEndpointMetaForm
from dojo.models import DojoMeta, Endpoint, Endpoint_Status, Finding, Product
from dojo.notifications.helper import create_notification
from dojo.utils import (
Product_Tab,
add_breadcrumb,
Expand Down Expand Up @@ -222,12 +221,6 @@ def delete_endpoint(request, eid):
messages.SUCCESS,
'Endpoint and relationships removed.',
extra_tags='alert-success')
create_notification(event='other',
title=f'Deletion of {endpoint}',
product=product,
description=f'The endpoint "{endpoint}" was deleted by {request.user}',
url=reverse('endpoint'),
icon="exclamation-triangle")
return HttpResponseRedirect(reverse('view_product', args=(product.id,)))

collector = NestedObjects(using=DEFAULT_DB_ALIAS)
Expand Down
57 changes: 57 additions & 0 deletions dojo/engagement/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from auditlog.models import LogEntry
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models.signals import post_delete, post_save, pre_save
from django.dispatch import receiver
from django.urls import reverse
from django.utils.translation import gettext as _

from dojo.models import Engagement
from dojo.notifications.helper import create_notification


@receiver(post_save, sender=Engagement)
def engagement_post_save(sender, instance, created, **kwargs):
if created:
title = _('Engagement created for "%(product)s": %(name)s') % {'product': instance.product, 'name': instance.name}
create_notification(event='engagement_added', title=title, engagement=instance, product=instance.product,
url=reverse('view_engagement', args=(instance.id,)))


@receiver(pre_save, sender=Engagement)
def engagement_pre_save(sender, instance, **kwargs):
old = sender.objects.filter(pk=instance.pk).first()
if old and instance.status != old.status:
if instance.status in ["Cancelled", "Completed"]:
create_notification(event='engagement_closed',
title=_('Closure of %s') % instance.name,
description=_('The engagement "%s" was closed') % (instance.name),
engagement=instance, url=reverse('engagement_all_findings', args=(instance.id, )))
elif instance.status in ["In Progress"] and old.status not in ["Not Started"]:
create_notification(event='engagement_reopened',
title=_('Reopening of %s') % instance.name,
engagement=instance,
description=_('The engagement "%s" was reopened') % (instance.name),
url=reverse('view_engagement', args=(instance.id, )))


@receiver(post_delete, sender=Engagement)
def engagement_post_delete(sender, instance, using, origin, **kwargs):
if instance == origin:
if settings.ENABLE_AUDITLOG:
le = LogEntry.objects.get(
action=LogEntry.Action.DELETE,
content_type=ContentType.objects.get(app_label='dojo', model='engagement'),
object_id=instance.id
)
description = _('The engagement "%(name)s" was deleted by %(user)s') % {
'name': instance.name, 'user': le.actor}
else:
description = _('The engagement "%(name)s" was deleted') % {'name': instance.name}
create_notification(event='engagement_deleted', # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
title=_('Deletion of %(name)s') % {'name': instance.name},
description=description,
product=instance.product,
url=reverse('view_product', args=(instance.product.id, )),
recipients=[instance.lead],
icon="exclamation-triangle")
25 changes: 2 additions & 23 deletions dojo/engagement/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,10 +279,6 @@ def edit_engagement(request, eid):
engagement = form.save(commit=False)
if (new_status == "Cancelled" or new_status == "Completed"):
engagement.active = False
create_notification(event='close_engagement',
title=f'Closure of {engagement.name}',
description=f'The engagement "{engagement.name}" was closed',
engagement=engagement, url=reverse('engagement_all_findings', args=(engagement.id, ))),
else:
engagement.active = True
engagement.save()
Expand Down Expand Up @@ -361,14 +357,6 @@ def delete_engagement(request, eid):
messages.SUCCESS,
message,
extra_tags='alert-success')
create_notification(event='other',
title=f'Deletion of {engagement.name}',
product=product,
description=f'The engagement "{engagement.name}" was deleted by {request.user}',
url=request.build_absolute_uri(reverse('view_engagements', args=(product.id, ))),
recipients=[engagement.lead],
icon="exclamation-triangle")

return HttpResponseRedirect(reverse("view_engagements", args=(product.id, )))

rels = ['Previewing the relationships has been disabled.', '']
Expand Down Expand Up @@ -404,8 +392,8 @@ def copy_engagement(request, eid):
messages.SUCCESS,
'Engagement Copied successfully.',
extra_tags='alert-success')
create_notification(event='other',
title=f'Copying of {engagement.name}',
create_notification(event='engagement_copied', # TODO - if 'copy' functionality will be supported by API as well, 'create_notification' needs to be migrated to place where it will be able to cover actions from both interfaces
title=_('Copying of %s') % engagement.name,
description=f'The engagement "{engagement.name}" was copied by {request.user}',
product=product,
url=request.build_absolute_uri(reverse('view_engagement', args=(engagement_copy.id, ))),
Expand Down Expand Up @@ -1138,10 +1126,6 @@ def close_eng(request, eid):
messages.SUCCESS,
'Engagement closed successfully.',
extra_tags='alert-success')
create_notification(event='close_engagement',
title=f'Closure of {eng.name}',
description=f'The engagement "{eng.name}" was closed',
engagement=eng, url=reverse('engagement_all_findings', args=(eng.id, ))),
return HttpResponseRedirect(reverse("view_engagements", args=(eng.product.id, )))


Expand All @@ -1154,11 +1138,6 @@ def reopen_eng(request, eid):
messages.SUCCESS,
'Engagement reopened successfully.',
extra_tags='alert-success')
create_notification(event='other',
title=f'Reopening of {eng.name}',
engagement=eng,
description=f'The engagement "{eng.name}" was reopened',
url=reverse('view_engagement', args=(eng.id, ))),
return HttpResponseRedirect(reverse("view_engagements", args=(eng.product.id, )))


Expand Down
36 changes: 26 additions & 10 deletions dojo/finding/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from django.urls import reverse
from django.utils import formats, timezone
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from django.views import View
from django.views.decorators.http import require_POST
from imagekit import ImageSpec
Expand Down Expand Up @@ -117,7 +118,7 @@
get_system_setting,
get_words_for_field,
match_finding_to_existing_findings,
process_notifications,
process_tag_notifications,
redirect,
redirect_to_return_url_or_else,
reopen_external_issue,
Expand Down Expand Up @@ -716,7 +717,7 @@ def process_form(self, request: HttpRequest, finding: Finding, context: dict):
reverse("view_finding", args=(finding.id,))
)
title = f"Finding: {finding.title}"
process_notifications(request, new_note, url, title)
process_tag_notifications(request, new_note, url, title)
# Add a message to the request
messages.add_message(
request, messages.SUCCESS, "Note saved.", extra_tags="alert-success"
Expand Down Expand Up @@ -1169,9 +1170,14 @@ def process_form(self, request: HttpRequest, finding: Finding, context: dict):
"Finding deleted successfully.",
extra_tags="alert-success",
)

# Note: this notification has not be moved to "@receiver(post_delete, sender=Finding)" method as many other notifications
# Because it could generate too much noise, we keep it here only for findings created by hand in WebUI
# TODO: but same should be implemented for API endpoint

# Send a notification that the finding had been deleted
create_notification(
event="other",
event="finding_deleted",
title=f"Deletion of {finding.title}",
description=f'The finding "{finding.title}" was deleted by {request.user}',
product=product,
Expand Down Expand Up @@ -1288,9 +1294,14 @@ def close_finding(request, fid):
"Finding closed.",
extra_tags="alert-success",
)

# Note: this notification has not be moved to "@receiver(pre_save, sender=Finding)" method as many other notifications
# Because it could generate too much noise, we keep it here only for findings created by hand in WebUI
# TODO: but same should be implemented for API endpoint

create_notification(
event="other",
title=f"Closing of {finding.title}",
event="finding_closed",
title=_("Closing of %s") % finding.title,
finding=finding,
description=f'The finding "{finding.title}" was closed by {request.user}',
url=reverse("view_finding", args=(finding.id,)),
Expand Down Expand Up @@ -1451,9 +1462,14 @@ def reopen_finding(request, fid):
messages.add_message(
request, messages.SUCCESS, "Finding Reopened.", extra_tags="alert-success"
)

# Note: this notification has not be moved to "@receiver(pre_save, sender=Finding)" method as many other notifications
# Because it could generate too much noise, we keep it here only for findings created by hand in WebUI
# TODO: but same should be implemented for API endpoint

create_notification(
event="other",
title=f"Reopening of {finding.title}",
event="finding_reopened",
title=_("Reopening of %s") % finding.title,
finding=finding,
description=f'The finding "{finding.title}" was reopened by {request.user}',
url=reverse("view_finding", args=(finding.id,)),
Expand Down Expand Up @@ -1510,8 +1526,8 @@ def copy_finding(request, fid):
extra_tags="alert-success",
)
create_notification(
event="other",
title=f"Copying of {finding.title}",
event="finding_copied", # TODO - if 'copy' functionality will be supported by API as well, 'create_notification' needs to be migrated to place where it will be able to cover actions from both interfaces
title=_("Copying of %s") % finding.title,
description=f'The finding "{finding.title}" was copied by {request.user} to {test.title}',
product=product,
url=request.build_absolute_uri(
Expand Down Expand Up @@ -1686,7 +1702,7 @@ def request_finding_review(request, fid):
logger.debug(f"Asking {reviewers_string} for review")

create_notification(
event="review_requested",
event="review_requested", # TODO - if 'review_requested' functionality will be supported by API as well, 'create_notification' needs to be migrated to place where it will be able to cover actions from both interfaces
title="Finding review requested",
requested_by=user,
note=new_note,
Expand Down
31 changes: 31 additions & 0 deletions dojo/finding_group/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from auditlog.models import LogEntry
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.urls import reverse
from django.utils.translation import gettext as _

from dojo.models import Finding_Group
from dojo.notifications.helper import create_notification


@receiver(post_delete, sender=Finding_Group)
def finding_group_post_delete(sender, instance, using, origin, **kwargs):
if instance == origin:
if settings.ENABLE_AUDITLOG:
le = LogEntry.objects.get(
action=LogEntry.Action.DELETE,
content_type=ContentType.objects.get(app_label='dojo', model='finding_group'),
object_id=instance.id
)
description = _('The finding group "%(name)s" was deleted by %(user)s') % {
'name': instance.name, 'user': le.actor}
else:
description = _('The finding group "%(name)s" was deleted') % {'name': instance.name}
create_notification(event='finding_group_deleted', # template does not exists, it will default to "other" but this event name needs to stay because of unit testing
title=_('Deletion of %(name)s') % {'name': instance.name},
description=description,
product=instance.test.engagement.product,
url=reverse('view_test', args=(instance.test.id, )),
icon="exclamation-triangle")
9 changes: 0 additions & 9 deletions dojo/finding_group/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from dojo.finding.views import prefetch_for_findings
from dojo.forms import DeleteFindingGroupForm, EditFindingGroupForm, FindingBulkUpdateForm
from dojo.models import Engagement, Finding, Finding_Group, GITHUB_PKey, Product
from dojo.notifications.helper import create_notification
from dojo.utils import Product_Tab, add_breadcrumb, get_page_items, get_system_setting, get_words_for_field

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -115,19 +114,11 @@ def delete_finding_group(request, fgid):
if 'id' in request.POST and str(finding_group.id) == request.POST['id']:
form = DeleteFindingGroupForm(request.POST, instance=finding_group)
if form.is_valid():
product = finding_group.test.engagement.product
finding_group.delete()
messages.add_message(request,
messages.SUCCESS,
'Finding Group and relationships removed.',
extra_tags='alert-success')

create_notification(event='other',
title=f'Deletion of {finding_group.name}',
product=product,
description=f'The finding group "{finding_group.name}" was deleted by {request.user}',
url=request.build_absolute_uri(reverse('view_test', args=(finding_group.test.id,))),
icon="exclamation-triangle")
return HttpResponseRedirect(reverse('view_test', args=(finding_group.test.id,)))

collector = NestedObjects(using=DEFAULT_DB_ALIAS)
Expand Down
Loading

0 comments on commit 3273c68

Please sign in to comment.