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.
This commit is contained in:
Kevin Heyer 2025-09-08 15:03:12 +02:00
parent ee78caa3d1
commit 43e86d0357
27 changed files with 1073 additions and 107 deletions

54
TODO
View file

@ -1,66 +1,12 @@
✅Risiken ✅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 ✅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 ✅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 ✅Maßnahmen
✅-Titel
✅-Status
✅-Frist
✅-Verantwortlicher
✅-Beschreibung
✅-Wiki-Link
✅Maßnahmenstatus ✅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 ✅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) ✅Schutzziele (CIA)
✅-Verfügbarkeit
✅-Integrität
✅-Vertraulichkeit
✅Benutzer ✅Benutzer
✅-Benutzer ist Risikoverantwortlicher
✅-Benutzer ist Maßnahmenverantwortlicher
✅Audit ✅Audit
✅-Logging
✅-Audit-Trail
✅Vorfälle ✅Vorfälle
Benachrichtigungen Benachrichtigungen

Binary file not shown.

View file

@ -0,0 +1,25 @@
# Generated by Django 5.2.6 on 2025-09-08 09:15
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('risks', '0008_rename_send_notification_sent'),
]
operations = [
migrations.AddField(
model_name='risk',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='risk',
name='updatet_at',
field=models.DateTimeField(auto_now=True),
),
]

View file

@ -0,0 +1,39 @@
# Generated by Django 5.2.6 on 2025-09-08 09:44
import multiselectfield.db.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('risks', '0009_risk_created_at_risk_updatet_at'),
]
operations = [
migrations.AlterField(
model_name='residualrisk',
name='impact',
field=models.IntegerField(choices=[('1', 'Low (< 1,000 € minor operational impact)'), ('2', 'Medium (1,0005,000 € local impact)'), ('3', 'High (5,00015,000 € team-level impact)'), ('4', 'Severe (50,000100,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 15 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,0005,000 € local impact)'), ('3', 'High (5,00015,000 € team-level impact)'), ('4', 'Severe (50,000100,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 15 years'), ('3', 'Likely once per year or more'), ('4', 'Very likely multiple times per year/monthly')], default=1),
),
]

View file

@ -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),
),
]

View file

@ -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,0005,000 € local impact)'), (3, 'High (5,00015,000 € team-level impact)'), (4, 'Severe (50,000100,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 15 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,0005,000 € local impact)'), (3, 'High (5,00015,000 € team-level impact)'), (4, 'Severe (50,000100,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 15 years'), (3, 'Likely once per year or more'), (4, 'Very likely multiple times per year/monthly')], default=1),
),
]

View file

@ -40,16 +40,19 @@ class Risk(models.Model):
] ]
CIA_CHOICES = [ CIA_CHOICES = [
(1, "Confidentiality"), ("1", "Confidentiality"),
(2, "Integrity"), ("2", "Integrity"),
(3, "Availability") ("3", "Availability")
] ]
# Basic information # Basic information
title = models.CharField(max_length=255) 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) asset = models.CharField(max_length=255, blank=True, null=True)
process = 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) 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 Protection Goals
cia = MultiSelectField(choices=CIA_CHOICES, max_length=100, blank=True, null=True) cia = MultiSelectField(choices=CIA_CHOICES, max_length=100, blank=True, null=True)

View file

@ -43,6 +43,8 @@ class RiskSerializer(serializers.ModelSerializer):
"asset", "asset",
"process", "process",
"category", "category",
"created_at",
"updatet_at",
"likelihood", "likelihood",
"impact", "impact",
"score", "score",

View file

@ -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.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver from django.dispatch import receiver
from .models import Control, Risk, ResidualRisk, AuditLog, Incident from .models import Control, Risk, ResidualRisk, AuditLog, Incident
from .utils import model_diff from .utils import model_diff
@receiver(post_save, sender=Control) # ---------------------------------------------------------------------------
def update_residual_risk_on_control_change(sender, instance, **kwargs): # General definitions
""" # ---------------------------------------------------------------------------
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()
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) @receiver(post_save, sender=Risk)
def log_risk_save(sender, instance, created, **kwargs): def log_risk_save(sender, instance, created, **kwargs):
if created: if created:
AuditLog.objects.create( AuditLog.objects.create(
user=getattr(instance, "_changed_by", None), user=getattr(instance, "_changed_by", None),
action="create", action="create",
model="Risk", model="Risk",
object_id=instance.pk, 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: else:
old = Risk.objects.get(pk=instance.pk) old = Risk.objects.get(pk=instance.pk)
changes = model_diff(old, instance) changes = model_diff(old, instance)
if changes: if changes:
clean_changes = {
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
for field, vals in changes.items()
}
AuditLog.objects.create( AuditLog.objects.create(
user=getattr(instance, "_changed_by", None), user=getattr(instance, "_changed_by", None),
action="update", action="update",
@ -57,6 +66,9 @@ def log_risk_delete(sender, instance, **kwargs):
changes=None, # no fields to track on deletion changes=None, # no fields to track on deletion
) )
# ---------------------------------------------------------------------------
# Controls
# ---------------------------------------------------------------------------
@receiver(post_save, sender=Control) @receiver(post_save, sender=Control)
def log_control_save(sender, instance, created, **kwargs): def log_control_save(sender, instance, created, **kwargs):
@ -66,13 +78,22 @@ def log_control_save(sender, instance, created, **kwargs):
action="create", action="create",
model="Control", model="Control",
object_id=instance.pk, 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: else:
old = Control.objects.get(pk=instance.pk) old = Control.objects.get(pk=instance.pk)
changes = model_diff(old, instance) changes = model_diff(old, instance)
if changes: if changes:
clean_changes = {
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
for field, vals in changes.items()
}
AuditLog.objects.create( AuditLog.objects.create(
user=getattr(instance, "_changed_by", None), user=getattr(instance, "_changed_by", None),
action="update", action="update",
@ -91,6 +112,26 @@ def log_control_delete(sender, instance, **kwargs):
changes=None, 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) @receiver(post_save, sender=ResidualRisk)
def log_residual_save(sender, instance, created, **kwargs): def log_residual_save(sender, instance, created, **kwargs):
@ -100,19 +141,28 @@ def log_residual_save(sender, instance, created, **kwargs):
action="create", action="create",
model="ResidualRisk", model="ResidualRisk",
object_id=instance.pk, 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: else:
old = ResidualRisk.objects.get(pk=instance.pk) old = ResidualRisk.objects.get(pk=instance.pk)
changes = model_diff(old, instance) changes = model_diff(old, instance)
if changes: if changes:
clean_changes = {
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
for field, vals in changes.items()
}
AuditLog.objects.create( AuditLog.objects.create(
user=getattr(instance, "_changed_by", None), user=getattr(instance, "_changed_by", None),
action="update", action="update",
model="ResidualRisk", model="ResidualRisk",
object_id=instance.pk, object_id=instance.pk,
changes=changes, changes=clean_changes,
) )
@receiver(post_delete, sender=ResidualRisk) @receiver(post_delete, sender=ResidualRisk)
@ -125,6 +175,9 @@ def log_residual_delete(sender, instance, **kwargs):
changes=None, changes=None,
) )
# ---------------------------------------------------------------------------
# Incidents
# ---------------------------------------------------------------------------
@receiver(post_save, sender=Incident) @receiver(post_save, sender=Incident)
def log_incident_save(sender, instance, created, **kwargs): 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) changes = model_diff(old, instance)
if changes: if changes:
clean_changes = {
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
for field, vals in changes.items()
}
AuditLog.objects.create( AuditLog.objects.create(
user=getattr(instance, "_changed_by", None), user=getattr(instance, "_changed_by", None),
action="update", action="update",

View file

View file

@ -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)

View file

@ -7,7 +7,10 @@ urlpatterns = [
path("", views.dashboard, name="dashboard"), path("", views.dashboard, name="dashboard"),
path("risks/index", views.dashboard, name="index"), path("risks/index", views.dashboard, name="index"),
path("risks/stats", views.stats, name="statistics"), path("risks/stats", views.stats, name="statistics"),
path("risks/risks", views.risks, name="risks"), path("risks/list_risks", views.list_risks, name="list_risks"),
path("risks/controls", views.controls, name="controls"), path("risks/risks/<int:id>", views.show_risk, name="show_risk"),
path("risks/incidents", views.incidents, name="incidents"), path("risks/list_controls", views.list_controls, name="list_controls"),
path("risks/controls/<int:id>", views.show_control, name="show_control"),
path("risks/list_incidents", views.list_incidents, name="list_incidents"),
path("risks/incidents/<int:id>", views.show_incident, name="show_incident"),
] ]

View file

@ -1,10 +1,14 @@
from django.contrib.admin.models import LogEntry
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated 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 .models import Risk, Control, ResidualRisk, AuditLog, Incident
from .serializers import ControlSerializer, RiskSerializer, ResidualRiskSerializer, UserSerializer, AuditSerializer, IncidentSerializer from .serializers import ControlSerializer, RiskSerializer, ResidualRiskSerializer, UserSerializer, AuditSerializer, IncidentSerializer
User = get_user_model()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# API # API
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -51,8 +55,6 @@ class ResidualRiskViewSet(viewsets.ModelViewSet):
serializer_class = ResidualRiskSerializer serializer_class = ResidualRiskSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
User = get_user_model()
class UserViewSet(viewsets.ReadOnlyModelViewSet): class UserViewSet(viewsets.ReadOnlyModelViewSet):
""" """
API endpoint for listing users and their responsibilities. API endpoint for listing users and their responsibilities.
@ -107,11 +109,85 @@ def dashboard(request):
def stats(request): def stats(request):
return render(request, "risks/statistics.html") return render(request, "risks/statistics.html")
def risks(request): def list_risks(request):
return render(request, "risks/list_risks.html") qs = Risk.objects.all().select_related("owner")
def controls(request): # GET-Parameter lesen
return render(request, "risks/list_controls.html") risk_id = request.GET.get("risk")
control_id = request.GET.get("control")
owner_id = request.GET.get("owner")
def incidents(request): if risk_id:
return render(request, "risks/list_incidents.html") 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 })

View file

@ -42,4 +42,20 @@
.home-icon { font-size: 20px; display: inline-block; margin: 10px; } .home-icon { font-size: 20px; display: inline-block; margin: 10px; }
/* Dropdown optisch näher am Screenshot */ /* Dropdown optisch näher am Screenshot */
.navbar-dropdown { border-top: none; box-shadow: 0 8px 16px rgba(0,0,0,.1); } .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;
}

View file

@ -14,7 +14,7 @@
<nav class="navbar topbar-nav" role="navigation" aria-label="main navigation"> <nav class="navbar topbar-nav" role="navigation" aria-label="main navigation">
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item logo" href="/risks/index"> <a class="navbar-item logo" href="/risks/index">
<strong class="logo-text">R</strong> <strong class="logo-text">RM</strong>
</a> </a>
<!-- Burger Menu für Mobile --> <!-- Burger Menu für Mobile -->
@ -38,17 +38,19 @@
<div class="navbar-item has-dropdown is-hoverable"> <div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">Risikomanagement</a> <a class="navbar-link">Risikomanagement</a>
<div class="navbar-dropdown"> <div class="navbar-dropdown">
<a class="navbar-item" href="/risks/risks">Risikoanalyse</a> <a class="navbar-item" href="/risks/list_risks">Risikoanalyse</a>
<a class="navbar-item" href="/risks/controls">Maßnahmen</a> <a class="navbar-item" href="/risks/list_controls">Maßnahmen</a>
<a class="navbar-item" href="/risks/incidents">Vorfälle</a> <a class="navbar-item" href="/risks/list_incidents">Vorfälle</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="navbar-end"> <div class="navbar-end">
<div class="actions"> <div class="actions">
<input type="text" placeholder="Suchen" class="search"> <input type="text" placeholder="Suchen" class="search">
<a class="navbar-item" href="/admin">Admin</a>
<div class="profile">KG</div> <div class="profile">KG</div>
<span class="icon"></span> <span class="icon"></span>
</div> </div>
@ -57,13 +59,22 @@
</header> </header>
<main class="content"> <main class="content">
<div class="home-icon"> {% block breadcrumbs %}
<a href="{% url 'risks:index' %}"> <nav class="breadcrumb top-breadcrumb" aria-label="breadcrumbs">
<span class="icon is-small"> <ul>
<i class="fas fa-home" aria-hidden="true" style="color: #6b2bbd"></i> <li>
</span> <a href="/risks/index">
</a> <span class="icon is-small">
</div> <i class="fas fa-home" aria-hidden="true" style="color: #6b2bbd"></i>
</span>
</a>
</li>
{% block crumbs %}
{% if title %} &rsaquo; {{ title }}{% endif %}
{% endblock %}
</ul>
</nav>
{% endblock %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
</body> </body>

View file

@ -0,0 +1,125 @@
{% extends "base.html" %}
{% block crumbs %}
<li><a href="{% url 'risks:list_controls' %}">Maßnahmen</a></li>
<li><a href="{% url 'risks:show_control' control.id %}">{{ control.title }}</a></li>
{% endblock %}
{% block content %}
<div class="container">
<section class="hero is-small">
<div class="hero-body">
<p class="title">Maßnahme: {{ control.title }}</p>
<p class="subtitle is-6">{{ control.description }}</p>
</div>
</section>
<!-- Überblick-->
<div class="card">
<header class="card-header">
<p class="card-header-title">Überblick</p>
</header>
<!-- Inhalt Überblick-->
<div class="card-content">
<div class="columns is-multiline">
<div class="column is-half">
<p>
<strong>Verknüpfte Risiken:</strong>
</p>
<p><strong><a>Zum Wiki Eintrag</a></strong></p>
</div>
<div class="column is-half">
<p><strong>Verantwortliche/r:</strong> {{ control.owner|default:"-" }}</p>
<p><strong>Erstellt am:</strong> {{ control.created_at|date:'d.m.Y H:i' }}</p>
<p><strong>Aktualisiert am:</strong> {{ control.updatet_at|date:'d.m.Y H:i' }}</p>
</div>
</div>
</div> <!-- Ende Inhalt Überblick -->
</div> <!-- Ende Überblick -->
<!-- Maßnahmen -->
<div class="card">
<header class="card-header">
<p class="card-header-title">Maßnahmen</p>
</header>
<div class="card-content">
{% if control.controls.all %}
<table class="table is-striped is-hoverable is-fullwidth">
<thead>
<tr>
<th>Titel</th>
<th>Status</th>
<th>Frist</th>
<th>Verantwortlicher</th>
<th>Link</th>
</tr>
</thead>
<tbody>
{% for control in control.controls.all %}
<tr onclick="window.location.href='/risks/controls/{{ control.id }}';" style="cursor:pointer;">
<td>{{ control.title }}</td>
<td>{{ control.get_status_display }}</td>
<td>
{% if control.due_date %}
{{ control.due_date|date:"d.m.Y" }}
{% else %}
{% endif %}
</td>
<td>
{% if control.responsible %}
{{ control.responsible.get_full_name|default:control.responsible.username }}
{% else %}
{% endif %}
</td>
<td>
{% if control.wiki_link %}
<a href="{{ control.wiki_link }}" target="_blank">🔗</a>
{% else %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="has-text-grey">Keine Maßnahmen erfasst.</p>
{% endif %}
</div>
</div>
<!-- Ende Maßnahmen -->
<!-- Historie -->
<div class="card">
<header class="card-header">
<p class="card-header-title">Historie</p>
</header>
<div class="card-content">
{% if logs %}
<table class="table is-striped is-fullwidth">
<thead>
<tr>
<th>Zeitpunkt</th>
<th>Benutzer</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td>{{ log.action_time|date:"d.m.Y H:i" }}</td>
<td>{{ log.user.get_full_name|default:log.user.username }}</td>
<td>{{ log.get_change_message }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="has-text-grey">Keine Historie vorhanden.</p>
{% endif %}
</div>
</div> <!-- Ende Historie -->
<br><br>
</div>
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block crumbs %}
<li><a href="{% url 'risks:list_incidents' %}">Vorfälle</a></li>
<li><a href="{% url 'risks:show_incident' incident.id %}">{{ incident.title }}</a></li>
{% endblock %}
{% block content %}
{% endblock %}

View file

@ -0,0 +1,224 @@
{% extends "base.html" %}
{% load risk_extras %}
{% block crumbs %}
<li><a href="{% url 'risks:list_risks' %}">Risikoanalyse</a></li>
<li><a href="{% url 'risks:show_risk' risk.id %}">{{ risk.title }}</a></li>
{% endblock %}
{% block content %}
<div class="container">
<section class="hero is-small">
<div class="hero-body">
<p class="title">Risiko: {{ risk.title }}</p>
<p class="subtitle is-6">{{ risk.description }}</p>
</div>
</section>
<!-- Überblick-->
<div class="card">
<header class="card-header">
<p class="card-header-title">Überblick</p>
</header>
<!-- Inhalt Überblick-->
<div class="card-content">
<div class="columns is-multiline">
<div class="column is-half">
<p><strong>Asset:</strong> {{ risk.asset|default:"-" }}</p>
<p><strong>Prozess:</strong> {{ risk.process|default:"-" }}</p>
<p>
<strong>Schutzziele:</strong>
{% if risk.cia %}
<ul>
{% for label in risk.cia %}
<li>{{ label|cia_label }}</li>
{% endfor %}
</ul>
{% else %}
<p>Noch nicht zugewiesen</p>
{% endif %}
</p>
</div>
<div class="column is-half">
<p><strong>Kategorie:</strong> {{ risk.category|default:"-" }}</p>
<p><strong>Risikoeigner:</strong> {{ risk.owner|default:"-" }}</p>
<p><strong>Erstellt am:</strong> {{ risk.created_at|date:'d.m.Y H:i' }}</p>
<p><strong>Aktualisiert am:</strong> {{ risk.updatet_at|date:'d.m.Y H:i' }}</p>
</div>
</div>
<!-- Risikobewertung -->
<h3>Risikobewertung</h3>
<div class="columns is-multiline">
<!-- Bruttorisiko -->
<div class="column is-half">
<div class="box">
<h4>Brutto (vor Maßnahmen)</h4>
<div class="columns is-multiline">
<div class="column is-half has-text-centered">
<p class="heading">Eintrittswahrscheinlichkeit</p>
<button class="button is-small is-info">
{{ risk.get_likelihood_display }}
</button>
</div>
<div class="column is-half has-text-centered">
<p class="heading">Schadensausmaß</p>
<button class="button is-small is-danger">
{{ risk.get_impact_display }}
</button>
</div>
<div class="column is-half has-text-centered">
<p class="heading">Stufe</p>
<button class="button is-small is-info">
{{ risk.level }}
</button>
</div>
<div class="column is-half has-text-centered">
<p class="heading">Score</p>
<button class="button is-small is-danger">
{{ risk.score }} / 25
</button>
</div>
</div>
</div>
</div> <!-- Ende Bruttorisiko -->
<!-- Nettorisiko -->
<div class="column is-half">
<div class="box">
<h4>Netto (nach Maßnahmen)</h4>
{% if risk.residualrisk %}
<div class="columns is-multiline">
<div class="column is-half has-text-centered">
<p class="heading">Eintrittswahrscheinlichkeit</p>
<button class="button is-small is-info">
{{ risk.residualrisk.get_likelihood_display }}
</button>
</div>
<div class="column is-half has-text-centered">
<p class="heading">Schadensausmaß</p>
<button class="button is-small is-danger">
{{ risk.residualrisk.get_impact_display }}
</button>
</div>
<div class="column is-half has-text-centered">
<p class="heading">Stufe</p>
<button class="button is-small is-info">
{{ risk.residualrisk.level }}
</button>
</div>
<div class="column is-half has-text-centered">
<p class="heading">Score</p>
<button class="button is-small is-danger">
{{ risk.residualrisk.score }} / 25
</button>
</div>
</div>
{% else %}
<p class="has-text-grey">Noch kein Nettorisiko erfasst.</p>
{% endif %}
</div>
</div> <!-- Ende Nettorisiko -->
</div> <!-- Ende Risikobewertung -->
</div> <!-- Ende Inhalt Überblick -->
</div> <!-- Ende Überblick -->
<!-- Maßnahmen -->
<div class="card">
<header class="card-header">
<p class="card-header-title">Maßnahmen</p>
</header>
<div class="card-content">
{% if risk.controls.all %}
<table class="table is-striped is-hoverable is-fullwidth">
<thead>
<tr>
<th>Titel</th>
<th>Status</th>
<th>Frist</th>
<th>Verantwortlicher</th>
<th>Link</th>
</tr>
</thead>
<tbody>
{% for control in risk.controls.all %}
<tr onclick="window.location.href='/risks/controls/{{ control.id }}';" style="cursor:pointer;">
<td>{{ control.title }}</td>
<td>{{ control.get_status_display }}</td>
<td>
{% if control.due_date %}
{{ control.due_date|date:"d.m.Y" }}
{% else %}
{% endif %}
</td>
<td>
{% if control.responsible %}
{{ control.responsible.get_full_name|default:control.responsible.username }}
{% else %}
{% endif %}
</td>
<td>
{% if control.wiki_link %}
<a href="{{ control.wiki_link }}" target="_blank">🔗</a>
{% else %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="has-text-grey">Keine Maßnahmen erfasst.</p>
{% endif %}
</div>
</div>
<!-- Ende Maßnahmen -->
<!-- Historie -->
<div class="card">
<header class="card-header">
<p class="card-header-title">Historie</p>
</header>
<div class="card-content">
{% if logs %}
<table class="table is-striped is-fullwidth">
<thead>
<tr>
<th>Zeitpunkt</th>
<th>Benutzer</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td>{{ log.action_time|date:"d.m.Y H:i" }}</td>
<td>{{ log.user.get_full_name|default:log.user.username }}</td>
<td>{{ log.get_change_message }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="has-text-grey">Keine Historie vorhanden.</p>
{% endif %}
</div>
</div> <!-- Ende Historie -->
<br><br>
</div>
{% endblock %}

View file

@ -1,7 +1,154 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block crumbs %} {% block crumbs %}
<li><a href="{% url 'risks:controls' %}">Maßnahmen</a></li> <li><a href="{% url 'risks:list_controls' %}">Maßnahmen</a></li>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
Maßnahmen <!-- Filter -->
<section class="section">
<div class="box">
<h2 class="title is-5">Auswahl</h2>
<form method="get">
<div class="columns is-multiline">
<!-- Maßnahmen -->
<div class="column is-3">
<div class="field">
<label class="label">Maßnahme</label>
<div class="control">
<div class="select is-fullwidth">
<select name="control" onchange="this.form.submit()">
<option value="">Alle</option>
{% for c in controls %}
<option value="{{ c.id }}" {% if request.GET.control == c.id|stringformat:"s" %}selected{% endif %}>
{{ c.title }}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
<!-- Risiko -->
<div class="column is-3">
<div class="field">
<label class="label">Risiko</label>
<div class="control">
<div class="select is-fullwidth">
<select name="risk" onchange="this.form.submit()">
<option value="">Alle</option>
{% for r in risks %}
<option value="{{ r.id }}" {% if request.GET.risk == r.id|stringformat:"s" %}selected{% endif %}>
{{ r.title }}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
<!-- Status -->
<div class="column is-3">
<div class="field">
<label class="label">Status</label>
<div class="control">
<div class="select is-fullwidth">
<select name="status" onchange="this.form.submit()">
<option value="">Alle</option>
{% for key,label in status_choices %}
<option value="{{ key }}" {% if request.GET.status == key %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
<!-- Verantwortliche/r -->
<div class="column is-3">
<div class="field">
<label class="label">Verantwortliche/r</label>
<div class="control">
<div class="select is-fullwidth">
<select name="responsible" onchange="this.form.submit()">
<option value="">Alle</option>
{% for u in users %}
<option value="{{ u.id }}" {% if request.GET.responsible == u.id|stringformat:"s" %}selected{% endif %}>
{{ u.get_full_name|default:u.username }}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
</div>
</form>
<h2 class="title is-5">Maßnahmen</h2>
<div class="table-container">
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
<thead>
<tr>
<th>Maßnahme</th>
<th>Risiken</th>
<th>Verantwortliche/r</th>
<th>Status</th>
<th>Frist</th>
<th>Link</th>
</tr>
</thead>
<tbody>
{% for c in controls %}
<tr onclick="window.location.href='{% url 'risks:show_control' c.id %}'" style="cursor:pointer;">
<td>{{ c.title }}</td>
<td>
{% if c.risk %}
<a href="{% url 'risks:show_risk' c.risk.id %}" onclick="event.stopPropagation();">
{{ c.risk.title }}
</a>
{% else %}
{% endif %}
</td>
<td>
{% if c.responsible %}
{{ c.responsible.get_full_name|default:c.responsible.username }}
{% else %}
{% endif %}
</td>
<td>{{ c.get_status_display }}</td>
<td>
{% if c.due_date %}
{{ c.due_date|date:"d.m.Y" }}
{% else %}
{% endif %}
</td>
<td>
{% if c.wiki_link %}
<a href="{{ c.wiki_link }}" target="_blank">🔗</a>
{% else %}
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="has-text-centered has-text-grey">Keine Maßnahmen gefunden</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</section>
{% endblock %} {% endblock %}

View file

@ -1,7 +1,113 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block crumbs %} {% block crumbs %}
<li><a href="{% url 'risks:incidents' %}">Vorfälle</a></li> <li><a href="{% url 'risks:list_incidents' %}">Vorfälle</a></li>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
Vorfälle <!-- Filter -->
<section class="section">
<div class="box">
<h2 class="title is-5">Auswahl</h2>
<div class="columns is-multiline">
<!-- Vorfälle -->
<div class="column is-3">
<div class="field">
<label class="label">Vorfall</label>
<div class="control">
<div class="select is-fullwidth">
<select>
<option>Alle</option>
<option>Vorfall A</option>
<option>Vorfall B</option>
</select>
</div>
</div>
</div>
</div>
<!-- Risiko -->
<div class="column is-3">
<div class="field">
<label class="label">Risiko</label>
<div class="control">
<div class="select is-fullwidth">
<select>
<option>Alle</option>
<option>Risiko 1</option>
<option>Risiko 2</option>
</select>
</div>
</div>
</div>
</div>
<!-- Status -->
<div class="column is-3">
<div class="field">
<label class="label">Status</label>
<div class="control">
<div class="select is-fullwidth">
<select>
<option>All</option>
<option>Opened</option>
<option>In progress</option>
<option>Closed</option>
</select>
</div>
</div>
</div>
</div>
<!-- Melder -->
<div class="column is-3">
<div class="field">
<label class="label">Meldende Person</label>
<div class="control">
<div class="select is-fullwidth">
<select>
<option>Alle</option>
<option>Kevin Heyer</option>
<option>Stefan Lange</option>
<option>Kirsten Herzhoff</option>
</select>
</div>
</div>
</div>
</div>
</div>
<h2 class="title is-5">Vorfälle</h2>
<div class="table-container">
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
<thead>
<tr>
<th>Vorfall</th>
<th>Zugehörige Risiken</th>
<th>Status</th>
<th>Gemeldet am</th>
<th>Gemeldet von</th>
</tr>
</thead>
<tbody>
<tr onclick="window.location.href='/risks/incidents/1';" style="cursor:pointer;">
<td>Switch entwendet</td>
<td>
<ul>
<li>
<a href="/risks/risks/1" onclick="event.stopPropagation();">Hardware Diebstahl</a>
</li>
</ul>
</td>
<td>Closed</td>
<td>08.09.2025</td>
<td>Kevin Heyer</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
{% endblock %} {% endblock %}

View file

@ -1,7 +1,125 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block crumbs %} {% block crumbs %}
<li><a href="{% url 'risks:risks' %}">Risiken</a></li> <li><a href="{% url 'risks:list_risks' %}">Risikoanalyse</a></li>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
Risiken <section class="section">
<div class="box">
<h2 class="title is-5">Auswahl</h2>
<!-- Filter -->
<form method="get">
<div class="columns is-multiline">
<!-- Risiko Filter -->
<div class="column is-3">
<div class="field">
<label class="label">Risiko</label>
<div class="control">
<div class="select is-fullwidth">
<select name="risk" onchange="this.form.submit()">
<option value="">Alle</option>
{% for r in risks %}
<option value="{{ r.id }}" {% if request.GET.risk == r.id|stringformat:"s" %}selected{% endif %}>
{{ r.title }}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
<!-- Maßnahmen Filter -->
<div class="column is-3">
<div class="field">
<label class="label">Maßnahmen</label>
<div class="control">
<div class="select is-fullwidth">
<select name="control" onchange="this.form.submit()">
<option value="">Alle</option>
{% for c in controls %}
<option value="{{ c.id }}" {% if request.GET.control == c.id|stringformat:"s" %}selected{% endif %}>
{{ c.title }}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
<!-- Risikoeigner Filter -->
<div class="column is-3">
<div class="field">
<label class="label">Risikoeigner</label>
<div class="control">
<div class="select is-fullwidth">
<select name="owner" onchange="this.form.submit()">
<option value="">Alle</option>
{% for u in owners %}
<option value="{{ u.id }}" {% if request.GET.owner == u.id|stringformat:"s" %}selected{% endif %}>
{{ u.get_full_name|default:u.username }}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
</div>
</form>
<h2 class="title is-5">Risiken</h2>
<!-- Risiken -->
<div class="table-container">
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
<thead>
<tr>
<th>Risiko</th>
<th>Asset / Prozes</th>
<th>Kategorie</th>
<th>Eintritt</th>
<th>Schaden</th>
<th>Score</th>
<th>Stufe</th>
<th>Risikoeigner</th>
</tr>
</thead>
<tbody>
{% for r in risks %}
<tr onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">
<td>{{ r.title }}</td>
<td>
{{ r.asset }}
{% if r.process %}
<br><small>{{ r.process }}</small>
{% endif %}
</td>
<td>{{ r.category }}</td>
<td>{{ r.get_likelihood_display }}</td>
<td>{{ r.get_impact_display }}</td>
<td>{{ r.score }}</td>
<td>{{ r.level }}</td>
<td>
{% if r.owner %}
{{ r.owner.get_full_name|default:r.owner.username }}
{% else %}
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="has-text-centered has-text-grey">Keine Risiken vorhanden</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> <!-- Ende Risiken -->
</div>
</section>
{% endblock %} {% endblock %}