feat: Enhance risk management application with user auditing and improved incident handling

- Added AuditUserMiddleware to track the current user for auditing purposes.
- Introduced audit_context for managing the current user in thread-local storage.
- Updated Control and Incident models to include created_at and updated_at timestamps.
- Refactored Control and Incident serializers to handle related risks and timestamps.
- Modified views to set the _changed_by attribute for user actions.
- Enhanced incident listing and detail views to display related risks and user actions.
- Updated templates for better presentation of risks and incidents.
- Added migrations for new fields and relationships in the database.
- Improved filtering options in the incident list view.
This commit is contained in:
Kevin Heyer 2025-09-09 12:00:29 +02:00
parent 43e86d0357
commit 686030e4cb
25 changed files with 540 additions and 171 deletions

View file

@ -50,6 +50,7 @@ MIDDLEWARE = [
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"risks.middleware.AuditUserMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]

View file

@ -18,8 +18,7 @@ urlpatterns = [
path("api/ping/", ping), # Public healthcheck endpoint path("api/ping/", ping), # Public healthcheck endpoint
path("api/secure-ping/", secure_ping), # Protected API endpoint path("api/secure-ping/", secure_ping), # Protected API endpoint
path("api/", include(router.urls)), path("api/", include(router.urls)),
path("", include("risks.urls")), path("", include("risks.urls", namespace="risks")),
path("risks/", include("risks.urls")),
] ]
# Add OIDC routes only if Single Sign-On is enabled # Add OIDC routes only if Single Sign-On is enabled

Binary file not shown.

View file

@ -19,13 +19,6 @@ class UserAdmin(BaseUserAdmin):
return obj.controls_responsible.count() return obj.controls_responsible.count()
responsible_controls_count.short_description = "Controls Responsible" responsible_controls_count.short_description = "Controls Responsible"
class ControlInline(admin.TabularInline):
model = Control
extra = 1
fields = ("title", "status", "due_date", "responsible", "wiki_link")
autocomplete_fields = ("responsible",)
class ResidualRiskInline(admin.StackedInline): class ResidualRiskInline(admin.StackedInline):
""" """
Inline editor for ResidualRisk, linked one-to-one with Risk Inline editor for ResidualRisk, linked one-to-one with Risk
@ -36,6 +29,11 @@ class ResidualRiskInline(admin.StackedInline):
readonly_fields = ("score", "level", "review_required") readonly_fields = ("score", "level", "review_required")
fields = ("likelihood", "impact", "score", "level", "review_required") fields = ("likelihood", "impact", "score", "level", "review_required")
class ControlRisksInline(admin.TabularInline):
model = Control.risks.through
fk_name = "risk"
extra = 1
autocomplete_fields = ("control",)
@admin.register(Risk) @admin.register(Risk)
class RiskAdmin(admin.ModelAdmin): class RiskAdmin(admin.ModelAdmin):
@ -50,7 +48,7 @@ class RiskAdmin(admin.ModelAdmin):
) )
list_filter = ("level", "likelihood", "impact", "owner") list_filter = ("level", "likelihood", "impact", "owner")
search_fields = ("title", "asset", "process", "category") search_fields = ("title", "asset", "process", "category")
inlines = [ControlInline, ResidualRiskInline] inlines = [ResidualRiskInline, ControlRisksInline] # Controls hier verknüpfen
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
obj._changed_by = request.user obj._changed_by = request.user
@ -82,10 +80,10 @@ class ResidualRiskAdmin(admin.ModelAdmin):
@admin.register(Control) @admin.register(Control)
class ControlAdmin(admin.ModelAdmin): class ControlAdmin(admin.ModelAdmin):
list_display = ("title", "status", "due_date", "responsible", "risk") list_display = ("title", "status", "due_date", "responsible")
list_filter = ("status", "due_date") list_filter = ("status", "due_date")
autocomplete_fields = ("risks", "responsible",)
search_fields = ("title", "description") search_fields = ("title", "description")
autocomplete_fields = ("responsible", "risk")
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
obj._changed_by = request.user obj._changed_by = request.user

8
risks/audit_context.py Normal file
View file

@ -0,0 +1,8 @@
import threading
_local = threading.local()
def set_current_user(user):
_local.user = user
def get_current_user():
return getattr(_local, "user", None)

9
risks/middleware.py Normal file
View file

@ -0,0 +1,9 @@
from .audit_context import set_current_user
class AuditUserMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
set_current_user(getattr(request, "user", None))
return self.get_response(request)

View file

@ -0,0 +1,52 @@
# Generated by Django 5.2.6 on 2025-09-09 07:00
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("risks", "0012_alter_residualrisk_impact_and_more"),
]
operations = [
migrations.AddField(
model_name="control",
name="created_at",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="control",
name="updatet_at",
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name="incident",
name="created_at",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="incident",
name="updatet_at",
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name="residualrisk",
name="created_at",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="residualrisk",
name="updatet_at",
field=models.DateTimeField(auto_now=True),
),
]

View file

@ -0,0 +1,21 @@
# Generated by Django 5.2.6 on 2025-09-09 07:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("risks", "0013_control_created_at_control_updatet_at_and_more"),
]
operations = [
migrations.RemoveField(
model_name="control",
name="risk",
),
migrations.AddField(
model_name="control",
name="risks",
field=models.ManyToManyField(related_name="controls", to="risks.risk"),
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 5.2.6 on 2025-09-09 08:37
import risks.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("risks", "0014_remove_control_risk_control_risks"),
]
operations = [
migrations.AlterField(
model_name="auditlog",
name="changes",
field=models.JSONField(
blank=True, encoder=risks.models.SafeJSONEncoder, null=True
),
),
]

View file

@ -0,0 +1,32 @@
# Generated by Django 5.2.6 on 2025-09-09 09:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("risks", "0015_alter_auditlog_changes"),
]
operations = [
migrations.RenameField(
model_name="control",
old_name="updatet_at",
new_name="updated_at",
),
migrations.RenameField(
model_name="incident",
old_name="updatet_at",
new_name="updated_at",
),
migrations.RenameField(
model_name="residualrisk",
old_name="updatet_at",
new_name="updated_at",
),
migrations.RenameField(
model_name="risk",
old_name="updatet_at",
new_name="updated_at",
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 5.2.6 on 2025-09-09 09:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("risks", "0016_rename_updatet_at_control_updated_at_and_more"),
]
operations = [
migrations.AlterField(
model_name="incident",
name="status",
field=models.CharField(
choices=[
("open", "Opened"),
("in_progress", "In Progress"),
("closed", "Closed"),
],
max_length=12,
),
),
]

View file

@ -1,7 +1,16 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models from django.db import models
from multiselectfield import MultiSelectField from multiselectfield import MultiSelectField
import datetime
import json
class SafeJSONEncoder(DjangoJSONEncoder):
def default(self, obj):
if isinstance(obj, datetime.date):
return obj.isoformat()
return super().default(obj)
class User(AbstractUser): class User(AbstractUser):
""" """
@ -52,7 +61,7 @@ class Risk(models.Model):
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,) created_at = models.DateTimeField(auto_now_add=True,)
updatet_at = models.DateTimeField(auto_now=True) updated_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)
@ -127,12 +136,15 @@ class ResidualRisk(models.Model):
review_required = models.BooleanField(default=False) review_required = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True,)
updated_at = models.DateTimeField(auto_now=True)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# Load previous state (if it exists) # Load previous state (if it exists)
if self.pk: if self.pk:
old = ResidualRisk.objects.get(pk=self.pk) old = ResidualRisk.objects.get(pk=self.pk)
if old.likelihood != self.likelihood or old.impact != self.impact: if old.likelihood != self.likelihood or old.impact != self.impact:
self.review_required = False self.review_required = True
self.score = self.likelihood * self.impact self.score = self.likelihood * self.impact
@ -174,9 +186,11 @@ class Control(models.Model):
) )
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
wiki_link = models.URLField(blank=True, null=True) wiki_link = models.URLField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True,)
updated_at = models.DateTimeField(auto_now=True)
# Relation to risk # Relation to risk
risk = models.ForeignKey(Risk, on_delete=models.CASCADE, related_name="controls") risks = models.ManyToManyField("Risk", related_name="controls")
def __str__(self): def __str__(self):
return f"{self.title} ({self.get_status_display()})" return f"{self.title} ({self.get_status_display()})"
@ -201,7 +215,7 @@ class AuditLog(models.Model):
action = models.CharField(max_length=10, choices=ACTION_CHOICES) action = models.CharField(max_length=10, choices=ACTION_CHOICES)
model = models.CharField(max_length=100) model = models.CharField(max_length=100)
object_id = models.CharField(max_length=50) object_id = models.CharField(max_length=50)
changes = models.JSONField(null=True, blank=True) changes = models.JSONField(null=True, blank=True, encoder=SafeJSONEncoder)
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
@ -214,7 +228,7 @@ class Incident(models.Model):
STATUS_CHOICES = [ STATUS_CHOICES = [
("open", "Opened"), ("open", "Opened"),
("in_progress", "In Progress"), ("in_progress", "In Progress"),
("close", "Closed"), ("closed", "Closed"),
] ]
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
@ -222,6 +236,8 @@ class Incident(models.Model):
reported_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="incidents") reported_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="incidents")
status = models.CharField(max_length=12, choices=STATUS_CHOICES) status = models.CharField(max_length=12, choices=STATUS_CHOICES)
related_risks = models.ManyToManyField("Risk", blank=True, related_name="incidents") related_risks = models.ManyToManyField("Risk", blank=True, related_name="incidents")
created_at = models.DateTimeField(auto_now_add=True,)
updated_at = models.DateTimeField(auto_now=True)
class Notification(models.Model): class Notification(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="notifications") user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="notifications")

View file

@ -18,17 +18,21 @@ class ResidualRiskSerializer(serializers.ModelSerializer):
class ControlSerializer(serializers.ModelSerializer): class ControlSerializer(serializers.ModelSerializer):
risks = serializers.PrimaryKeyRelatedField(many=True, queryset=Risk.objects.all())
class Meta: class Meta:
model = Control model = Control
fields = [ fields = [
"id", "id",
"title", "title",
"status", "status",
"created_at",
"updated_at",
"due_date", "due_date",
"responsible", "responsible",
"description", "description",
"wiki_link", "wiki_link",
"risk", "risks",
] ]
class RiskSerializer(serializers.ModelSerializer): class RiskSerializer(serializers.ModelSerializer):
@ -44,16 +48,14 @@ class RiskSerializer(serializers.ModelSerializer):
"process", "process",
"category", "category",
"created_at", "created_at",
"updatet_at", "updated_at",
"likelihood", "likelihood",
"impact", "impact",
"score", "score",
"level", "level",
"owner", "owner",
"follow_up", "follow_up",
"confidentiality", "cia",
"integrity",
"availability",
"controls", "controls",
] ]
@ -93,15 +95,30 @@ class RiskSummarySerializer(serializers.ModelSerializer):
fields = ["id", "title", "score", "level"] fields = ["id", "title", "score", "level"]
class IncidentSerializer(serializers.ModelSerializer): class IncidentSerializer(serializers.ModelSerializer):
related_risks = RiskSummarySerializer(many=True, read_only=True) related_risks = serializers.PrimaryKeyRelatedField(
many=True, queryset=Risk.objects.all()
)
date_reported = serializers.DateField(format="%Y-%m-%d", required=False)
created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
class Meta: class Meta:
model = Incident model = Incident
fields = [ fields = [
"id", "id", "title", "description", "date_reported",
"title", "created_at", "updated_at", "status", "related_risks",
"description",
"date_reported",
"status",
"related_risks",
] ]
def create(self, validated_data):
risks = validated_data.pop("related_risks", [])
obj = super().create(validated_data)
if risks:
obj.related_risks.set(risks)
return obj
def update(self, instance, validated_data):
risks = validated_data.pop("related_risks", None)
obj = super().update(instance, validated_data)
if risks is not None:
obj.related_risks.set(risks)
return obj

View file

@ -2,6 +2,7 @@ from datetime import date, datetime
from django.db.models import Model 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 .audit_context import get_current_user
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
@ -21,8 +22,6 @@ def serialize_value(value):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@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),
@ -39,7 +38,6 @@ def log_risk_save(sender, instance, created, **kwargs):
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 = { clean_changes = {
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])} field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
@ -50,7 +48,7 @@ def log_risk_save(sender, instance, created, **kwargs):
action="update", action="update",
model="Risk", model="Risk",
object_id=instance.pk, object_id=instance.pk,
changes=changes, changes=clean_changes,
) )
@receiver(post_delete, sender=Risk) @receiver(post_delete, sender=Risk)
@ -58,8 +56,9 @@ def log_risk_delete(sender, instance, **kwargs):
""" """
Signal that runs after a Risk is deleted. Signal that runs after a Risk is deleted.
""" """
user = getattr(instance, "_changed_by", None) or get_current_user()
AuditLog.objects.create( AuditLog.objects.create(
user=getattr(instance, "_changed_by", None), user=user,
action="delete", action="delete",
model="Risk", model="Risk",
object_id=instance.pk, object_id=instance.pk,
@ -88,7 +87,6 @@ def log_control_save(sender, instance, created, **kwargs):
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 = { clean_changes = {
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])} field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
@ -99,35 +97,39 @@ def log_control_save(sender, instance, created, **kwargs):
action="update", action="update",
model="Control", model="Control",
object_id=instance.pk, object_id=instance.pk,
changes=changes, changes=clean_changes,
) )
@receiver(post_delete, sender=Control) @receiver(post_delete, sender=Control)
def log_control_delete(sender, instance, **kwargs): def log_control_delete(sender, instance, **kwargs):
user = getattr(instance, "_changed_by", None) or get_current_user()
AuditLog.objects.create( AuditLog.objects.create(
user=getattr(instance, "_changed_by", None), user=user,
action="delete", action="delete",
model="Control", model="Control",
object_id=instance.pk, object_id=instance.pk,
changes=None, changes=None,
) )
@receiver(post_save, sender=Control) @receiver(m2m_changed, sender=Control.risks.through)
def update_residual_risk_on_control_change(sender, instance, **kwargs): def control_risks_changed(sender, instance, action, reverse, pk_set, **kwargs):
""" if action in {"post_add", "post_remove", "post_clear"}:
Whenever a control is saved, check if the related risk has a residual risk. if action == "post_clear":
If a control is completed or verified, flag the residual risk for review. affected_risks = instance.risks.all()
""" elif pk_set:
if reverse:
from .models import Risk
affected_risks = Risk.objects.filter(pk__in=pk_set)
else:
affected_risks = Risk.objects.filter(pk__in=pk_set)
else:
affected_risks = instance.risks.all()
risk = instance.risk from .models import ResidualRisk
for risk in affected_risks:
# Ensure residual risk exists residual, _ = ResidualRisk.objects.get_or_create(risk=risk)
residual, created = ResidualRisk.objects.get_or_create(risk=risk) residual.review_required = True
residual.save()
# 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 # Residual risks
@ -151,7 +153,6 @@ def log_residual_save(sender, instance, created, **kwargs):
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 = { clean_changes = {
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])} field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
@ -167,8 +168,9 @@ def log_residual_save(sender, instance, created, **kwargs):
@receiver(post_delete, sender=ResidualRisk) @receiver(post_delete, sender=ResidualRisk)
def log_residual_delete(sender, instance, **kwargs): def log_residual_delete(sender, instance, **kwargs):
user = getattr(instance, "_changed_by", None) or get_current_user()
AuditLog.objects.create( AuditLog.objects.create(
user=getattr(instance, "_changed_by", None), user=user,
action="delete", action="delete",
model="ResidualRisk", model="ResidualRisk",
object_id=instance.pk, object_id=instance.pk,
@ -187,12 +189,16 @@ def log_incident_save(sender, instance, created, **kwargs):
action="create", action="create",
model="Incident", model="Incident",
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 = Incident.objects.get(pk=instance.pk) old = Incident.objects.get(pk=instance.pk)
changes = model_diff(old, instance) changes = model_diff(old, instance)
if changes: if changes:
clean_changes = { clean_changes = {
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])} field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
@ -203,14 +209,15 @@ def log_incident_save(sender, instance, created, **kwargs):
action="update", action="update",
model="Incident", model="Incident",
object_id=instance.pk, object_id=instance.pk,
changes=changes, changes=clean_changes,
) )
@receiver(m2m_changed, sender=Incident.related_risks.through) @receiver(m2m_changed, sender=Incident.related_risks.through)
def log_incident_risks_change(sender, instance, action, reverse, model, pk_set, **kwargs): def log_incident_risks_change(sender, instance, action, reverse, model, pk_set, **kwargs):
if action in ["post_add", "post_remove", "post_clear"]: if action in ["post_add", "post_remove", "post_clear"]:
user = getattr(instance, "_changed_by", None) or get_current_user()
AuditLog.objects.create( AuditLog.objects.create(
user=getattr(instance, "_changed_by", None), user=user,
action="update", action="update",
model="Incident", model="Incident",
object_id=instance.pk, object_id=instance.pk,
@ -219,8 +226,9 @@ def log_incident_risks_change(sender, instance, action, reverse, model, pk_set,
@receiver(post_delete, sender=Incident) @receiver(post_delete, sender=Incident)
def log_incident_delete(sender, instance, **kwargs): def log_incident_delete(sender, instance, **kwargs):
user = getattr(instance, "_changed_by", None) or get_current_user()
AuditLog.objects.create( AuditLog.objects.create(
user=getattr(instance, "_changed_by", None), user=user,
action="delete", action="delete",
model="Incident", model="Incident",
object_id=instance.pk, object_id=instance.pk,

View file

@ -24,12 +24,10 @@ class RiskViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer): def perform_create(self, serializer):
instance = serializer.save() instance = serializer.save()
instance._changed_by = self.request.user instance._changed_by = self.request.user
instance.save()
def perform_update(self, serializer): def perform_update(self, serializer):
instance = serializer.save() instance = serializer.save()
instance._changed_by = self.request.user instance._changed_by = self.request.user
instance.save()
class ControlViewSet(viewsets.ModelViewSet): class ControlViewSet(viewsets.ModelViewSet):
""" """
@ -43,12 +41,10 @@ class ControlViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer): def perform_create(self, serializer):
instance = serializer.save() instance = serializer.save()
instance._changed_by = self.request.user instance._changed_by = self.request.user
instance.save()
def perform_update(self, serializer): def perform_update(self, serializer):
instance = serializer.save() instance = serializer.save()
instance._changed_by = self.request.user instance._changed_by = self.request.user
instance.save()
class ResidualRiskViewSet(viewsets.ModelViewSet): class ResidualRiskViewSet(viewsets.ModelViewSet):
queryset = ResidualRisk.objects.all() queryset = ResidualRisk.objects.all()
@ -66,12 +62,10 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
def perform_create(self, serializer): def perform_create(self, serializer):
instance = serializer.save() instance = serializer.save()
instance._changed_by = self.request.user instance._changed_by = self.request.user
instance.save()
def perform_update(self, serializer): def perform_update(self, serializer):
instance = serializer.save() instance = serializer.save()
instance._changed_by = self.request.user instance._changed_by = self.request.user
instance.save()
class AuditViewSet(viewsets.ReadOnlyModelViewSet): class AuditViewSet(viewsets.ReadOnlyModelViewSet):
""" """
@ -92,12 +86,10 @@ class IncidentViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer): def perform_create(self, serializer):
instance = serializer.save(reported_by=self.request.user) instance = serializer.save(reported_by=self.request.user)
instance._changed_by = self.request.user instance._changed_by = self.request.user
instance.save()
def perform_update(self, serializer): def perform_update(self, serializer):
instance = serializer.save() instance = serializer.save()
instance._changed_by = self.request.user instance._changed_by = self.request.user
instance.save()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Web # Web
@ -146,9 +138,8 @@ def show_risk(request, id):
return render(request, "risks/item_risk.html", {"risk": risk, "logs": logs}) return render(request, "risks/item_risk.html", {"risk": risk, "logs": logs})
def list_controls(request): def list_controls(request):
qs = Control.objects.all().select_related("risk", "responsible") qs = Control.objects.all().select_related("responsible")
# Filter
control_id = request.GET.get("control") control_id = request.GET.get("control")
risk_id = request.GET.get("risk") risk_id = request.GET.get("risk")
status = request.GET.get("status") status = request.GET.get("status")
@ -157,13 +148,13 @@ def list_controls(request):
if control_id: if control_id:
qs = qs.filter(id=control_id) qs = qs.filter(id=control_id)
if risk_id: if risk_id:
qs = qs.filter(risk_id=risk_id) qs = qs.filter(risks__id=risk_id) # FIX
if status: if status:
qs = qs.filter(status=status) qs = qs.filter(status=status)
if responsible_id: if responsible_id:
qs = qs.filter(responsible_id=responsible_id) qs = qs.filter(responsible_id=responsible_id)
controls = qs.order_by("title") controls = qs.order_by("title").distinct()
risks = Risk.objects.all().order_by("title") risks = Risk.objects.all().order_by("title")
users = User.objects.filter(responsible_controls__isnull=False).distinct().order_by("username") users = User.objects.filter(responsible_controls__isnull=False).distinct().order_by("username")
@ -185,9 +176,39 @@ def show_control(request, id):
return render(request, "risks/item_control.html", {"control": control, "logs": logs}) return render(request, "risks/item_control.html", {"control": control, "logs": logs})
def list_incidents(request): def list_incidents(request):
return render(request, "risks/list_incidents.html") qs = Incident.objects.all().select_related("reported_by").prefetch_related("related_risks")
risk_id = request.GET.get("risk")
status = request.GET.get("status")
reported_by = request.GET.get("reported_by")
if risk_id:
qs = qs.filter(related_risks__id=risk_id) # FIX
if status:
qs = qs.filter(status=status)
if reported_by:
qs = qs.filter(reported_by=reported_by)
incidents = qs.order_by("title").distinct()
risks = Risk.objects.all().order_by("title")
users = User.objects.filter(incidents__isnull=False).distinct().order_by("username") # sinnvoller
return render(request, "risks/list_incidents.html", {
"incidents": incidents,
"risks": risks,
"users": users,
"status_choices": Incident.STATUS_CHOICES,
})
def show_incident(request, id): def show_incident(request, id):
incident = Incident.objects.get(pk=id) incident = get_object_or_404(Incident, pk=id)
return render(request, "risks/item_incident.html", {"incident": incident }) ct = ContentType.objects.get_for_model(Incident)
logs = LogEntry.objects.filter(
content_type=ct,
object_id=incident.pk
).order_by("-action_time")
return render(request, "risks/item_incident.html", {"incident": incident, "logs": logs})

View file

@ -20,59 +20,63 @@
<div class="card-content"> <div class="card-content">
<div class="columns is-multiline"> <div class="columns is-multiline">
<div class="column is-half"> <div class="column is-half">
<p> <p><strong>Verantwortliche/r:</strong> {{ control.responsible|default:"-" }}</p>
<strong>Verknüpfte Risiken:</strong>
</p>
<p><strong><a>Zum Wiki Eintrag</a></strong></p> <p><strong><a>Zum Wiki Eintrag</a></strong></p>
</div> </div>
<div class="column is-half"> <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>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> <p><strong>Aktualisiert am:</strong> {{ control.updated_at|date:'d.m.Y H:i' }}</p>
</div> </div>
</div> </div>
</div> <!-- Ende Inhalt Überblick --> </div> <!-- Ende Inhalt Überblick -->
</div> <!-- Ende Überblick --> </div> <!-- Ende Überblick -->
<!-- Maßnahmen --> <!-- Risiken -->
<div class="card"> <div class="card">
<header class="card-header"> <header class="card-header">
<p class="card-header-title">Maßnahmen</p> <p class="card-header-title">Verknüpfte Risiken</p>
</header> </header>
<div class="card-content"> <div class="card-content">
{% if control.controls.all %} {% if control.risks %}
<table class="table is-striped is-hoverable is-fullwidth"> <table class="table is-striped is-hoverable is-fullwidth">
<thead> <thead>
<tr> <tr>
<th>Titel</th> <th>Titel</th>
<th>Status</th> <th>Risikoeigner</th>
<th>Frist</th> <th>Kategorie</th>
<th>Verantwortlicher</th> <th>Asset</th>
<th>Link</th> <th>Prozess</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for control in control.controls.all %} {% for risk in control.risks.all %}
<tr onclick="window.location.href='/risks/controls/{{ control.id }}';" style="cursor:pointer;"> <tr onclick="window.location.href='/risks/risks/{{ risk.id }}';" style="cursor:pointer;">
<td>{{ control.title }}</td> <td>{{ risk.title }}</td>
<td>{{ control.get_status_display }}</td>
<td> <td>
{% if control.due_date %} {% if risk.owner %}
{{ control.due_date|date:"d.m.Y" }} {{ risk.owner }}
{% else %} {% else %}
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if control.responsible %} {% if risk.category %}
{{ control.responsible.get_full_name|default:control.responsible.username }} {{ risk.category }}
{% else %} {% else %}
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if control.wiki_link %} {% if risk.asset %}
<a href="{{ control.wiki_link }}" target="_blank">🔗</a> {{ risk.asset }}
{% else %}
{% endif %}
</td>
<td>
{% if risk.process %}
{{ risk.process }}
{% else %} {% else %}
{% endif %} {% endif %}
@ -82,7 +86,7 @@
</tbody> </tbody>
</table> </table>
{% else %} {% else %}
<p class="has-text-grey">Keine Maßnahmen erfasst.</p> <p class="has-text-grey">Keine Verknüpften Risiken.</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View file

@ -4,5 +4,126 @@
<li><a href="{% url 'risks:show_incident' incident.id %}">{{ incident.title }}</a></li> <li><a href="{% url 'risks:show_incident' incident.id %}">{{ incident.title }}</a></li>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container">
<section class="hero is-small">
<div class="hero-body">
<p class="title">Vorfall: {{ incident.title }}</p>
<p class="subtitle is-6">{{ incident.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>Gemeldet von:</strong> {{ incident.reported_by|default:"-" }}</p>
<p><strong>Gemeldet am:</strong> {{ incident.date_reported|date:'d.m.Y' }}</p>
<p><strong>Status:</strong> {{ incident.status }}</p>
</div>
<div class="column is-half">
<p><strong>Erstellt am:</strong> {{ incident.created_at|date:'d.m.Y H:i' }}</p>
<p><strong>Aktualisiert am:</strong> {{ incident.updated_at|date:'d.m.Y H:i' }}</p>
</div>
</div>
</div> <!-- Ende Inhalt Überblick -->
</div> <!-- Ende Überblick -->
<!-- Risiken -->
<div class="card">
<header class="card-header">
<p class="card-header-title">Zugehörige Risiken</p>
</header>
<div class="card-content">
{% if incident.related_risks %}
<table class="table is-striped is-hoverable is-fullwidth">
<thead>
<tr>
<th>Titel</th>
<th>Risikoeigner</th>
<th>Kategorie</th>
<th>Asset</th>
<th>Prozess</th>
</tr>
</thead>
<tbody>
{% for risk in incident.related_risks.all %}
<tr onclick="window.location.href='/risks/risks/{{ risk.id }}';" style="cursor:pointer;">
<td>{{ risk.title }}</td>
<td>
{% if risk.owner %}
{{ risk.owner }}
{% else %}
{% endif %}
</td>
<td>
{% if risk.category %}
{{ risk.category }}
{% else %}
{% endif %}
</td>
<td>
{% if risk.asset %}
{{ risk.asset }}
{% else %}
{% endif %}
</td>
<td>
{% if risk.process %}
{{ risk.process }}
{% else %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="has-text-grey">Keine Verknüpften Risiken.</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 %} {% endblock %}

View file

@ -40,7 +40,7 @@
<p><strong>Kategorie:</strong> {{ risk.category|default:"-" }}</p> <p><strong>Kategorie:</strong> {{ risk.category|default:"-" }}</p>
<p><strong>Risikoeigner:</strong> {{ risk.owner|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>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> <p><strong>Aktualisiert am:</strong> {{ risk.updated_at|date:'d.m.Y H:i' }}</p>
</div> </div>
</div> </div>
<!-- Risikobewertung --> <!-- Risikobewertung -->

View file

@ -8,74 +8,86 @@
<div class="box"> <div class="box">
<h2 class="title is-5">Auswahl</h2> <h2 class="title is-5">Auswahl</h2>
<div class="columns is-multiline"> <form method="get">
<div class="columns is-multiline">
<!-- Vorfälle --> <!-- Vorfälle -->
<div class="column is-3"> <div class="column is-3">
<div class="field"> <div class="field">
<label class="label">Vorfall</label> <label class="label">Vorfall</label>
<div class="control"> <div class="control">
<div class="select is-fullwidth"> <div class="select is-fullwidth">
<select> <select>
<option>Alle</option> <option>Alle</option>
<option>Vorfall A</option> {% for i in incidents %}
<option>Vorfall B</option> <option value="{{ i.id }}" {% if request.GET.risk == i.id|stringformat:"s" %}selected{% endif %}>
</select> {{ i.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>
<!-- 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>
{% 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>
</div> </div>
</div> </div>
</form>
<!-- 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> <h2 class="title is-5">Vorfälle</h2>
@ -91,19 +103,25 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr onclick="window.location.href='/risks/incidents/1';" style="cursor:pointer;"> {% for i in incidents %}
<td>Switch entwendet</td> <tr onclick="window.location.href='{% url 'risks:show_incident' i.id %}'" style="cursor:pointer;">
<td>{{ i.title }}</td>
<td> <td>
{% if i.related_risks.exists %}
<ul> <ul>
<li> {% for r in i.related_risks.all %}
<a href="/risks/risks/1" onclick="event.stopPropagation();">Hardware Diebstahl</a> <li>{{ r.title }}</li>
</li> {% endfor %}
{% else %}
Noch kein Risiko zugeordnet
{% endif %}
</ul> </ul>
</td> </td>
<td>Closed</td> <td>{{ i.get_status_display }}</td>
<td>08.09.2025</td> <td>{{ i.date_reported|date:"d.m.Y" }}</td>
<td>Kevin Heyer</td> <td>{{ i.reported_by }}</td>
</tr> </tr>
{% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>