diff --git a/config/settings.py b/config/settings.py index 5b3769f..a8864c4 100644 --- a/config/settings.py +++ b/config/settings.py @@ -216,5 +216,5 @@ if SSO_ENABLED: # --------------------------------------------------------------------------- CRONJOBS = [ - ("0 8 * * *", "risks.utils.check_risk_followups"), + ("0 8 * * *", "risks.utils.check_risk_followups", ">> /var/log/wira_followups.log 2>&1"), ] \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 index e831ec3..deb65ca 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 index a2fc887..33d0ec2 100644 Binary files a/locale/de/LC_MESSAGES/django.mo 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 index 7befb6d..13d90de 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: wira-risk-management\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-10 08:16+0200\n" +"POT-Creation-Date: 2025-09-10 11:18+0200\n" "PO-Revision-Date: 2025-09-09 13:45+0200\n" "Last-Translator: Kevin Heyer \n" "Language-Team: German\n" @@ -20,15 +20,41 @@ msgstr "Verwaltung" msgid "Admin" msgstr "Admin" -#: risks/admin.py:13 +#: risks/admin.py:15 risks/models.py:36 templates/risks/dashboard.html:75 +#: templates/risks/dashboard.html:80 templates/risks/dashboard.html:85 +#: templates/risks/list_risks.html:76 +msgid "Risks" +msgstr "Risiken" + +#: risks/admin.py:16 risks/models.py:190 templates/risks/list_risks.html:37 +msgid "Controls" +msgstr "Maßnahmen" + +#: risks/admin.py:17 +msgid "Residual risks" +msgstr "Restrisiken" + +#: risks/admin.py:18 +msgid "Reviews" +msgstr "Prüfung" + +#: risks/admin.py:19 risks/models.py:258 +msgid "Incidents" +msgstr "Vorfälle" + +#: risks/admin.py:20 +msgid "Users" +msgstr "Benutzer" + +#: risks/admin.py:26 msgid "SSO Information" msgstr "SSO-Informationen" -#: risks/admin.py:20 +#: risks/admin.py:35 msgid "Risks Owned" msgstr "Eigene Risiken" -#: risks/admin.py:24 +#: risks/admin.py:39 msgid "Controls Responsible" msgstr "Verantwortlich für Maßnahmen" @@ -41,160 +67,241 @@ msgstr "Risikomanagement" msgid "Risk" msgstr "Risiko" -#: risks/models.py:36 templates/risks/dashboard.html:75 -#: templates/risks/dashboard.html:80 templates/risks/dashboard.html:85 -#: templates/risks/list_risks.html:76 -msgid "Risks" -msgstr "Risiken" - #: risks/models.py:39 -msgid "Very low – occurs less than once every 5 years" -msgstr "Sehr niedrig – tritt seltener als einmal in fünf Jahren auf" +msgid "Open" +msgstr "Offen" -#: risks/models.py:40 -msgid "Low – once every 1–5 years" -msgstr "Niedrig – einmal in 1–5 Jahren" - -#: risks/models.py:41 -msgid "Likely – once per year or more" -msgstr "Wahrscheinlich – einmal pro Jahr oder öfter" - -#: risks/models.py:42 -msgid "Very likely – multiple times per year/monthly" -msgstr "Sehr wahrscheinlich – mehrmals pro Jahr/monatlich" - -#: risks/models.py:45 -msgid "Very Low (< 1,000 € – minor operational impact)" -msgstr "Sehr Gering (< 1.000 € – geringe betriebliche Auswirkungen)" - -#: risks/models.py:46 -msgid "Low (1,000–5,000 € – local impact)" -msgstr "Gering (1.000–5.000 € – lokale Auswirkungen)" - -#: risks/models.py:47 -msgid "High (5,000–15,000 € – team-level impact)" -msgstr "Hoch (5.000–15.000 € – Auswirkungen auf Teamebene)" - -#: risks/models.py:48 -msgid "Severe (50,000–100,000 € – regional impact)" -msgstr "Schwerwiegend (50.000–100.000 € – regionale Auswirkungen)" - -#: risks/models.py:49 -msgid "Critical (> 100,000 € – existential threat)" -msgstr "Kritisch (> 100.000 € – existenzielle Bedrohung)" - -#: risks/models.py:52 templates/risks/dashboard.html:74 -msgid "Confidentiality" -msgstr "Vertraulichkeit" - -#: risks/models.py:53 templates/risks/dashboard.html:79 -msgid "Integrity" -msgstr "Integrität" - -#: risks/models.py:54 templates/risks/dashboard.html:84 -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 templates/risks/list_risks.html:85 -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 templates/risks/list_risks.html:37 -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 +#: risks/models.py:40 risks/models.py:262 msgid "In Progress" msgstr "In Bearbeitung" -#: risks/models.py:249 +#: risks/models.py:41 risks/models.py:263 msgid "Closed" msgstr "Geschlossen" -#: risks/models.py:253 +#: risks/models.py:42 +msgid "Review required" +msgstr "Prüfung nötig" + +#: risks/models.py:45 +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:46 +msgid "Low – once every 1–5 years" +msgstr "Niedrig – einmal in 1–5 Jahren" + +#: risks/models.py:47 +msgid "Likely – once per year or more" +msgstr "Wahrscheinlich – einmal pro Jahr oder öfter" + +#: risks/models.py:48 +msgid "Very likely – multiple times per year/monthly" +msgstr "Sehr wahrscheinlich – mehrmals pro Jahr/monatlich" + +#: risks/models.py:51 +msgid "Very Low (< 1,000 € – minor operational impact)" +msgstr "Sehr Gering (< 1.000 € – geringe betriebliche Auswirkungen)" + +#: risks/models.py:52 +msgid "Low (1,000–5,000 € – local impact)" +msgstr "Gering (1.000–5.000 € – lokale Auswirkungen)" + +#: risks/models.py:53 +msgid "High (5,000–15,000 € – team-level impact)" +msgstr "Hoch (5.000–15.000 € – Auswirkungen auf Teamebene)" + +#: risks/models.py:54 +msgid "Severe (50,000–100,000 € – regional impact)" +msgstr "Schwerwiegend (50.000–100.000 € – regionale Auswirkungen)" + +#: risks/models.py:55 +msgid "Critical (> 100,000 € – existential threat)" +msgstr "Kritisch (> 100.000 € – existenzielle Bedrohung)" + +#: risks/models.py:58 templates/risks/dashboard.html:74 +msgid "Confidentiality" +msgstr "Vertraulichkeit" + +#: risks/models.py:59 templates/risks/dashboard.html:79 +msgid "Integrity" +msgstr "Integrität" + +#: risks/models.py:60 templates/risks/dashboard.html:84 +msgid "Availability" +msgstr "Verfügbarkeit" + +#: risks/models.py:64 risks/models.py:200 risks/models.py:265 +msgid "Title" +msgstr "Titel" + +#: risks/models.py:65 risks/models.py:266 +msgid "Description" +msgstr "Beschreibung" + +#: risks/models.py:66 +msgid "Asset" +msgstr "Asset" + +#: risks/models.py:67 +msgid "Process" +msgstr "Prozess" + +#: risks/models.py:68 templates/risks/list_risks.html:85 +msgid "Category" +msgstr "Kategorie" + +#: risks/models.py:69 +msgid "Created at" +msgstr "Erstellt am" + +#: risks/models.py:70 +msgid "Updated at" +msgstr "Aktualisiert am" + +#: risks/models.py:73 +msgid "Status" +msgstr "Status" + +#: risks/models.py:133 +msgid "Residual Risk" +msgstr "Restrisiko" + +#: risks/models.py:134 +msgid "Residual Risks" +msgstr "Restrisiken" + +#: risks/models.py:189 +msgid "Control" +msgstr "Maßnahme" + +#: risks/models.py:193 +msgid "Planned" +msgstr "Geplant" + +#: risks/models.py:194 +msgid "In progress" +msgstr "In Bearbeitung" + +#: risks/models.py:195 +msgid "Completed" +msgstr "Abgeschlossen" + +#: risks/models.py:196 +msgid "Verified" +msgstr "Verifiziert" + +#: risks/models.py:197 +msgid "Rejected" +msgstr "Abgelehnt" + +#: risks/models.py:226 +msgid "Auditlog" +msgstr "Audit-Log" + +#: risks/models.py:227 +msgid "Auditlogs" +msgstr "Audit-Logs" + +#: risks/models.py:257 +msgid "Incident" +msgstr "Vorfall" + +#: risks/models.py:261 +msgid "Opened" +msgstr "Eröffnet" + +#: risks/models.py:267 msgid "Date reported" msgstr "Meldedatum" -#: risks/models.py:255 +#: risks/models.py:269 msgid "Reported by" msgstr "Gemeldet von" +#: risks/models.py:298 +msgid "User" +msgstr "Benutzer" + +#: risks/signals.py:57 +#, python-brace-format +msgid "User '{u}' created" +msgstr "Benutzer '{u}' erstellte" + +#: risks/signals.py:62 +#, python-brace-format +msgid "User '{u}' deleted" +msgstr "Benutzer '{u}' löschte" + +#: risks/signals.py:70 +#, python-brace-format +msgid "Risk '{title}' {state}" +msgstr "Risiko '{title}' {state}" + +#: risks/signals.py:72 risks/signals.py:147 risks/signals.py:240 +#: risks/signals.py:296 +msgid "created" +msgstr "erstellt" + +#: risks/signals.py:72 risks/signals.py:147 risks/signals.py:240 +#: risks/signals.py:296 +msgid "updated" +msgstr "Aktualisiert" + +#: risks/signals.py:78 +#, python-brace-format +msgid "Risk '{title}' deleted" +msgstr "Risiko '{title}' gelöscht" + +#: risks/signals.py:145 +#, python-brace-format +msgid "Control '{title}' {state}" +msgstr "Maßnahme '{title}' {state}" + +#: risks/signals.py:154 +#, python-brace-format +msgid "Control '{title}' deleted" +msgstr "Maßnahme '{title}' gelöscht" + +#: risks/signals.py:211 +#, python-brace-format +msgid "Review required for risk '{t}' due to control change" +msgstr "Prüfung nötig für: '{t}', da Maßnahmen geändert wurden" + +#: risks/signals.py:230 +#, python-brace-format +msgid "Review required for risk '{t}'" +msgstr "Prüfung benötigt für Risiko '{t}'" + +#: risks/signals.py:235 +#, python-brace-format +msgid "Review completed for risk '{t}'" +msgstr "Prüfung Abgeschlossen für Risiko '{t}'" + +#: risks/signals.py:239 +#, python-brace-format +msgid "Residual risk {state} for '{t}'" +msgstr "Restrisiko {state} für '{t}'" + +#: risks/signals.py:244 +#, python-brace-format +msgid "Residual risk deleted for '{t}'" +msgstr "Restrisiko für '{t}' gelöscht" + +#: risks/signals.py:296 +msgid "Incident '{t}' {s}" +msgstr "Vorfälle '{t}' {s}" + +#: risks/signals.py:301 +#, python-brace-format +msgid "Incident '{t}' deleted" +msgstr "Vorfall '{t}' gelöscht" + +#: risks/utils.py:48 +#, python-brace-format +msgid "Follow-up reached: review required for risk '{t}'" +msgstr "Wiedervorlagedatum erreicht: Prüfung nötig für Risiko '{t}'" + #: templates/risks/dashboard.html:9 msgid "Dashboard" msgstr "Dashboard" @@ -227,6 +334,26 @@ msgstr "Vorfälle nach Status" msgid "Risks by CIA" msgstr "CIA Risiken" +#: templates/risks/item_risk.html:68 templates/risks/item_risk.html:114 +#: templates/risks/list_risks.html:86 +msgid "Likelihood" +msgstr "Eintritt" + +#: templates/risks/item_risk.html:77 templates/risks/item_risk.html:123 +#: templates/risks/list_risks.html:87 +msgid "Impact" +msgstr "Schaden" + +#: templates/risks/item_risk.html:86 templates/risks/item_risk.html:132 +#: templates/risks/list_risks.html:89 +msgid "Level" +msgstr "Stufe" + +#: templates/risks/item_risk.html:95 templates/risks/item_risk.html:140 +#: templates/risks/list_risks.html:88 +msgid "Score" +msgstr "Score" + #: templates/risks/list_risks.html:4 msgid "Risk analysis" msgstr "Risikoanalyse" @@ -247,19 +374,3 @@ msgstr "Risikoeigner" #: templates/risks/list_risks.html:84 msgid "Asset / Process" msgstr "Asset / Prozess" - -#: templates/risks/list_risks.html:86 -msgid "Likelihood" -msgstr "Eintritt" - -#: templates/risks/list_risks.html:87 -msgid "Impact" -msgstr "Schaden" - -#: templates/risks/list_risks.html:88 -msgid "Score" -msgstr "Score" - -#: templates/risks/list_risks.html:89 -msgid "Level" -msgstr "Stufe" diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 9ec2d19..0fe311a 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-10 08:16+0200\n" +"POT-Creation-Date: 2025-09-10 11:18+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -26,15 +26,41 @@ msgstr "" msgid "Admin" msgstr "" -#: risks/admin.py:13 -msgid "SSO Information" +#: risks/admin.py:15 risks/models.py:36 templates/risks/dashboard.html:75 +#: templates/risks/dashboard.html:80 templates/risks/dashboard.html:85 +#: templates/risks/list_risks.html:76 +msgid "Risks" +msgstr "" + +#: risks/admin.py:16 risks/models.py:190 templates/risks/list_risks.html:37 +msgid "Controls" +msgstr "" + +#: risks/admin.py:17 +msgid "Residual risks" +msgstr "" + +#: risks/admin.py:18 +msgid "Reviews" +msgstr "" + +#: risks/admin.py:19 risks/models.py:258 +msgid "Incidents" msgstr "" #: risks/admin.py:20 +msgid "Users" +msgstr "" + +#: risks/admin.py:26 +msgid "SSO Information" +msgstr "" + +#: risks/admin.py:35 msgid "Risks Owned" msgstr "" -#: risks/admin.py:24 +#: risks/admin.py:39 msgid "Controls Responsible" msgstr "" @@ -47,160 +73,242 @@ msgstr "" msgid "Risk" msgstr "" -#: risks/models.py:36 templates/risks/dashboard.html:75 -#: templates/risks/dashboard.html:80 templates/risks/dashboard.html:85 -#: templates/risks/list_risks.html:76 -msgid "Risks" -msgstr "" - #: risks/models.py:39 -msgid "Very low – occurs less than once every 5 years" +msgid "Open" 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 templates/risks/dashboard.html:74 -msgid "Confidentiality" -msgstr "" - -#: risks/models.py:53 templates/risks/dashboard.html:79 -msgid "Integrity" -msgstr "" - -#: risks/models.py:54 templates/risks/dashboard.html:84 -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 templates/risks/list_risks.html:85 -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 templates/risks/list_risks.html:37 -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 +#: risks/models.py:40 risks/models.py:262 msgid "In Progress" msgstr "" -#: risks/models.py:249 +#: risks/models.py:41 risks/models.py:263 msgid "Closed" msgstr "" -#: risks/models.py:253 +#: risks/models.py:42 +msgid "Review required" +msgstr "" + +#: risks/models.py:45 +msgid "Very low – occurs less than once every 5 years" +msgstr "" + +#: risks/models.py:46 +msgid "Low – once every 1–5 years" +msgstr "" + +#: risks/models.py:47 +msgid "Likely – once per year or more" +msgstr "" + +#: risks/models.py:48 +msgid "Very likely – multiple times per year/monthly" +msgstr "" + +#: risks/models.py:51 +msgid "Very Low (< 1,000 € – minor operational impact)" +msgstr "" + +#: risks/models.py:52 +msgid "Low (1,000–5,000 € – local impact)" +msgstr "" + +#: risks/models.py:53 +msgid "High (5,000–15,000 € – team-level impact)" +msgstr "" + +#: risks/models.py:54 +msgid "Severe (50,000–100,000 € – regional impact)" +msgstr "" + +#: risks/models.py:55 +msgid "Critical (> 100,000 € – existential threat)" +msgstr "" + +#: risks/models.py:58 templates/risks/dashboard.html:74 +msgid "Confidentiality" +msgstr "" + +#: risks/models.py:59 templates/risks/dashboard.html:79 +msgid "Integrity" +msgstr "" + +#: risks/models.py:60 templates/risks/dashboard.html:84 +msgid "Availability" +msgstr "" + +#: risks/models.py:64 risks/models.py:200 risks/models.py:265 +msgid "Title" +msgstr "" + +#: risks/models.py:65 risks/models.py:266 +msgid "Description" +msgstr "" + +#: risks/models.py:66 +msgid "Asset" +msgstr "" + +#: risks/models.py:67 +msgid "Process" +msgstr "" + +#: risks/models.py:68 templates/risks/list_risks.html:85 +msgid "Category" +msgstr "" + +#: risks/models.py:69 +msgid "Created at" +msgstr "" + +#: risks/models.py:70 +msgid "Updated at" +msgstr "" + +#: risks/models.py:73 +msgid "Status" +msgstr "" + +#: risks/models.py:133 +msgid "Residual Risk" +msgstr "" + +#: risks/models.py:134 +msgid "Residual Risks" +msgstr "" + +#: risks/models.py:189 +msgid "Control" +msgstr "" + +#: risks/models.py:193 +msgid "Planned" +msgstr "" + +#: risks/models.py:194 +msgid "In progress" +msgstr "" + +#: risks/models.py:195 +msgid "Completed" +msgstr "" + +#: risks/models.py:196 +msgid "Verified" +msgstr "" + +#: risks/models.py:197 +msgid "Rejected" +msgstr "" + +#: risks/models.py:226 +msgid "Auditlog" +msgstr "" + +#: risks/models.py:227 +msgid "Auditlogs" +msgstr "" + +#: risks/models.py:257 +msgid "Incident" +msgstr "" + +#: risks/models.py:261 +msgid "Opened" +msgstr "" + +#: risks/models.py:267 msgid "Date reported" msgstr "" -#: risks/models.py:255 +#: risks/models.py:269 msgid "Reported by" msgstr "" +#: risks/models.py:298 +msgid "User" +msgstr "" + +#: risks/signals.py:57 +#, python-brace-format +msgid "User '{u}' created" +msgstr "" + +#: risks/signals.py:62 +#, python-brace-format +msgid "User '{u}' deleted" +msgstr "" + +#: risks/signals.py:70 +#, python-brace-format +msgid "Risk '{title}' {state}" +msgstr "" + +#: risks/signals.py:72 risks/signals.py:147 risks/signals.py:240 +#: risks/signals.py:296 +msgid "created" +msgstr "" + +#: risks/signals.py:72 risks/signals.py:147 risks/signals.py:240 +#: risks/signals.py:296 +msgid "updated" +msgstr "" + +#: risks/signals.py:78 +#, python-brace-format +msgid "Risk '{title}' deleted" +msgstr "" + +#: risks/signals.py:145 +#, python-brace-format +msgid "Control '{title}' {state}" +msgstr "" + +#: risks/signals.py:154 +#, python-brace-format +msgid "Control '{title}' deleted" +msgstr "" + +#: risks/signals.py:211 +#, python-brace-format +msgid "Review required for risk '{t}' due to control change" +msgstr "" + +#: risks/signals.py:230 +#, python-brace-format +msgid "Review required for risk '{t}'" +msgstr "" + +#: risks/signals.py:235 +#, python-brace-format +msgid "Review completed for risk '{t}'" +msgstr "" + +#: risks/signals.py:239 +#, python-brace-format +msgid "Residual risk {state} for '{t}'" +msgstr "" + +#: risks/signals.py:244 +#, python-brace-format +msgid "Residual risk deleted for '{t}'" +msgstr "" + +#: risks/signals.py:296 +#, python-brace-format +msgid "Incident '{t}' {s}" +msgstr "" + +#: risks/signals.py:301 +#, python-brace-format +msgid "Incident '{t}' deleted" +msgstr "" + +#: risks/utils.py:48 +#, python-brace-format +msgid "Follow-up reached: review required for risk '{t}'" +msgstr "" + #: templates/risks/dashboard.html:9 msgid "Dashboard" msgstr "" @@ -233,6 +341,26 @@ msgstr "" msgid "Risks by CIA" msgstr "" +#: templates/risks/item_risk.html:68 templates/risks/item_risk.html:114 +#: templates/risks/list_risks.html:86 +msgid "Likelihood" +msgstr "" + +#: templates/risks/item_risk.html:77 templates/risks/item_risk.html:123 +#: templates/risks/list_risks.html:87 +msgid "Impact" +msgstr "" + +#: templates/risks/item_risk.html:86 templates/risks/item_risk.html:132 +#: templates/risks/list_risks.html:89 +msgid "Level" +msgstr "" + +#: templates/risks/item_risk.html:95 templates/risks/item_risk.html:140 +#: templates/risks/list_risks.html:88 +msgid "Score" +msgstr "" + #: templates/risks/list_risks.html:4 msgid "Risk analysis" msgstr "" @@ -253,19 +381,3 @@ msgstr "" #: templates/risks/list_risks.html:84 msgid "Asset / Process" msgstr "" - -#: templates/risks/list_risks.html:86 -msgid "Likelihood" -msgstr "" - -#: templates/risks/list_risks.html:87 -msgid "Impact" -msgstr "" - -#: templates/risks/list_risks.html:88 -msgid "Score" -msgstr "" - -#: templates/risks/list_risks.html:89 -msgid "Level" -msgstr "" diff --git a/risks/admin.py b/risks/admin.py index 0616cf8..b1a783f 100644 --- a/risks/admin.py +++ b/risks/admin.py @@ -1,12 +1,25 @@ 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 +from .models import Control, Incident, NotificationPreference , Risk, ResidualRisk, User admin.site.site_header = _("Administration") admin.site.site_title = _("Admin") admin.site.index_title = _("Administration") +class NotificationPreferenceInline(admin.StackedInline): + model = NotificationPreference + can_delete = False + extra = 0 + fieldsets = ( + (_("Risks"), {"fields": ("risk_created","risk_updated","risk_deleted")}), + (_("Controls"), {"fields": ("control_created","control_updated","control_deleted")}), + (_("Residual risks"), {"fields": ("residual_created","residual_updated","residual_deleted")}), + (_("Reviews"), {"fields": ("review_required","review_completed")}), + (_("Incidents"), {"fields": ("incident_created","incident_updated","incident_deleted")}), + (_("Users"), {"fields": ("user_created","user_deleted")}), + ) + @admin.register(User) class UserAdmin(BaseUserAdmin): fieldsets = BaseUserAdmin.fieldsets + ( @@ -14,6 +27,8 @@ class UserAdmin(BaseUserAdmin): ) list_display = ("username", "email", "is_staff", "is_superuser", "is_sso_user", "owned_risks_count", "responsible_controls_count") + + inlines = [NotificationPreferenceInline] def owned_risks_count(self, obj): return obj.risks_owned.count() @@ -44,6 +59,7 @@ class RiskAdmin(admin.ModelAdmin): list_display = ( "title", "owner_name", + "status", "score", "level", "likelihood", @@ -56,7 +72,7 @@ class RiskAdmin(admin.ModelAdmin): return "-" return obj.owner.get_full_name() or obj.owner.username - list_filter = ("level", "likelihood", "impact", "owner") + list_filter = ("status", "level", "likelihood", "impact", "owner") search_fields = ("title", "asset", "process", "category") inlines = [ResidualRiskInline, ControlRisksInline] @@ -117,4 +133,5 @@ class IncidentAdmin(admin.ModelAdmin): def delete_model(self, request, obj): obj._changed_by = request.user - super().delete_model(request, obj) \ No newline at end of file + super().delete_model(request, obj) + diff --git a/risks/migrations/0021_risk_status_notificationpreference.py b/risks/migrations/0021_risk_status_notificationpreference.py new file mode 100644 index 0000000..c1059b8 --- /dev/null +++ b/risks/migrations/0021_risk_status_notificationpreference.py @@ -0,0 +1,71 @@ +# Generated by Django 5.2.6 on 2025-09-10 09:18 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("risks", "0020_alter_residualrisk_impact_alter_risk_impact"), + ] + + operations = [ + migrations.AddField( + model_name="risk", + name="status", + field=models.CharField( + choices=[ + ("open", "Open"), + ("in_progress", "In Progress"), + ("closed", "Closed"), + ("review_required", "Review required"), + ], + db_index=True, + default="open", + max_length=20, + verbose_name="Status", + ), + ), + migrations.CreateModel( + name="NotificationPreference", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("risk_created", models.BooleanField(default=True)), + ("risk_updated", models.BooleanField(default=True)), + ("risk_deleted", models.BooleanField(default=True)), + ("control_created", models.BooleanField(default=True)), + ("control_updated", models.BooleanField(default=True)), + ("control_deleted", models.BooleanField(default=True)), + ("residual_created", models.BooleanField(default=True)), + ("residual_updated", models.BooleanField(default=True)), + ("residual_deleted", models.BooleanField(default=True)), + ("review_required", models.BooleanField(default=True)), + ("review_completed", models.BooleanField(default=True)), + ("user_created", models.BooleanField(default=False)), + ("user_deleted", models.BooleanField(default=False)), + ("incident_created", models.BooleanField(default=True)), + ("incident_updated", models.BooleanField(default=True)), + ("incident_deleted", models.BooleanField(default=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="notification_preference", + to=settings.AUTH_USER_MODEL, + verbose_name="User", + ), + ), + ], + ), + ] diff --git a/risks/models.py b/risks/models.py index 07c739a..426dbd9 100644 --- a/risks/models.py +++ b/risks/models.py @@ -35,6 +35,12 @@ class Risk(models.Model): verbose_name = _("Risk") verbose_name_plural = _("Risks") + STATUS_CHOICES = [ + ("open", _("Open")), + ("in_progress", _("In Progress")), + ("closed", _("Closed")), + ("review_required", _("Review required")), + ] LIKELIHOOD_CHOICES = [ (1, _("Very low – occurs less than once every 5 years")), (2, _("Low – once every 1–5 years")), @@ -63,6 +69,14 @@ class Risk(models.Model): created_at = models.DateTimeField(_("Created at"), auto_now_add=True) updated_at = models.DateTimeField(_("Updated at"), auto_now=True) + status = models.CharField( + _("Status"), + max_length=20, + choices=STATUS_CHOICES, + default="open", + db_index=True, + ) + # CIA Protection Goals cia = MultiSelectField(choices=CIA_CHOICES, max_length=100, blank=True, null=True) @@ -271,4 +285,52 @@ class Notification(models.Model): def __str__(self): user_display = self.user.username if self.user else "System" - return f"{user_display}: {self.message[:50]}..." \ No newline at end of file + return f"{user_display}: {self.message[:50]}..." + +class NotificationPreference(models.Model): + """ + Wich events does the user want to receive as notifications? + """ + user = models.OneToOneField( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="notification_preference", + verbose_name=_("User"), + ) + + # Risks + risk_created = models.BooleanField(default=True) + risk_updated = models.BooleanField(default=True) + risk_deleted = models.BooleanField(default=True) + + # Controls + control_created = models.BooleanField(default=True) + control_updated = models.BooleanField(default=True) + control_deleted = models.BooleanField(default=True) + + # Residual risks + residual_created = models.BooleanField(default=True) + residual_updated = models.BooleanField(default=True) + residual_deleted = models.BooleanField(default=True) + + # Reviews + review_required = models.BooleanField(default=True) + review_completed = models.BooleanField(default=True) + + # Users + user_created = models.BooleanField(default=False) + user_deleted = models.BooleanField(default=False) + + # Incidents + incident_created = models.BooleanField(default=True) + incident_updated = models.BooleanField(default=True) + incident_deleted = models.BooleanField(default=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"Prefs({self.user})" + + def should_notify(self, event_code: str) -> bool: + return bool(getattr(self, event_code, False)) \ No newline at end of file diff --git a/risks/serializers.py b/risks/serializers.py index fb12561..91a1642 100644 --- a/risks/serializers.py +++ b/risks/serializers.py @@ -53,6 +53,7 @@ class RiskSerializer(serializers.ModelSerializer): "impact", "score", "level", + "status", "owner", "follow_up", "cia", diff --git a/risks/signals.py b/risks/signals.py index 599a838..99b2d38 100644 --- a/risks/signals.py +++ b/risks/signals.py @@ -1,15 +1,19 @@ from datetime import date, datetime +from django.contrib.auth import get_user_model from django.db.models import Model from django.db.models.signals import post_save, post_delete, m2m_changed from django.dispatch import receiver +from django.utils.translation import gettext_lazy as _ from .audit_context import get_current_user -from .models import Control, Risk, ResidualRisk, AuditLog, Incident +from .models import Control, Risk, ResidualRisk, AuditLog, Incident, Notification, NotificationPreference from .utils import model_diff # --------------------------------------------------------------------------- # General definitions # --------------------------------------------------------------------------- +User = get_user_model() + def serialize_value(value): if isinstance(value, Model): return value.pk # oder str(value), wenn du mehr Infos willst @@ -17,9 +21,65 @@ def serialize_value(value): return value.isoformat() return value +def _pref(user: User) -> NotificationPreference | None: + if not user: + return None + pref = getattr(user, "notification_preference", None) + if not pref: + pref = NotificationPreference.objects.create(user=user) + return pref + +def _notify(users, message: str, event_code: str): + """legt Notification für alle users an, die dieses Event wünschen.""" + for u in set(filter(None, users)): + pref = _pref(u) + if pref and pref.should_notify(event_code): + Notification.objects.create(user=u, message=message) + +def _risk_stakeholders(risk: Risk): + """Risikoeigner + alle Verantwortlichen zugehöriger Controls.""" + owners = [risk.owner] if risk.owner else [] + responsibles = list( + User.objects.filter(responsible_controls__risks=risk).distinct() + ) + return set(owners + responsibles) + +# --------------------------------------------------------------------------- +# Incidents +# --------------------------------------------------------------------------- +@receiver(post_save, sender=User) +def user_saved(sender, instance: User, created, **kwargs): + # Prefs automatisch anlegen + _pref(instance) + # An Staff, die dieses Event wollen + if created: + staff = User.objects.filter(is_staff=True, notification_preference__user_created=True) + _notify(staff, _("User '{u}' created").format(u=instance.username), "user_created") + +@receiver(post_delete, sender=User) +def user_deleted(sender, instance: User, **kwargs): + staff = User.objects.filter(is_staff=True, notification_preference__user_deleted=True) + _notify(staff, _("User '{u}' deleted").format(u=instance.username), "user_deleted") + # --------------------------------------------------------------------------- # Risks # --------------------------------------------------------------------------- +@receiver(post_save, sender=Risk) +def risk_saved(sender, instance: Risk, created, **kwargs): + event = "risk_created" if created else "risk_updated" + msg = _("Risk '{title}' {state}").format( + title=instance.title, + state=_("created") if created else _("updated"), + ) + _notify([instance.owner], msg, event) + +@receiver(post_delete, sender=Risk) +def risk_deleted(sender, instance: Risk, **kwargs): + msg = _("Risk '{title}' deleted").format(title=instance.title) + # Owner existiert evtl. nicht mehr -> kein Notify nötig + if instance.owner: + _notify([instance.owner], msg, "risk_deleted") + @receiver(post_save, sender=Risk) def log_risk_save(sender, instance, created, **kwargs): if created: @@ -68,6 +128,32 @@ def log_risk_delete(sender, instance, **kwargs): # --------------------------------------------------------------------------- # Controls # --------------------------------------------------------------------------- +@receiver(post_save, sender=Control) +def control_saved(sender, instance: Control, created, **kwargs): + # Review-Flag für alle betroffenen Residuals setzen + for risk in instance.risks.all(): + resid, created = ResidualRisk.objects.get_or_create(risk=risk) + # Statuswechsel auf Review Required + if not resid.review_required: + resid.review_required = True + resid.save() + if risk.status != "review_required": + Risk.objects.filter(pk=risk.pk).update(status="review_required") + + # Notifications + event = "control_created" if created else "control_updated" + msg = _("Control '{title}' {state}").format( + title=instance.title, + state=_("created") if created else _("updated"), + ) + stakeholders = {instance.responsible} | set(r.owner for r in instance.risks.all() if r.owner) + _notify(stakeholders, msg, event) + +@receiver(post_delete, sender=Control) +def control_deleted(sender, instance: Control, **kwargs): + msg = _("Control '{title}' deleted").format(title=instance.title) + stakeholders = {instance.responsible} | set(r.owner for r in instance.risks.all() if r.owner) + _notify(stakeholders, msg, "control_deleted") @receiver(post_save, sender=Control) def log_control_save(sender, instance, created, **kwargs): @@ -112,28 +198,51 @@ def log_control_delete(sender, instance, **kwargs): ) @receiver(m2m_changed, sender=Control.risks.through) -def control_risks_changed(sender, instance, action, reverse, pk_set, **kwargs): +def control_risks_changed(sender, instance: Control, action, reverse, pk_set, **kwargs): if action in {"post_add", "post_remove", "post_clear"}: - if action == "post_clear": - 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() - - from .models import ResidualRisk - for risk in affected_risks: - residual, _ = ResidualRisk.objects.get_or_create(risk=risk) - residual.review_required = True - residual.save() + affected = instance.risks.all() if not pk_set else Risk.objects.filter(pk__in=pk_set) + for risk in affected: + resid, created = ResidualRisk.objects.get_or_create(risk=risk) + if not resid.review_required: + resid.review_required = True + resid.save() + if risk.status != "review_required": + Risk.objects.filter(pk=risk.pk).update(status="review_required") + _notify(_risk_stakeholders(risk), _("Review required for risk '{t}' due to control change").format(t=risk.title), "review_required") # --------------------------------------------------------------------------- # Residual risks # --------------------------------------------------------------------------- +@receiver(post_save, sender=ResidualRisk) +def residual_saved(sender, instance: ResidualRisk, created, **kwargs): + # AuditLog erstellst du bereits anderswo – hier Fokus auf Status/Notify + risk = instance.risk + old = None + if not created: + try: + old = ResidualRisk.objects.get(pk=instance.pk) + except ResidualRisk.DoesNotExist: + pass + + # Review-Logik: wenn review_required=True -> Risk.status = review_required + if instance.review_required and risk.status != "review_required": + Risk.objects.filter(pk=risk.pk).update(status="review_required") + _notify(_risk_stakeholders(risk), _("Review required for risk '{t}'").format(t=risk.title), "review_required") + elif old and old.review_required and not instance.review_required: + # Review abgeschlossen + if risk.status == "review_required": + Risk.objects.filter(pk=risk.pk).update(status="open") + _notify(_risk_stakeholders(risk), _("Review completed for risk '{t}'").format(t=risk.title), "review_completed") + + # Standard-Events + event = "residual_created" if created else "residual_updated" + _notify(_risk_stakeholders(risk), _("Residual risk {state} for '{t}'").format( + state=_("created") if created else _("updated"), t=risk.title), event) + +@receiver(post_delete, sender=ResidualRisk) +def residual_deleted(sender, instance: ResidualRisk, **kwargs): + _notify(_risk_stakeholders(instance.risk), _("Residual risk deleted for '{t}'").format(t=instance.risk.title), "residual_deleted") + @receiver(post_save, sender=ResidualRisk) def log_residual_save(sender, instance, created, **kwargs): @@ -180,6 +289,16 @@ def log_residual_delete(sender, instance, **kwargs): # --------------------------------------------------------------------------- # Incidents # --------------------------------------------------------------------------- +@receiver(post_save, sender=Incident) +def incident_saved(sender, instance: Incident, created, **kwargs): + event = "incident_created" if created else "incident_updated" + stakeholders = set([instance.reported_by]) | set(r.owner for r in instance.related_risks.all() if r.owner) + _notify(stakeholders, _("Incident '{t}' {s}").format(t=instance.title, s=_("created") if created else _("updated")), event) + +@receiver(post_delete, sender=Incident) +def incident_deleted(sender, instance: Incident, **kwargs): + stakeholders = set([instance.reported_by]) | set(r.owner for r in instance.related_risks.all() if r.owner) + _notify(stakeholders, _("Incident '{t}' deleted").format(t=instance.title), "incident_deleted") @receiver(post_save, sender=Incident) def log_incident_save(sender, instance, created, **kwargs): diff --git a/risks/urls.py b/risks/urls.py index 6fbc91a..b450e3f 100644 --- a/risks/urls.py +++ b/risks/urls.py @@ -6,7 +6,6 @@ app_name = "risks" urlpatterns = [ path("", views.dashboard, name="dashboard"), path("risks/index", views.dashboard, name="index"), - path("risks/stats", views.stats, name="statistics"), path("risks/list_risks", views.list_risks, name="list_risks"), path("risks/risks/", views.show_risk, name="show_risk"), path("risks/list_controls", views.list_controls, name="list_controls"), diff --git a/risks/utils.py b/risks/utils.py index 36b1cec..c71dff4 100644 --- a/risks/utils.py +++ b/risks/utils.py @@ -1,5 +1,7 @@ +from django.contrib.auth import get_user_model from django.utils.timezone import now -from .models import AuditLog, Risk, Notification +from django.utils.translation import gettext_lazy as _ +from .models import AuditLog, Notification, Risk, ResidualRisk def model_diff(old, new, fields=None): """ @@ -29,21 +31,28 @@ def check_risk_followups(): Ensures no duplicate notifications per risk per day """ today = now().date() - risks = Risk.objects.filter(follow_up__lte=today) + risks = Risk.objects.filter(follow_up__lte=today).select_related("owner") for risk in risks: - if risk.owner: - notification, created = Notification.objects.get_or_create( - user=risk.owner, - message=f"Follow-up required for risk: {risk.title}", - defaults={"read": False, "sent": False}, - ) + # Risk-Status auf review_required setzen (nicht überschreiben, wenn bereits closed) + if risk.status != "closed" and risk.status != "review_required": + Risk.objects.filter(pk=risk.pk).update(status="review_required") - if created: - AuditLog.objects.create( - user=None, # system action - action="create", - model="Notification", - object_id=notification.pk, - changes={"message": notification.message, "user": risk.owner.username}, - ) \ No newline at end of file + # ResidualRisk-Objekt sicherstellen und Review-Flag setzen + resid, created = ResidualRisk.objects.get_or_create(risk=risk) + if not resid.review_required: + resid.review_required = True + resid.save() + + # Notification an Stakeholder + message = _("Follow-up reached: review required for risk '{t}'").format(t=risk.title) + notification, created = Notification.objects.get_or_create( + user=risk.owner, + message=message, + defaults={"read": False, "sent": False}, + ) + if created: + AuditLog.objects.create( + user=None, action="create", model="Notification", object_id=notification.pk, + changes={"message": notification.message, "user": risk.owner.username if risk.owner else None}, + ) \ No newline at end of file diff --git a/risks/views.py b/risks/views.py index 1a994e7..e7d3569 100644 --- a/risks/views.py +++ b/risks/views.py @@ -50,6 +50,9 @@ class ControlViewSet(viewsets.ModelViewSet): instance._changed_by = self.request.user class ResidualRiskViewSet(viewsets.ModelViewSet): + """ + API endpoint for Residual risks. + """ queryset = ResidualRisk.objects.all() serializer_class = ResidualRiskSerializer permission_classes = [IsAuthenticated] @@ -98,12 +101,11 @@ class IncidentViewSet(viewsets.ModelViewSet): # Web => Risks, Controls, Incidents # --------------------------------------------------------------------------- -@login_required -def stats(request): - return render(request, "risks/statistics.html") - @login_required def list_risks(request): + """ + View for listing all Risks + """ qs = Risk.objects.all().select_related("owner") # GET-Parameter lesen @@ -131,10 +133,14 @@ def list_risks(request): @login_required def show_risk(request, id): + """ + View for single risk + """ 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") @@ -142,6 +148,9 @@ def show_risk(request, id): @login_required def list_controls(request): + """ + View for listing all Controls + """ qs = Control.objects.all().select_related("responsible") control_id = request.GET.get("control") @@ -183,6 +192,9 @@ def show_control(request, id): @login_required def list_incidents(request): + """ + View for listing all Incidents + """ qs = Incident.objects.all().select_related("reported_by").prefetch_related("related_risks") risk_id = request.GET.get("risk") @@ -218,13 +230,11 @@ def show_incident(request, id): ).order_by("-action_time") return render(request, "risks/item_incident.html", {"incident": incident, "logs": logs}) - -# --------------------------------------------------------------------------- -# Web => Dashboard -# --------------------------------------------------------------------------- - @login_required def dashboard(request): + """ + Dashboardview with KPIs + """ # Risikoübersicht risks_total = Risk.objects.count() risks_by_level = Risk.objects.values('level').annotate(count=Count('id')) diff --git a/templates/risks/item_risk.html b/templates/risks/item_risk.html index 2a3267b..20ba0fa 100644 --- a/templates/risks/item_risk.html +++ b/templates/risks/item_risk.html @@ -209,6 +209,37 @@ + +
+
+

Vorfälle

+
+
+ {% if risk.incidents.exists %} + + + + + + + + + + {% for i in risk.incidents.all %} + + + + + + {% endfor %} + +
VorfallStatusgemeldet am
{{ i.title }}{{ i.status }}{{ i.created_at|date:"d.m.Y H:i" }}
+ {% else %} +

Keine Vorfälle bekannt.

+ {% endif %} +
+
+