From 43e86d0357d7157ccd670f60946ad85ea563aeb7 Mon Sep 17 00:00:00 2001 From: Kevin Heyer Date: Mon, 8 Sep 2025 15:03:12 +0200 Subject: [PATCH] feat: Enhance Risk Management Module - Updated Risk model to include description, created_at, and updated_at fields. - Modified RiskSerializer to include created_at and updated_at in serialized output. - Improved logging in signals for Risk and Control models, including serialization of values. - Added new template tags for CIA label mapping. - Refactored URL patterns for better clarity and added detail views for risks, controls, and incidents. - Implemented list and detail views for risks, controls, and incidents with filtering options. - Enhanced CSS for better UI/UX, including breadcrumbs and table styling. - Created new templates for displaying individual risks, controls, and incidents with detailed information. --- TODO | 54 ----- api/__pycache__/__init__.cpython-311.pyc | Bin 180 -> 173 bytes api/__pycache__/views.cpython-311.pyc | Bin 1262 -> 1255 bytes config/__pycache__/__init__.cpython-311.pyc | Bin 183 -> 176 bytes config/__pycache__/settings.cpython-311.pyc | Bin 5070 -> 5063 bytes config/__pycache__/urls.cpython-311.pyc | Bin 1865 -> 1858 bytes config/__pycache__/wsgi.cpython-311.pyc | Bin 703 -> 696 bytes db.sqlite3 | Bin 204800 -> 208896 bytes .../0009_risk_created_at_risk_updatet_at.py | 25 ++ ...0010_alter_residualrisk_impact_and_more.py | 39 +++ risks/migrations/0011_risk_description.py | 18 ++ ...0012_alter_residualrisk_impact_and_more.py | 33 +++ risks/models.py | 9 +- risks/serializers.py | 2 + risks/signals.py | 97 ++++++-- risks/templatetags/___init__.py | 0 risks/templatetags/risk_extras.py | 9 + risks/urls.py | 9 +- risks/views.py | 94 +++++++- static/css/design.css | 18 +- templates/base.html | 33 ++- templates/risks/item_control.html | 125 ++++++++++ templates/risks/item_incident.html | 8 + templates/risks/item_risk.html | 224 ++++++++++++++++++ templates/risks/list_controls.html | 151 +++++++++++- templates/risks/list_incidents.html | 110 ++++++++- templates/risks/list_risks.html | 122 +++++++++- 27 files changed, 1073 insertions(+), 107 deletions(-) create mode 100644 risks/migrations/0009_risk_created_at_risk_updatet_at.py create mode 100644 risks/migrations/0010_alter_residualrisk_impact_and_more.py create mode 100644 risks/migrations/0011_risk_description.py create mode 100644 risks/migrations/0012_alter_residualrisk_impact_and_more.py create mode 100644 risks/templatetags/___init__.py create mode 100644 risks/templatetags/risk_extras.py create mode 100644 templates/risks/item_control.html create mode 100644 templates/risks/item_incident.html create mode 100644 templates/risks/item_risk.html diff --git a/TODO b/TODO index 3a5c5be..95b27bb 100644 --- a/TODO +++ b/TODO @@ -1,66 +1,12 @@ ✅Risiken -✅-Titel -✅-Asset -✅-Prozess -✅-Kategorie -✅-Eintrittswahrscheinlichkeit vor Maßnahmen -✅-Schadenshöhe vor Maßnahmen -✅-Score vor Maßnahmen (Berechnet sich aus Eintrittswahrscheinlichkeit und Schadenshöhe) -✅-Stufe vor Maßnahmen (Errechnet sich aus dem Score) -✅-Risikoeigner -✅-Maßnahmen (Ein Risiko kann mehrere Maßnahmen haben) -✅-Restrisiko -✅-Wiedervorlage -✅-Schutzziele - ✅Eintrittswahrscheinlichkeiten -✅-1, sehr gering, Voraussichtliches Auftreten seltener als einmal in 5 Jahren. -✅-2, gering, Voraussichtliches Auftreten einmal in 1 bis 5 Jahren. -✅-3, wahrscheinlich, Voraussichtliches Auftreten einmal pro Jahr oder häufiger. -✅-4, sehr wahrscheinlich, Voraussichtliches Auftreten mehrmals pro Jahr oder monatlich. - ✅Schadenshöhen -✅-1, niedrig, (z.B. Schaden < 1.000 €, geringer operativer Einfluss) -✅-2, mittel, (z.B. Schaden 1.000 € -5.000 €, lokaler Einfluss) -✅-3, hoch, (z.B. Schaden 5.000 € -15.000 €, Einfluss auf ein Team) -✅-4, erheblich, (z.B. Schaden 50.000 € -100.000 €, regionaler Einfluss) -✅-5, kritisch, (z.B. Schaden > 100.000 €, existenzbedrohend) - ✅Maßnahmen -✅-Titel -✅-Status -✅-Frist -✅-Verantwortlicher -✅-Beschreibung -✅-Wiki-Link - ✅Maßnahmenstatus -✅-Geplant, Die Maßnahme wurde identifiziert und im Risikoregister erfasst, die Umsetzung hat jedoch noch nicht begonnen. Dies ist der Ausgangsstatus für jede neue Maßnahme. -✅-In Bearbeitung, Die Umsetzung der Maßnahme hat begonnen. -✅-Abgeschlossen, Die Maßnahme wurde vollständig umgesetzt (Triggert Neubewertung durch Risikoeigner) -✅-Überprüft, Die Wirksamkeit der abgeschlossenen Maßnahme wurde verifiziert und bestätigt. -✅-Abgelehnt/Verworfen, Eine geplante Maßnahme wird nicht umgesetzt, weil sie entweder nicht mehr relevant ist, die Kosten zu hoch sind oder eine alternative, effektivere Maßnahme gefunden wurde. Dies muss gut dokumentiert und begründet werden. - ✅Restrisiko -✅-Risiko identifikation -✅-Eintrittswahrscheinlichkeit nach Maßnahmen -✅-Schadenshöhe nach Maßnahmen -✅-Score nach Maßnahmen (Berechnet sich aus Eintrittswahrscheinlichkeit und Schadenshöhe) -✅-Stufe nach Maßnahmen (Errechnet sich aus dem Score) - ✅Schutzziele (CIA) -✅-Verfügbarkeit -✅-Integrität -✅-Vertraulichkeit - ✅Benutzer -✅-Benutzer ist Risikoverantwortlicher -✅-Benutzer ist Maßnahmenverantwortlicher - ✅Audit -✅-Logging -✅-Audit-Trail - ✅Vorfälle Benachrichtigungen diff --git a/api/__pycache__/__init__.cpython-311.pyc b/api/__pycache__/__init__.cpython-311.pyc index 2fb33de5555a0f3d11b9035f34f604a1b22c5b5a..6ba22184670e8181044f0ecf1f7206e707d78756 100644 GIT binary patch delta 45 zcmdnOxR#N7IWI340}#AU**B5fRLns?BR@A)KRdN7GcO~xGPOv*D77GeVr)17Fdz=u delta 52 zcmZ3>xP_5>IWI340}ymv-8GThRMuTTBR@A)KO?m=wJ0$qH#1MaD77HJII|?bC^L0p GSU3QsoDtOk diff --git a/api/__pycache__/views.cpython-311.pyc b/api/__pycache__/views.cpython-311.pyc index d70b87cd06bacfbbbc97a490519687d515069c41..b7efb642b9757d76b76808bb53d20268cd8e0051 100644 GIT binary patch delta 48 zcmaFI`J9t`IWI340}#AU*|(8<8Kao3enx(7s(yBAS!P~FYGrDXeo<;c{^oOx3d{g~ C&k#%i delta 55 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 Ma0Zw=#bWFD2P 100,000 € – existential threat)')], default=1), + ), + migrations.AlterField( + model_name='residualrisk', + name='likelihood', + field=models.IntegerField(choices=[('1', 'Very low – occurs less than once every 5 years'), ('2', 'Low – once every 1–5 years'), ('3', 'Likely – once per year or more'), ('4', 'Very likely – multiple times per year/monthly')], default=1), + ), + migrations.AlterField( + model_name='risk', + name='cia', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('1', 'Confidentiality'), ('2', 'Integrity'), ('3', 'Availability')], max_length=100, null=True), + ), + migrations.AlterField( + model_name='risk', + name='impact', + field=models.IntegerField(choices=[('1', 'Low (< 1,000 € – minor operational impact)'), ('2', 'Medium (1,000–5,000 € – local impact)'), ('3', 'High (5,000–15,000 € – team-level impact)'), ('4', 'Severe (50,000–100,000 € – regional impact)'), ('5', 'Critical (> 100,000 € – existential threat)')], default=1), + ), + migrations.AlterField( + model_name='risk', + name='likelihood', + field=models.IntegerField(choices=[('1', 'Very low – occurs less than once every 5 years'), ('2', 'Low – once every 1–5 years'), ('3', 'Likely – once per year or more'), ('4', 'Very likely – multiple times per year/monthly')], default=1), + ), + ] diff --git a/risks/migrations/0011_risk_description.py b/risks/migrations/0011_risk_description.py new file mode 100644 index 0000000..ef8025d --- /dev/null +++ b/risks/migrations/0011_risk_description.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.6 on 2025-09-08 09:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('risks', '0010_alter_residualrisk_impact_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='risk', + name='description', + field=models.TextField(blank=True, max_length=225, null=True), + ), + ] diff --git a/risks/migrations/0012_alter_residualrisk_impact_and_more.py b/risks/migrations/0012_alter_residualrisk_impact_and_more.py new file mode 100644 index 0000000..76b918c --- /dev/null +++ b/risks/migrations/0012_alter_residualrisk_impact_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.6 on 2025-09-08 09:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('risks', '0011_risk_description'), + ] + + operations = [ + migrations.AlterField( + model_name='residualrisk', + name='impact', + field=models.IntegerField(choices=[(1, 'Low (< 1,000 € – minor operational impact)'), (2, 'Medium (1,000–5,000 € – local impact)'), (3, 'High (5,000–15,000 € – team-level impact)'), (4, 'Severe (50,000–100,000 € – regional impact)'), (5, 'Critical (> 100,000 € – existential threat)')], default=1), + ), + migrations.AlterField( + model_name='residualrisk', + name='likelihood', + field=models.IntegerField(choices=[(1, 'Very low – occurs less than once every 5 years'), (2, 'Low – once every 1–5 years'), (3, 'Likely – once per year or more'), (4, 'Very likely – multiple times per year/monthly')], default=1), + ), + migrations.AlterField( + model_name='risk', + name='impact', + field=models.IntegerField(choices=[(1, 'Low (< 1,000 € – minor operational impact)'), (2, 'Medium (1,000–5,000 € – local impact)'), (3, 'High (5,000–15,000 € – team-level impact)'), (4, 'Severe (50,000–100,000 € – regional impact)'), (5, 'Critical (> 100,000 € – existential threat)')], default=1), + ), + migrations.AlterField( + model_name='risk', + name='likelihood', + field=models.IntegerField(choices=[(1, 'Very low – occurs less than once every 5 years'), (2, 'Low – once every 1–5 years'), (3, 'Likely – once per year or more'), (4, 'Very likely – multiple times per year/monthly')], default=1), + ), + ] diff --git a/risks/models.py b/risks/models.py index 09509ee..ced4069 100644 --- a/risks/models.py +++ b/risks/models.py @@ -40,16 +40,19 @@ class Risk(models.Model): ] CIA_CHOICES = [ - (1, "Confidentiality"), - (2, "Integrity"), - (3, "Availability") + ("1", "Confidentiality"), + ("2", "Integrity"), + ("3", "Availability") ] # Basic information title = models.CharField(max_length=255) + description = models.TextField(max_length=225, blank=True, null=True) asset = models.CharField(max_length=255, blank=True, null=True) process = models.CharField(max_length=255, blank=True, null=True) category = models.CharField(max_length=255, blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True,) + updatet_at = models.DateTimeField(auto_now=True) # CIA Protection Goals cia = MultiSelectField(choices=CIA_CHOICES, max_length=100, blank=True, null=True) diff --git a/risks/serializers.py b/risks/serializers.py index 2f065ce..0570c3e 100644 --- a/risks/serializers.py +++ b/risks/serializers.py @@ -43,6 +43,8 @@ class RiskSerializer(serializers.ModelSerializer): "asset", "process", "category", + "created_at", + "updatet_at", "likelihood", "impact", "score", diff --git a/risks/signals.py b/risks/signals.py index 89b57c5..c8ccef2 100644 --- a/risks/signals.py +++ b/risks/signals.py @@ -1,41 +1,50 @@ +from datetime import date, datetime +from django.db.models import Model from django.db.models.signals import post_save, post_delete, m2m_changed from django.dispatch import receiver from .models import Control, Risk, ResidualRisk, AuditLog, Incident from .utils import model_diff -@receiver(post_save, sender=Control) -def update_residual_risk_on_control_change(sender, instance, **kwargs): - """ - Whenever a control is saved, check if the related risk has a residual risk. - If a control is completed or verified, flag the residual risk for review. - """ - - risk = instance.risk - - # Ensure residual risk exists - residual, created = ResidualRisk.objects.get_or_create(risk=risk) - - # If a control is marked as completed or verified, we mark residual risk for review - if instance.status in ["completed", "verified"]: - residual.review_required = True - residual.save() +# --------------------------------------------------------------------------- +# General definitions +# --------------------------------------------------------------------------- +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 +# --------------------------------------------------------------------------- +# Risks +# --------------------------------------------------------------------------- @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": getattr(instance, f.name)} for f in instance._meta.fields}, + 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", @@ -57,6 +66,9 @@ def log_risk_delete(sender, instance, **kwargs): changes=None, # no fields to track on deletion ) +# --------------------------------------------------------------------------- +# Controls +# --------------------------------------------------------------------------- @receiver(post_save, sender=Control) def log_control_save(sender, instance, created, **kwargs): @@ -66,13 +78,22 @@ def log_control_save(sender, instance, created, **kwargs): action="create", model="Control", object_id=instance.pk, - changes={f.name: {"old": None, "new": getattr(instance, f.name)} for f in instance._meta.fields}, + 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", @@ -91,6 +112,26 @@ def log_control_delete(sender, instance, **kwargs): changes=None, ) +@receiver(post_save, sender=Control) +def update_residual_risk_on_control_change(sender, instance, **kwargs): + """ + Whenever a control is saved, check if the related risk has a residual risk. + If a control is completed or verified, flag the residual risk for review. + """ + + risk = instance.risk + + # Ensure residual risk exists + residual, created = ResidualRisk.objects.get_or_create(risk=risk) + + # If a control is marked as completed or verified, we mark residual risk for review + if instance.status in ["completed", "verified"]: + residual.review_required = True + residual.save() + +# --------------------------------------------------------------------------- +# Residual risks +# --------------------------------------------------------------------------- @receiver(post_save, sender=ResidualRisk) def log_residual_save(sender, instance, created, **kwargs): @@ -100,19 +141,28 @@ def log_residual_save(sender, instance, created, **kwargs): action="create", model="ResidualRisk", object_id=instance.pk, - changes={f.name: {"old": None, "new": getattr(instance, f.name)} for f in instance._meta.fields}, + 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=changes, + changes=clean_changes, ) @receiver(post_delete, sender=ResidualRisk) @@ -125,6 +175,9 @@ def log_residual_delete(sender, instance, **kwargs): changes=None, ) +# --------------------------------------------------------------------------- +# Incidents +# --------------------------------------------------------------------------- @receiver(post_save, sender=Incident) def log_incident_save(sender, instance, created, **kwargs): @@ -141,6 +194,10 @@ def log_incident_save(sender, instance, created, **kwargs): 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", diff --git a/risks/templatetags/___init__.py b/risks/templatetags/___init__.py new file mode 100644 index 0000000..e69de29 diff --git a/risks/templatetags/risk_extras.py b/risks/templatetags/risk_extras.py new file mode 100644 index 0000000..ed5945d --- /dev/null +++ b/risks/templatetags/risk_extras.py @@ -0,0 +1,9 @@ +from django import template +from ..models import Risk + +register = template.Library() + +@register.filter +def cia_label(value): + mapping = dict(Risk.CIA_CHOICES) + return mapping.get(value, value) diff --git a/risks/urls.py b/risks/urls.py index fdee6b0..6fbc91a 100644 --- a/risks/urls.py +++ b/risks/urls.py @@ -7,7 +7,10 @@ urlpatterns = [ path("", views.dashboard, name="dashboard"), path("risks/index", views.dashboard, name="index"), path("risks/stats", views.stats, name="statistics"), - path("risks/risks", views.risks, name="risks"), - path("risks/controls", views.controls, name="controls"), - path("risks/incidents", views.incidents, name="incidents"), + path("risks/list_risks", views.list_risks, name="list_risks"), + path("risks/risks/", views.show_risk, name="show_risk"), + path("risks/list_controls", views.list_controls, name="list_controls"), + path("risks/controls/", views.show_control, name="show_control"), + path("risks/list_incidents", views.list_incidents, name="list_incidents"), + path("risks/incidents/", views.show_incident, name="show_incident"), ] \ No newline at end of file diff --git a/risks/views.py b/risks/views.py index 9c810b0..0b1a62d 100644 --- a/risks/views.py +++ b/risks/views.py @@ -1,10 +1,14 @@ +from django.contrib.admin.models import LogEntry from django.contrib.auth import get_user_model +from django.contrib.contenttypes.models import ContentType from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated -from django.shortcuts import render +from django.shortcuts import render, get_object_or_404 from .models import Risk, Control, ResidualRisk, AuditLog, Incident from .serializers import ControlSerializer, RiskSerializer, ResidualRiskSerializer, UserSerializer, AuditSerializer, IncidentSerializer +User = get_user_model() + # --------------------------------------------------------------------------- # API # --------------------------------------------------------------------------- @@ -51,8 +55,6 @@ class ResidualRiskViewSet(viewsets.ModelViewSet): serializer_class = ResidualRiskSerializer permission_classes = [IsAuthenticated] -User = get_user_model() - class UserViewSet(viewsets.ReadOnlyModelViewSet): """ API endpoint for listing users and their responsibilities. @@ -107,11 +109,85 @@ def dashboard(request): def stats(request): return render(request, "risks/statistics.html") -def risks(request): - return render(request, "risks/list_risks.html") +def list_risks(request): + qs = Risk.objects.all().select_related("owner") -def controls(request): - return render(request, "risks/list_controls.html") + # GET-Parameter lesen + risk_id = request.GET.get("risk") + control_id = request.GET.get("control") + owner_id = request.GET.get("owner") -def incidents(request): - return render(request, "risks/list_incidents.html") \ No newline at end of file + if risk_id: + qs = qs.filter(id=risk_id) + if control_id: + qs = qs.filter(controls__id=control_id) + if owner_id: + qs = qs.filter(owner_id=owner_id) + + risks = qs.order_by("title").distinct() + + controls = Control.objects.all().order_by("title") + owners = User.objects.filter(owned_risks__isnull=False).distinct().order_by("username") + + return render(request, "risks/list_risks.html", { + "risks": risks, + "controls": controls, + "owners": owners, + }) + +def show_risk(request, id): + risk = get_object_or_404(Risk, pk=id) + ct = ContentType.objects.get_for_model(Risk) + logs = LogEntry.objects.filter( + content_type=ct, + object_id=risk.pk + ).order_by("-action_time") + + return render(request, "risks/item_risk.html", {"risk": risk, "logs": logs}) + +def list_controls(request): + qs = Control.objects.all().select_related("risk", "responsible") + + # Filter + control_id = request.GET.get("control") + risk_id = request.GET.get("risk") + status = request.GET.get("status") + responsible_id = request.GET.get("responsible") + + if control_id: + qs = qs.filter(id=control_id) + if risk_id: + qs = qs.filter(risk_id=risk_id) + if status: + qs = qs.filter(status=status) + if responsible_id: + qs = qs.filter(responsible_id=responsible_id) + + controls = qs.order_by("title") + + risks = Risk.objects.all().order_by("title") + users = User.objects.filter(responsible_controls__isnull=False).distinct().order_by("username") + + return render(request, "risks/list_controls.html", { + "controls": controls, + "risks": risks, + "users": users, + "status_choices": Control.STATUS_CHOICES, + }) + +def show_control(request, id): + control = get_object_or_404(Control, pk=id) + ct = ContentType.objects.get_for_model(Control) + logs = LogEntry.objects.filter( + content_type=ct, + object_id=control.pk + ).order_by("-action_time") + + return render(request, "risks/item_control.html", {"control": control, "logs": logs}) + +def list_incidents(request): + return render(request, "risks/list_incidents.html") + +def show_incident(request, id): + incident = Incident.objects.get(pk=id) + return render(request, "risks/item_incident.html", {"incident": incident }) \ No newline at end of file diff --git a/static/css/design.css b/static/css/design.css index 359ea4a..a24aa1f 100644 --- a/static/css/design.css +++ b/static/css/design.css @@ -42,4 +42,20 @@ .home-icon { font-size: 20px; display: inline-block; margin: 10px; } /* Dropdown optisch näher am Screenshot */ -.navbar-dropdown { border-top: none; box-shadow: 0 8px 16px rgba(0,0,0,.1); } \ No newline at end of file +.navbar-dropdown { border-top: none; box-shadow: 0 8px 16px rgba(0,0,0,.1); } + +/* Breadcrumbs */ +.top-breadcrumb { padding: 10px 0;} + +.breadcrumb { + margin-bottom: 20px; + background-color: #f0ebeb; +} + +.content li{ + margin-top: 5px !important; +} + +.content li+li { + margin: 0 !important; +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index b31b9c5..6112d79 100644 --- a/templates/base.html +++ b/templates/base.html @@ -14,7 +14,7 @@