diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index d2e5ac0..d95596c 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 31827bc..9f89ecc 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 899d0f9..5b3769f 100644 --- a/config/settings.py +++ b/config/settings.py @@ -47,6 +47,7 @@ INSTALLED_APPS = [ MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", @@ -134,14 +135,28 @@ AUTH_PASSWORD_VALIDATORS = [ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] +# Login-Flow +LOGIN_URL = "login" +LOGIN_REDIRECT_URL = "risks:dashboard" +LOGOUT_REDIRECT_URL = "login" + +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + # --------------------------------------------------------------------------- # Internationalization # --------------------------------------------------------------------------- LANGUAGE_CODE = "de" -TIME_ZONE = "UTC" +TIME_ZONE = "Europe/Berlin" USE_I18N = True USE_TZ = True +LANGUAGES = [ + ("de", "Deutsch"), + ("en", "English"), +] + +LOCALE_PATHS = [BASE_DIR / "locale"] + # --------------------------------------------------------------------------- # Static files # --------------------------------------------------------------------------- diff --git a/config/urls.py b/config/urls.py index 4802609..fba7162 100644 --- a/config/urls.py +++ b/config/urls.py @@ -15,9 +15,11 @@ router.register(r"logs", AuditViewSet) urlpatterns = [ path("admin/", admin.site.urls), + path("i18n/", include("django.conf.urls.i18n")), # Language Switch path("api/ping/", ping), # Public healthcheck endpoint path("api/secure-ping/", secure_ping), # Protected API endpoint path("api/", include(router.urls)), + path("accounts/", include("django.contrib.auth.urls")), path("", include("risks.urls", namespace="risks")), ] diff --git a/db.sqlite3 b/db.sqlite3 index 16f0f61..a13ef47 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 0000000..0584151 Binary files /dev/null and b/locale/de/LC_MESSAGES/django.mo differ diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..cbe4abe --- /dev/null +++ b/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,211 @@ +msgid "" +msgstr "" +"Project-Id-Version: wira-risk-management\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-09-09 14:20+0200\n" +"PO-Revision-Date: 2025-09-09 13:45+0200\n" +"Last-Translator: Kevin Heyer \n" +"Language-Team: German\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: risks/admin.py:6 risks/admin.py:8 +msgid "Administration" +msgstr "Verwaltung" + +#: risks/admin.py:7 +msgid "Admin" +msgstr "Admin" + +#: risks/admin.py:13 +msgid "SSO Information" +msgstr "SSO-Informationen" + +#: risks/admin.py:20 +msgid "Risks Owned" +msgstr "Eigene Risiken" + +#: risks/admin.py:24 +msgid "Controls Responsible" +msgstr "Verantwortlich für Maßnahmen" + +#: risks/apps.py:7 +msgid "Risk Management" +msgstr "Risikomanagement" + +#: risks/models.py:35 +msgid "Risk" +msgstr "Risiko" + +#: risks/models.py:36 +msgid "Risks" +msgstr "Risiken" + +#: risks/models.py:39 +#, fuzzy +#| msgid "Very low occurs less than once every 5 years" +msgid "Very low – occurs less than once every 5 years" +msgstr "Sehr niedrig – tritt seltener als einmal in fünf Jahren auf" + +#: risks/models.py:40 +#, fuzzy +#| msgid "Low once every 15 years" +msgid "Low – once every 1–5 years" +msgstr "Niedrig – einmal in 1–5 Jahren" + +#: risks/models.py:41 +#, fuzzy +#| msgid "Likely once per year or more" +msgid "Likely – once per year or more" +msgstr "Wahrscheinlich – einmal pro Jahr oder öfter" + +#: risks/models.py:42 +#, fuzzy +#| msgid "Very likely multiple times per year/monthly" +msgid "Very likely – multiple times per year/monthly" +msgstr "Sehr wahrscheinlich – mehrmals pro Jahr/monatlich" + +#: risks/models.py:45 +#, fuzzy +#| msgid "Low (< 1,000 minor operational impact)" +msgid "Very Low (< 1,000 € – minor operational impact)" +msgstr "Sehr Gering (< 1.000 € – geringe betriebliche Auswirkungen)" + +#: risks/models.py:46 +#, fuzzy +#| msgid "Medium (1,0005,000 local impact)" +msgid "Low (1,000–5,000 € – local impact)" +msgstr "Gering (1.000–5.000 € – lokale Auswirkungen)" + +#: risks/models.py:47 +#, fuzzy +#| msgid "High (5,00015,000 team-level impact)" +msgid "High (5,000–15,000 € – team-level impact)" +msgstr "Hoch (5.000–15.000 € – Auswirkungen auf Teamebene)" + +#: risks/models.py:48 +#, fuzzy +#| msgid "Severe (50,000100,000 regional impact)" +msgid "Severe (50,000–100,000 € – regional impact)" +msgstr "Schwerwiegend (50.000–100.000 € – regionale Auswirkungen)" + +#: risks/models.py:49 +#, fuzzy +#| msgid "Critical (> 100,000 existential threat)" +msgid "Critical (> 100,000 € – existential threat)" +msgstr "Kritisch (> 100.000 € – existenzielle Bedrohung)" + +#: risks/models.py:52 +msgid "Confidentiality" +msgstr "Vertraulichkeit" + +#: risks/models.py:53 +msgid "Integrity" +msgstr "Integrität" + +#: risks/models.py:54 +msgid "Availability" +msgstr "Verfügbarkeit" + +#: risks/models.py:58 risks/models.py:186 risks/models.py:251 +msgid "Title" +msgstr "Titel" + +#: risks/models.py:59 risks/models.py:252 +msgid "Description" +msgstr "Beschreibung" + +#: risks/models.py:60 +msgid "Asset" +msgstr "Asset" + +#: risks/models.py:61 +msgid "Process" +msgstr "Prozess" + +#: risks/models.py:62 +msgid "Category" +msgstr "Kategorie" + +#: risks/models.py:63 +msgid "Created at" +msgstr "Erstellt am" + +#: risks/models.py:64 +msgid "Updated at" +msgstr "Aktualisiert am" + +#: risks/models.py:119 +msgid "Residual Risk" +msgstr "Restrisiko" + +#: risks/models.py:120 +msgid "Residual Risks" +msgstr "Restrisiken" + +#: risks/models.py:175 +msgid "Control" +msgstr "Maßnahme" + +#: risks/models.py:176 +msgid "Controls" +msgstr "Maßnahmen" + +#: risks/models.py:179 +msgid "Planned" +msgstr "Geplant" + +#: risks/models.py:180 +msgid "In progress" +msgstr "In Bearbeitung" + +#: risks/models.py:181 +msgid "Completed" +msgstr "Abgeschlossen" + +#: risks/models.py:182 +msgid "Verified" +msgstr "Verifiziert" + +#: risks/models.py:183 +msgid "Rejected" +msgstr "Abgelehnt" + +#: risks/models.py:212 +msgid "Auditlog" +msgstr "Audit-Log" + +#: risks/models.py:213 +msgid "Auditlogs" +msgstr "Audit-Logs" + +#: risks/models.py:243 +msgid "Incident" +msgstr "Vorfall" + +#: risks/models.py:244 +msgid "Incidents" +msgstr "Vorfälle" + +#: risks/models.py:247 +msgid "Opened" +msgstr "Eröffnet" + +#: risks/models.py:248 +msgid "In Progress" +msgstr "In Bearbeitung" + +#: risks/models.py:249 +msgid "Closed" +msgstr "Geschlossen" + +#: risks/models.py:253 +msgid "Date reported" +msgstr "Meldedatum" + +#: risks/models.py:255 +msgid "Reported by" +msgstr "Gemeldet von" diff --git a/locale/de/LC_MESSAGES/formats.py b/locale/de/LC_MESSAGES/formats.py new file mode 100644 index 0000000..e7b3e15 --- /dev/null +++ b/locale/de/LC_MESSAGES/formats.py @@ -0,0 +1,3 @@ +DATE_FORMAT = "d.m.Y" +DATETIME_FORMAT = "d.m.Y H:i" +TIME_FORMAT = "H:i" \ No newline at end of file diff --git a/locale/en/LC_MESSAGES/django.mo b/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000..71cbdf3 Binary files /dev/null and b/locale/en/LC_MESSAGES/django.mo differ diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000..84faae8 --- /dev/null +++ b/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,199 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-09-09 14:20+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: risks/admin.py:6 risks/admin.py:8 +msgid "Administration" +msgstr "" + +#: risks/admin.py:7 +msgid "Admin" +msgstr "" + +#: risks/admin.py:13 +msgid "SSO Information" +msgstr "" + +#: risks/admin.py:20 +msgid "Risks Owned" +msgstr "" + +#: risks/admin.py:24 +msgid "Controls Responsible" +msgstr "" + +#: risks/apps.py:7 +msgid "Risk Management" +msgstr "" + +#: risks/models.py:35 +msgid "Risk" +msgstr "" + +#: risks/models.py:36 +msgid "Risks" +msgstr "" + +#: risks/models.py:39 +msgid "Very low – occurs less than once every 5 years" +msgstr "" + +#: risks/models.py:40 +msgid "Low – once every 1–5 years" +msgstr "" + +#: risks/models.py:41 +msgid "Likely – once per year or more" +msgstr "" + +#: risks/models.py:42 +msgid "Very likely – multiple times per year/monthly" +msgstr "" + +#: risks/models.py:45 +msgid "Very Low (< 1,000 € – minor operational impact)" +msgstr "" + +#: risks/models.py:46 +msgid "Low (1,000–5,000 € – local impact)" +msgstr "" + +#: risks/models.py:47 +msgid "High (5,000–15,000 € – team-level impact)" +msgstr "" + +#: risks/models.py:48 +msgid "Severe (50,000–100,000 € – regional impact)" +msgstr "" + +#: risks/models.py:49 +msgid "Critical (> 100,000 € – existential threat)" +msgstr "" + +#: risks/models.py:52 +msgid "Confidentiality" +msgstr "" + +#: risks/models.py:53 +msgid "Integrity" +msgstr "" + +#: risks/models.py:54 +msgid "Availability" +msgstr "" + +#: risks/models.py:58 risks/models.py:186 risks/models.py:251 +msgid "Title" +msgstr "" + +#: risks/models.py:59 risks/models.py:252 +msgid "Description" +msgstr "" + +#: risks/models.py:60 +msgid "Asset" +msgstr "" + +#: risks/models.py:61 +msgid "Process" +msgstr "" + +#: risks/models.py:62 +msgid "Category" +msgstr "" + +#: risks/models.py:63 +msgid "Created at" +msgstr "" + +#: risks/models.py:64 +msgid "Updated at" +msgstr "" + +#: risks/models.py:119 +msgid "Residual Risk" +msgstr "" + +#: risks/models.py:120 +msgid "Residual Risks" +msgstr "" + +#: risks/models.py:175 +msgid "Control" +msgstr "" + +#: risks/models.py:176 +msgid "Controls" +msgstr "" + +#: risks/models.py:179 +msgid "Planned" +msgstr "" + +#: risks/models.py:180 +msgid "In progress" +msgstr "" + +#: risks/models.py:181 +msgid "Completed" +msgstr "" + +#: risks/models.py:182 +msgid "Verified" +msgstr "" + +#: risks/models.py:183 +msgid "Rejected" +msgstr "" + +#: risks/models.py:212 +msgid "Auditlog" +msgstr "" + +#: risks/models.py:213 +msgid "Auditlogs" +msgstr "" + +#: risks/models.py:243 +msgid "Incident" +msgstr "" + +#: risks/models.py:244 +msgid "Incidents" +msgstr "" + +#: risks/models.py:247 +msgid "Opened" +msgstr "" + +#: risks/models.py:248 +msgid "In Progress" +msgstr "" + +#: risks/models.py:249 +msgid "Closed" +msgstr "" + +#: risks/models.py:253 +msgid "Date reported" +msgstr "" + +#: risks/models.py:255 +msgid "Reported by" +msgstr "" diff --git a/risks/admin.py b/risks/admin.py index 32ee87d..7163e70 100644 --- a/risks/admin.py +++ b/risks/admin.py @@ -1,23 +1,27 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.utils.translation import gettext_lazy as _ from .models import User, Risk, ResidualRisk, Control, Incident +admin.site.site_header = _("Administration") +admin.site.site_title = _("Admin") +admin.site.index_title = _("Administration") @admin.register(User) class UserAdmin(BaseUserAdmin): fieldsets = BaseUserAdmin.fieldsets + ( - ("SSO Information", {"fields": ("is_sso_user",)}), + (_("SSO Information"), {"fields": ("is_sso_user",)}), ) list_display = ("username", "email", "is_staff", "is_superuser", "is_sso_user", "owned_risks_count", "responsible_controls_count") def owned_risks_count(self, obj): return obj.risks_owned.count() - owned_risks_count.short_description = "Risks Owned" + owned_risks_count.short_description = _("Risks Owned") def responsible_controls_count(self, obj): return obj.controls_responsible.count() - responsible_controls_count.short_description = "Controls Responsible" + responsible_controls_count.short_description = _("Controls Responsible") class ResidualRiskInline(admin.StackedInline): """ @@ -25,7 +29,7 @@ class ResidualRiskInline(admin.StackedInline): """ model = ResidualRisk extra = 0 - can_delete = False # Since each Risk can have at most one residual risk + can_delete = False readonly_fields = ("score", "level", "review_required") fields = ("likelihood", "impact", "score", "level", "review_required") @@ -48,7 +52,7 @@ class RiskAdmin(admin.ModelAdmin): ) list_filter = ("level", "likelihood", "impact", "owner") search_fields = ("title", "asset", "process", "category") - inlines = [ResidualRiskInline, ControlRisksInline] # Controls hier verknüpfen + inlines = [ResidualRiskInline, ControlRisksInline] def save_model(self, request, obj, form, change): obj._changed_by = request.user diff --git a/risks/apps.py b/risks/apps.py index 8b6096a..8f3dd84 100644 --- a/risks/apps.py +++ b/risks/apps.py @@ -1,10 +1,10 @@ from django.apps import AppConfig - +from django.utils.translation import gettext_lazy as _ class RisksConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'risks' + default_auto_field = "django.db.models.BigAutoField" + name = "risks" + verbose_name = _("Risk Management") def ready(self): - # Import signals when app is ready import risks.signals \ No newline at end of file diff --git a/risks/migrations/0018_alter_auditlog_options_alter_control_options_and_more.py b/risks/migrations/0018_alter_auditlog_options_alter_control_options_and_more.py new file mode 100644 index 0000000..1b34164 --- /dev/null +++ b/risks/migrations/0018_alter_auditlog_options_alter_control_options_and_more.py @@ -0,0 +1,108 @@ +# Generated by Django 5.2.6 on 2025-09-09 11:53 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("risks", "0017_alter_incident_status"), + ] + + operations = [ + migrations.AlterModelOptions( + name="auditlog", + options={"verbose_name": "Auditlog", "verbose_name_plural": "Auditlogs"}, + ), + migrations.AlterModelOptions( + name="control", + options={"verbose_name": "Control", "verbose_name_plural": "Controls"}, + ), + migrations.AlterModelOptions( + name="residualrisk", + options={ + "verbose_name": "Residual Risk", + "verbose_name_plural": "Residual Risks", + }, + ), + migrations.AlterModelOptions( + name="risk", + options={"verbose_name": "Risk", "verbose_name_plural": "Risks"}, + ), + migrations.AlterField( + model_name="control", + name="title", + field=models.CharField(max_length=255, verbose_name="Title"), + ), + migrations.AlterField( + model_name="incident", + name="date_reported", + field=models.DateField(blank=True, null=True, verbose_name="Date reported"), + ), + migrations.AlterField( + model_name="incident", + name="description", + field=models.TextField(blank=True, null=True, verbose_name="Description"), + ), + migrations.AlterField( + model_name="incident", + name="reported_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="incidents", + to=settings.AUTH_USER_MODEL, + verbose_name="Reported by", + ), + ), + migrations.AlterField( + model_name="incident", + name="title", + field=models.CharField(max_length=255, verbose_name="Title"), + ), + migrations.AlterField( + model_name="risk", + name="asset", + field=models.CharField( + blank=True, max_length=255, null=True, verbose_name="Asset" + ), + ), + migrations.AlterField( + model_name="risk", + name="category", + field=models.CharField( + blank=True, max_length=255, null=True, verbose_name="Category" + ), + ), + migrations.AlterField( + model_name="risk", + name="created_at", + field=models.DateTimeField(auto_now_add=True, verbose_name="Created at"), + ), + migrations.AlterField( + model_name="risk", + name="description", + field=models.TextField( + blank=True, max_length=225, null=True, verbose_name="Description" + ), + ), + migrations.AlterField( + model_name="risk", + name="process", + field=models.CharField( + blank=True, max_length=255, null=True, verbose_name="Process" + ), + ), + migrations.AlterField( + model_name="risk", + name="title", + field=models.CharField(max_length=255, verbose_name="Title"), + ), + migrations.AlterField( + model_name="risk", + name="updated_at", + field=models.DateTimeField(auto_now=True, verbose_name="Updated at"), + ), + ] diff --git a/risks/migrations/0019_alter_incident_options.py b/risks/migrations/0019_alter_incident_options.py new file mode 100644 index 0000000..818f699 --- /dev/null +++ b/risks/migrations/0019_alter_incident_options.py @@ -0,0 +1,16 @@ +# Generated by Django 5.2.6 on 2025-09-09 11:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("risks", "0018_alter_auditlog_options_alter_control_options_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="incident", + options={"verbose_name": "Incident", "verbose_name_plural": "Incidents"}, + ), + ] diff --git a/risks/migrations/0020_alter_residualrisk_impact_alter_risk_impact.py b/risks/migrations/0020_alter_residualrisk_impact_alter_risk_impact.py new file mode 100644 index 0000000..6fab0b5 --- /dev/null +++ b/risks/migrations/0020_alter_residualrisk_impact_alter_risk_impact.py @@ -0,0 +1,40 @@ +# Generated by Django 5.2.6 on 2025-09-09 12:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("risks", "0019_alter_incident_options"), + ] + + operations = [ + migrations.AlterField( + model_name="residualrisk", + name="impact", + field=models.IntegerField( + choices=[ + (1, "Very Low (< 1,000 € – minor operational impact)"), + (2, "Low (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="impact", + field=models.IntegerField( + choices=[ + (1, "Very Low (< 1,000 € – minor operational impact)"), + (2, "Low (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, + ), + ), + ] diff --git a/risks/models.py b/risks/models.py index 6d5ace5..07c739a 100644 --- a/risks/models.py +++ b/risks/models.py @@ -2,6 +2,7 @@ from django.conf import settings from django.contrib.auth.models import AbstractUser from django.core.serializers.json import DjangoJSONEncoder from django.db import models +from django.utils.translation import gettext_lazy as _ from multiselectfield import MultiSelectField import datetime import json @@ -29,39 +30,38 @@ class User(AbstractUser): return self.responsible_controls.all() class Risk(models.Model): - """ - Represents an information security risk. - """ + + class Meta: + verbose_name = _("Risk") + verbose_name_plural = _("Risks") LIKELIHOOD_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"), + (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")), ] - IMPACT_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)"), + (1, _("Very Low (< 1,000 € – minor operational impact)")), + (2, _("Low (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)")), ] - 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,) - updated_at = models.DateTimeField(auto_now=True) + title = models.CharField(_("Title"), max_length=255) + description = models.TextField(_("Description"), max_length=225, blank=True, null=True) + asset = models.CharField(_("Asset"), max_length=255, blank=True, null=True) + process = models.CharField(_("Process"), max_length=255, blank=True, null=True) + category = models.CharField(_("Category"), max_length=255, blank=True, null=True) + created_at = models.DateTimeField(_("Created at"), auto_now_add=True) + updated_at = models.DateTimeField(_("Updated at"), auto_now=True) # CIA Protection Goals cia = MultiSelectField(choices=CIA_CHOICES, max_length=100, blank=True, null=True) @@ -115,6 +115,10 @@ class ResidualRisk(models.Model): Residual Risk after implementing controls """ + class Meta: + verbose_name = _("Residual Risk") + verbose_name_plural = _("Residual Risks") + risk = models.OneToOneField( Risk, on_delete=models.CASCADE, @@ -167,15 +171,19 @@ class Control(models.Model): """ A security control/measure linked to a risk. """ + class Meta: + verbose_name = _("Control") + verbose_name_plural = _("Controls") + STATUS_CHOICES = [ - ("planned", "Planned"), - ("in_progress", "In progress"), - ("completed", "Completed"), - ("verified", "Verified"), - ("rejected", "Rejected"), + ("planned", _("Planned")), + ("in_progress", _("In progress")), + ("completed", _("Completed")), + ("verified", _("Verified")), + ("rejected", _("Rejected")), ] - title = models.CharField(max_length=255) + title = models.CharField(_("Title"), max_length=255) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="planned") due_date = models.DateField(blank=True, null=True) responsible = models.ForeignKey( @@ -199,6 +207,11 @@ class AuditLog(models.Model): """ Generic audit log entry for tracking changes. """ + + class Meta: + verbose_name = _("Auditlog") + verbose_name_plural = _("Auditlogs") + ACTION_CHOICES = [ ("create", "Created"), ("update", "Updated"), @@ -225,15 +238,23 @@ class Incident(models.Model): """ Incidents and related risks """ + + class Meta: + verbose_name = _("Incident") + verbose_name_plural = _("Incidents") + STATUS_CHOICES = [ - ("open", "Opened"), - ("in_progress", "In Progress"), - ("closed", "Closed"), + ("open", _("Opened")), + ("in_progress", _("In Progress")), + ("closed", _("Closed")), ] - title = models.CharField(max_length=255) - description = models.TextField(blank=True, null=True) - date_reported = models.DateField(blank=True, null=True) - reported_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="incidents") + title = models.CharField(_("Title"), max_length=255) + description = models.TextField(_("Description"), blank=True, null=True) + date_reported = models.DateField(_("Date reported"), blank=True, null=True) + reported_by = models.ForeignKey( + settings.AUTH_USER_MODEL, verbose_name=_("Reported by"), + null=True, blank=True, on_delete=models.SET_NULL, related_name="incidents" + ) status = models.CharField(max_length=12, choices=STATUS_CHOICES) related_risks = models.ManyToManyField("Risk", blank=True, related_name="incidents") created_at = models.DateTimeField(auto_now_add=True,) diff --git a/risks/templatetags/risk_extras.py b/risks/templatetags/risk_extras.py index ed5945d..524b0f0 100644 --- a/risks/templatetags/risk_extras.py +++ b/risks/templatetags/risk_extras.py @@ -7,3 +7,60 @@ register = template.Library() def cia_label(value): mapping = dict(Risk.CIA_CHOICES) return mapping.get(value, value) + +LIKELIHOOD_MAP = { + 1: "is-control-verylow", + 2: "is-control-low", + 3: "is-control-mid", + 4: "is-control-high", +} + +IMPACT_MAP = { + 1: "is-control-verylow", + 2: "is-control-low", + 3: "is-control-mid", + 4: "is-control-high", + 5: "is-control-veryhigh", +} + +LEVEL_MAP = { + "Low": "is-control-low", + "Medium": "is-control-mid", + "High": "is-control-high", + "Critical": "is-control-veryhigh", +} + +@register.filter +def likelihood_class(val): + try: + return LIKELIHOOD_MAP.get(int(val), "is-light") + except (TypeError, ValueError): + return "is-light" + +@register.filter +def impact_class(val): + try: + return IMPACT_MAP.get(int(val), "is-light") + except (TypeError, ValueError): + return "is-light" + +@register.filter +def level_class(level): + return LEVEL_MAP.get(str(level), "is-light") + +@register.filter +def score_class(score): + """Score 1..20 → 5 Stufen""" + try: + s = int(score) + except (TypeError, ValueError): + return "is-light" + if s <= 4: + return "is-control-verylow" + if s <= 8: + return "is-control-low" + if s <= 12: + return "is-control-mid" + if s <= 16: + return "is-control-high" + return "is-control-veryhigh" \ No newline at end of file diff --git a/risks/views.py b/risks/views.py index f3dc6f0..fab38da 100644 --- a/risks/views.py +++ b/risks/views.py @@ -1,5 +1,6 @@ from django.contrib.admin.models import LogEntry from django.contrib.auth import get_user_model +from django.contrib.auth.decorators import login_required from django.contrib.contenttypes.models import ContentType from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated @@ -95,12 +96,15 @@ class IncidentViewSet(viewsets.ModelViewSet): # Web # --------------------------------------------------------------------------- +@login_required def dashboard(request): return render(request, "risks/dashboard.html") +@login_required def stats(request): return render(request, "risks/statistics.html") +@login_required def list_risks(request): qs = Risk.objects.all().select_related("owner") @@ -127,16 +131,18 @@ def list_risks(request): "owners": owners, }) +@login_required def show_risk(request, id): - risk = get_object_or_404(Risk, pk=id) + risk = get_object_or_404( + Risk.objects.select_related("residual_risk", "owner").prefetch_related("controls"), + pk=id, + ) ct = ContentType.objects.get_for_model(Risk) - logs = LogEntry.objects.filter( - content_type=ct, - object_id=risk.pk - ).order_by("-action_time") - + 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}) +@login_required def list_controls(request): qs = Control.objects.all().select_related("responsible") @@ -166,6 +172,7 @@ def list_controls(request): "status_choices": Control.STATUS_CHOICES, }) +@login_required def show_control(request, id): control = get_object_or_404(Control, pk=id) ct = ContentType.objects.get_for_model(Control) @@ -176,7 +183,7 @@ def show_control(request, id): return render(request, "risks/item_control.html", {"control": control, "logs": logs}) - +@login_required def list_incidents(request): qs = Incident.objects.all().select_related("reported_by").prefetch_related("related_risks") @@ -203,6 +210,7 @@ def list_incidents(request): "status_choices": Incident.STATUS_CHOICES, }) +@login_required def show_incident(request, id): incident = get_object_or_404(Incident, pk=id) ct = ContentType.objects.get_for_model(Incident) @@ -211,4 +219,4 @@ def show_incident(request, id): object_id=incident.pk ).order_by("-action_time") - return render(request, "risks/item_incident.html", {"incident": incident, "logs": logs}) \ No newline at end of file + return render(request, "risks/item_incident.html", {"incident": incident, "logs": logs}) diff --git a/static/css/design.css b/static/css/design.css index a24aa1f..dcbc5d7 100644 --- a/static/css/design.css +++ b/static/css/design.css @@ -1,3 +1,86 @@ +/* Base palette */ +:root{ + --c-verylow:#22c55e; --c-verylow-100:#dcfce7; --c-verylow-300:#86efac; --c-verylow-inv:#fff; + --c-low:#84cc16; --c-low-100:#ecfccb; --c-low-300:#bef264; --c-low-inv:#111; + --c-mid:#eab308; --c-mid-100:#fef9c3; --c-mid-300:#fde047; --c-mid-inv:#111; + --c-high:#f97316; --c-high-100:#ffedd5; --c-high-300:#fbbf24; --c-high-inv:#111; + --c-veryhigh:#dc2626; --c-veryhigh-100:#fee2e2;--c-veryhigh-300:#fca5a5; --c-veryhigh-inv:#fff; +} + +/* Helpers (wie Bulma) */ +.has-text-control-verylow{color:var(--c-verylow)!important} +.has-text-control-low{color:var(--c-low)!important} +.has-text-control-mid{color:var(--c-mid)!important} +.has-text-control-high{color:var(--c-high)!important} +.has-text-control-veryhigh{color:var(--c-veryhigh)!important} + +.has-background-control-verylow{background:var(--c-verylow)!important;color:var(--c-verylow-inv)!important} +.has-background-control-low{background:var(--c-low)!important;color:var(--c-low-inv)!important} +.has-background-control-mid{background:var(--c-mid)!important;color:var(--c-mid-inv)!important} +.has-background-control-high{background:var(--c-high)!important;color:var(--c-high-inv)!important} +.has-background-control-veryhigh{background:var(--c-veryhigh)!important;color:var(--c-veryhigh-inv)!important} + +/* Buttons */ +.button.is-control-verylow{background:var(--c-verylow);border-color:transparent;color:var(--c-verylow-inv)} +.button.is-control-low{background:var(--c-low);border-color:transparent;color:var(--c-low-inv)} +.button.is-control-mid{background:var(--c-mid);border-color:transparent;color:var(--c-mid-inv)} +.button.is-control-high{background:var(--c-high);border-color:transparent;color:var(--c-high-inv)} +.button.is-control-veryhigh{background:var(--c-veryhigh);border-color:transparent;color:var(--c-veryhigh-inv)} +.button.is-control-verylow:hover{filter:brightness(.92)} +.button.is-control-low:hover{filter:brightness(.92)} +.button.is-control-mid:hover{filter:brightness(.92)} +.button.is-control-high:hover{filter:brightness(.92)} +.button.is-control-veryhigh:hover{filter:brightness(.92)} +.button.is-control-verylow.is-light{background:var(--c-verylow-100);color:var(--c-verylow)} +.button.is-control-low.is-light{background:var(--c-low-100);color:var(--c-low)} +.button.is-control-mid.is-light{background:var(--c-mid-100);color:var(--c-mid)} +.button.is-control-high.is-light{background:var(--c-high-100);color:var(--c-high)} +.button.is-control-veryhigh.is-light{background:var(--c-veryhigh-100);color:var(--c-veryhigh)} + +/* Tags */ +.tag.is-control-verylow{background:var(--c-verylow);color:var(--c-verylow-inv)} +.tag.is-control-low{background:var(--c-low);color:var(--c-low-inv)} +.tag.is-control-mid{background:var(--c-mid);color:var(--c-mid-inv)} +.tag.is-control-high{background:var(--c-high);color:var(--c-high-inv)} +.tag.is-control-veryhigh{background:var(--c-veryhigh);color:var(--c-veryhigh-inv)} +.tag.is-control-verylow.is-light{background:var(--c-verylow-100);color:var(--c-verylow)} +.tag.is-control-low.is-light{background:var(--c-low-100);color:var(--c-low)} +.tag.is-control-mid.is-light{background:var(--c-mid-100);color:var(--c-mid)} +.tag.is-control-high.is-light{background:var(--c-high-100);color:var(--c-high)} +.tag.is-control-veryhigh.is-light{background:var(--c-veryhigh-100);color:var(--c-veryhigh)} + +/* Notifications */ +.notification.is-control-verylow{background:var(--c-verylow-100);border-left:4px solid var(--c-verylow);color:#111} +.notification.is-control-low{background:var(--c-low-100);border-left:4px solid var(--c-low);color:#111} +.notification.is-control-mid{background:var(--c-mid-100);border-left:4px solid var(--c-mid);color:#111} +.notification.is-control-high{background:var(--c-high-100);border-left:4px solid var(--c-high);color:#111} +.notification.is-control-veryhigh{background:var(--c-veryhigh-100);border-left:4px solid var(--c-veryhigh);color:#111} + +/* Messages */ +.message.is-control-verylow .message-header{background:var(--c-verylow);color:var(--c-verylow-inv)} +.message.is-control-low .message-header{background:var(--c-low);color:var(--c-low-inv)} +.message.is-control-mid .message-header{background:var(--c-mid);color:var(--c-mid-inv)} +.message.is-control-high .message-header{background:var(--c-high);color:var(--c-high-inv)} +.message.is-control-veryhigh .message-header{background:var(--c-veryhigh);color:var(--c-veryhigh-inv)} +.message.is-control-verylow .message-body{border-color:var(--c-verylow-300)} +.message.is-control-low .message-body{border-color:var(--c-low-300)} +.message.is-control-mid .message-body{border-color:var(--c-mid-300)} +.message.is-control-high .message-body{border-color:var(--c-high-300)} +.message.is-control-veryhigh .message-body{border-color:var(--c-veryhigh-300)} + +/* Progress (optional) */ +.progress.is-control-verylow::-webkit-progress-value{background:var(--c-verylow)} +.progress.is-control-low::-webkit-progress-value{background:var(--c-low)} +.progress.is-control-mid::-webkit-progress-value{background:var(--c-mid)} +.progress.is-control-high::-webkit-progress-value{background:var(--c-high)} +.progress.is-control-veryhigh::-webkit-progress-value{background:var(--c-veryhigh)} +.progress.is-control-verylow::-moz-progress-bar{background:var(--c-verylow)} +.progress.is-control-low::-moz-progress-bar{background:var(--c-low)} +.progress.is-control-mid::-moz-progress-bar{background:var(--c-mid)} +.progress.is-control-high::-moz-progress-bar{background:var(--c-high)} +.progress.is-control-veryhigh::-moz-progress-bar{background:var(--c-veryhigh)} + + /* Topbar-Farbe erzwingen (Bulma überschreibt sonst mit weiß) */ .navbar.topbar-nav { background-color: #d6801e !important; /* Orange wie im Screenshot */ diff --git a/templates/admin/base_site.html b/templates/admin/base_site.html new file mode 100644 index 0000000..70747f6 --- /dev/null +++ b/templates/admin/base_site.html @@ -0,0 +1,24 @@ +{# templates/admin/base_site.html #} +{% extends "admin/base.html" %} +{% load i18n %} + +{% block title %}{{ title }} | {{ site_title }}{% endblock %} + +{% block branding %} +

{{ site_header }}

+{% endblock %} + +{% block usertools %} +{{ block.super }} +
+ {% csrf_token %} + + +
+{% endblock %} diff --git a/templates/base.html b/templates/base.html index 6112d79..e9fea9d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -47,14 +47,60 @@ +