ISO-27001-Risk-Management/risks/signals.py

444 lines
18 KiB
Python
Raw Normal View History

from datetime import date, datetime
from django.contrib.auth import get_user_model
from django.db.models import Model
from django.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from .audit_context import get_current_user
from .models import Control, Risk, ResidualRisk, AuditLog, Incident, Notification, NotificationKind, NotificationPreference
from .utils import model_diff, notify_event
# ---------------------------------------------------------------------------
# General definitions
# ---------------------------------------------------------------------------
User = get_user_model()
def serialize_value(value):
if isinstance(value, Model):
return value.pk # oder str(value), wenn du mehr Infos willst
if isinstance(value, (datetime, date)):
return value.isoformat()
return value
def _pref(user: User) -> NotificationPreference | None:
if not user:
return None
pref = getattr(user, "notification_preference", None)
if not pref:
pref = NotificationPreference.objects.create(user=user)
return pref
def _notify(users, message: str, event_code: str):
"""legt Notification für alle users an, die dieses Event wünschen."""
for u in set(filter(None, users)):
pref = _pref(u)
if pref and pref.should_notify(event_code):
Notification.objects.create(user=u, message=message)
def _risk_stakeholders(risk: Risk):
"""Risikoeigner + alle Verantwortlichen zugehöriger Controls."""
owners = [risk.owner] if risk.owner else []
responsibles = list(
User.objects.filter(responsible_controls__risks=risk).distinct()
)
return set(owners + responsibles)
# ---------------------------------------------------------------------------
# Incidents
# ---------------------------------------------------------------------------
@receiver(post_save, sender=User)
def user_saved(sender, instance: User, created, **kwargs):
# Prefs automatisch anlegen
_pref(instance)
# An Staff, die dieses Event wollen
if created:
staff = User.objects.filter(is_staff=True, notification_preference__user_created=True)
_notify(staff, _("User '{u}' created").format(u=instance.username), "user_created")
@receiver(post_delete, sender=User)
def user_deleted(sender, instance: User, **kwargs):
staff = User.objects.filter(is_staff=True, notification_preference__user_deleted=True)
_notify(staff, _("User '{u}' deleted").format(u=instance.username), "user_deleted")
# ---------------------------------------------------------------------------
# Risks
# ---------------------------------------------------------------------------
@receiver(post_save, sender=Risk)
def risk_saved(sender, instance: Risk, created, **kwargs):
event = "risk_created" if created else "risk_updated"
msg = _("Risk '{title}' {state}").format(
title=instance.title,
state=_("created") if created else _("updated"),
)
_notify([instance.owner], msg, event)
@receiver(post_delete, sender=Risk)
def risk_deleted(sender, instance: Risk, **kwargs):
msg = _("Risk '{title}' deleted").format(title=instance.title)
# Owner existiert evtl. nicht mehr -> kein Notify nötig
if instance.owner:
_notify([instance.owner], msg, "risk_deleted")
@receiver(post_save, sender=Risk)
def log_risk_save(sender, instance, created, **kwargs):
if created:
AuditLog.objects.create(
user=getattr(instance, "_changed_by", None),
action="create",
model="Risk",
object_id=instance.pk,
changes={
f.name: {
"old": None,
"new": serialize_value(getattr(instance, f.name))
} for f in instance._meta.fields
},
)
else:
old = Risk.objects.get(pk=instance.pk)
changes = model_diff(old, instance)
if changes:
clean_changes = {
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
for field, vals in changes.items()
}
AuditLog.objects.create(
user=getattr(instance, "_changed_by", None),
action="update",
model="Risk",
object_id=instance.pk,
changes=clean_changes,
)
if created:
notify_event(
NotificationKind.RISK_CREATED,
message=_("Risk created: {t}").format(t=instance.title),
users=[instance.owner] if instance.owner_id else None,
)
else:
notify_event(
NotificationKind.RISK_UPDATED,
message=_("Risk updated: {t}").format(t=instance.title),
users=[instance.owner] if instance.owner_id else None,
)
@receiver(post_delete, sender=Risk)
def log_risk_delete(sender, instance, **kwargs):
"""
Signal that runs after a Risk is deleted.
"""
user = getattr(instance, "_changed_by", None) or get_current_user()
AuditLog.objects.create(
user=user,
action="delete",
model="Risk",
object_id=instance.pk,
changes=None, # no fields to track on deletion
)
notify_event(
NotificationKind.RISK_DELETED,
message=_("Risk deleted: {t}").format(t=instance.title),
users=[instance.owner] if instance.owner_id else None,
)
# ---------------------------------------------------------------------------
# Controls
# ---------------------------------------------------------------------------
@receiver(post_save, sender=Control)
def control_saved(sender, instance: Control, created, **kwargs):
# Review-Flag für alle betroffenen Residuals setzen
for risk in instance.risks.all():
resid, created = ResidualRisk.objects.get_or_create(risk=risk)
# Statuswechsel auf Review Required
if not resid.review_required:
resid.review_required = True
resid.save()
if risk.status != "review_required":
Risk.objects.filter(pk=risk.pk).update(status="review_required")
# Notifications
event = "control_created" if created else "control_updated"
msg = _("Control '{title}' {state}").format(
title=instance.title,
state=_("created") if created else _("updated"),
)
stakeholders = {instance.responsible} | set(r.owner for r in instance.risks.all() if r.owner)
_notify(stakeholders, msg, event)
@receiver(post_delete, sender=Control)
def control_deleted(sender, instance: Control, **kwargs):
msg = _("Control '{title}' deleted").format(title=instance.title)
stakeholders = {instance.responsible} | set(r.owner for r in instance.risks.all() if r.owner)
_notify(stakeholders, msg, "control_deleted")
@receiver(post_save, sender=Control)
def log_control_save(sender, instance, created, **kwargs):
if created:
AuditLog.objects.create(
user=getattr(instance, "_changed_by", None),
action="create",
model="Control",
object_id=instance.pk,
changes={
f.name: {
"old": None,
"new": serialize_value(getattr(instance, f.name))
} for f in instance._meta.fields
},
)
else:
old = Control.objects.get(pk=instance.pk)
changes = model_diff(old, instance)
if changes:
clean_changes = {
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
for field, vals in changes.items()
}
AuditLog.objects.create(
user=getattr(instance, "_changed_by", None),
action="update",
model="Control",
object_id=instance.pk,
changes=clean_changes,
)
kind = NotificationKind.CONTROL_CREATED if created else NotificationKind.CONTROL_UPDATED
notify_event(
kind,
message=_("Control {event}: {t}").format(
event=_("created") if created else _("updated"),
t=instance.title,
),
users=[instance.responsible] if instance.responsible_id else None,
)
@receiver(post_delete, sender=Control)
def log_control_delete(sender, instance, **kwargs):
user = getattr(instance, "_changed_by", None) or get_current_user()
AuditLog.objects.create(
user=user,
action="delete",
model="Control",
object_id=instance.pk,
changes=None,
)
notify_event(
NotificationKind.CONTROL_DELETED,
message=_("Control deleted: {t}").format(t=instance.title),
users=[instance.responsible] if instance.responsible_id else None,
)
@receiver(m2m_changed, sender=Control.risks.through)
def control_risks_changed(sender, instance: Control, action, reverse, pk_set, **kwargs):
if action in {"post_add", "post_remove", "post_clear"}:
affected = instance.risks.all() if not pk_set else Risk.objects.filter(pk__in=pk_set)
for risk in affected:
resid, created = ResidualRisk.objects.get_or_create(risk=risk)
if not resid.review_required:
resid.review_required = True
resid.save()
if risk.status != "review_required":
Risk.objects.filter(pk=risk.pk).update(status="review_required")
_notify(_risk_stakeholders(risk), _("Review required for risk '{t}' due to control change").format(t=risk.title), "review_required")
# ---------------------------------------------------------------------------
# Residual risks
# ---------------------------------------------------------------------------
@receiver(post_save, sender=ResidualRisk)
def residual_saved(sender, instance: ResidualRisk, created, **kwargs):
# AuditLog erstellst du bereits anderswo hier Fokus auf Status/Notify
risk = instance.risk
old = None
if not created:
try:
old = ResidualRisk.objects.get(pk=instance.pk)
except ResidualRisk.DoesNotExist:
pass
# Review-Logik: wenn review_required=True -> Risk.status = review_required
if instance.review_required and risk.status != "review_required":
Risk.objects.filter(pk=risk.pk).update(status="review_required")
_notify(_risk_stakeholders(risk), _("Review required for risk '{t}'").format(t=risk.title), "review_required")
elif old and old.review_required and not instance.review_required:
# Review abgeschlossen
if risk.status == "review_required":
Risk.objects.filter(pk=risk.pk).update(status="open")
_notify(_risk_stakeholders(risk), _("Review completed for risk '{t}'").format(t=risk.title), "review_completed")
# Standard-Events
event = "residual_created" if created else "residual_updated"
_notify(_risk_stakeholders(risk), _("Residual risk {state} for '{t}'").format(
state=_("created") if created else _("updated"), t=risk.title), event)
@receiver(post_delete, sender=ResidualRisk)
def residual_deleted(sender, instance: ResidualRisk, **kwargs):
_notify(_risk_stakeholders(instance.risk), _("Residual risk deleted for '{t}'").format(t=instance.risk.title), "residual_deleted")
@receiver(post_save, sender=ResidualRisk)
def log_residual_save(sender, instance, created, **kwargs):
if created:
AuditLog.objects.create(
user=getattr(instance, "_changed_by", None),
action="create",
model="ResidualRisk",
object_id=instance.pk,
changes={
f.name: {
"old": None,
"new": serialize_value(getattr(instance, f.name))
} for f in instance._meta.fields
},
)
else:
old = ResidualRisk.objects.get(pk=instance.pk)
changes = model_diff(old, instance)
if changes:
clean_changes = {
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
for field, vals in changes.items()
}
AuditLog.objects.create(
user=getattr(instance, "_changed_by", None),
action="update",
model="ResidualRisk",
object_id=instance.pk,
changes=clean_changes,
)
if created:
notify_event(
NotificationKind.RESIDUAL_CREATED,
message=_("Residual created for risk: {t}").format(t=instance.risk.title),
users=[instance.risk.owner] if instance.risk.owner_id else None,
)
else:
# Änderungen prüfen
old = ResidualRisk.objects.get(pk=instance.pk)
changes = model_diff(old, instance)
# Review-Flag Wechsel gezielt melden:
if "review_required" in changes:
if getattr(instance, "review_required", False):
notify_event(
NotificationKind.RESIDUAL_REVIEW_REQUIRED,
message=_("Residual review required for risk: {t}").format(t=instance.risk.title),
users=[instance.risk.owner] if instance.risk.owner_id else None,
)
else:
notify_event(
NotificationKind.RESIDUAL_REVIEW_COMPLETED,
message=_("Residual review completed for risk: {t}").format(t=instance.risk.title),
users=[instance.risk.owner] if instance.risk.owner_id else None,
)
else:
notify_event(
NotificationKind.RESIDUAL_UPDATED,
message=_("Residual updated for risk: {t}").format(t=instance.risk.title),
users=[instance.risk.owner] if instance.risk.owner_id else None,
)
@receiver(post_delete, sender=ResidualRisk)
def log_residual_delete(sender, instance, **kwargs):
user = getattr(instance, "_changed_by", None) or get_current_user()
AuditLog.objects.create(
user=user,
action="delete",
model="ResidualRisk",
object_id=instance.pk,
changes=None,
)
notify_event(
NotificationKind.RESIDUAL_DELETED,
message=_("Residual deleted for risk: {t}").format(t=instance.risk.title),
users=[instance.risk.owner] if instance.risk.owner_id else None,
)
# ---------------------------------------------------------------------------
# Incidents
# ---------------------------------------------------------------------------
@receiver(post_save, sender=Incident)
def incident_saved(sender, instance: Incident, created, **kwargs):
event = "incident_created" if created else "incident_updated"
stakeholders = set([instance.reported_by]) | set(r.owner for r in instance.related_risks.all() if r.owner)
_notify(stakeholders, _("Incident '{t}' {s}").format(t=instance.title, s=_("created") if created else _("updated")), event)
@receiver(post_delete, sender=Incident)
def incident_deleted(sender, instance: Incident, **kwargs):
stakeholders = set([instance.reported_by]) | set(r.owner for r in instance.related_risks.all() if r.owner)
_notify(stakeholders, _("Incident '{t}' deleted").format(t=instance.title), "incident_deleted")
@receiver(post_save, sender=Incident)
def log_incident_save(sender, instance, created, **kwargs):
if created:
AuditLog.objects.create(
user=getattr(instance, "_changed_by", None),
action="create",
model="Incident",
object_id=instance.pk,
changes={
f.name: {
"old": None,
"new": serialize_value(getattr(instance, f.name))
} for f in instance._meta.fields
},
)
else:
old = Incident.objects.get(pk=instance.pk)
changes = model_diff(old, instance)
if changes:
clean_changes = {
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
for field, vals in changes.items()
}
AuditLog.objects.create(
user=getattr(instance, "_changed_by", None),
action="update",
model="Incident",
object_id=instance.pk,
changes=clean_changes,
)
kind = NotificationKind.INCIDENT_CREATED if created else NotificationKind.INCIDENT_UPDATED
notify_event(
kind,
message=_("Incident {event}: {t}").format(
event=_("created") if created else _("updated"),
t=instance.title,
),
users=[instance.reported_by] if instance.reported_by_id else None,
)
@receiver(m2m_changed, sender=Incident.related_risks.through)
def log_incident_risks_change(sender, instance, action, reverse, model, pk_set, **kwargs):
if action in ["post_add", "post_remove", "post_clear"]:
user = getattr(instance, "_changed_by", None) or get_current_user()
AuditLog.objects.create(
user=user,
action="update",
model="Incident",
object_id=instance.pk,
changes={"related_risks": {"action": action, "ids": list(pk_set)}},
)
@receiver(post_delete, sender=Incident)
def log_incident_delete(sender, instance, **kwargs):
user = getattr(instance, "_changed_by", None) or get_current_user()
AuditLog.objects.create(
user=user,
action="delete",
model="Incident",
object_id=instance.pk,
changes=None,
)
notify_event(
NotificationKind.INCIDENT_DELETED,
message=_("Incident deleted: {t}").format(t=instance.title),
users=[instance.reported_by] if instance.reported_by_id else None,
)