refactor risks models
This commit is contained in:
parent
bfd1c081e9
commit
65dc2231bb
42 changed files with 625 additions and 1363 deletions
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
|
@ -1,9 +1,11 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-05 20:00
|
||||
# Generated by Django 5.2.6 on 2025-09-22 06:32
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import multiselectfield.db.fields
|
||||
import risks.models.auditlog
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
@ -14,9 +16,26 @@ class Migration(migrations.Migration):
|
|||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='NotificationRule',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('kind', models.CharField(choices=[('risk.created', 'Risk created'), ('risk.updated', 'Risk updated'), ('risk.deleted', 'Risk deleted'), ('risk.review_required', 'Risk review required'), ('risk.review_completed', 'Risk review completed'), ('control.created', 'Control created'), ('control.updated', 'Control updated'), ('control.deleted', 'Control deleted'), ('residual.created', 'Residual created'), ('residual.updated', 'Residual updated'), ('residual.deleted', 'Residual deleted'), ('residual.review_required', 'Residual review required'), ('residual.review_completed', 'Residual review completed'), ('incident.created', 'Incident created'), ('incident.updated', 'Incident updated'), ('incident.deleted', 'Incident deleted'), ('user.created', 'User created'), ('user.deleted', 'User deleted')], max_length=40, unique=True, verbose_name='Event')),
|
||||
('enabled_in_app', models.BooleanField(default=True, verbose_name='Show in app')),
|
||||
('enabled_email', models.BooleanField(default=False, verbose_name='Send via email')),
|
||||
('to_owner', models.BooleanField(default=True, verbose_name='Send to owner/responsible/reporter (if available)')),
|
||||
('to_staff', models.BooleanField(default=False, verbose_name='Send to all staff')),
|
||||
('extra_recipients', models.TextField(blank=True, verbose_name='Extra recipients (emails, comma or newline separated)')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Notification rule',
|
||||
'verbose_name_plural': 'Notification rules',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
|
@ -44,36 +63,145 @@ class Migration(migrations.Migration):
|
|||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AuditLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action', models.CharField(choices=[('create', 'Created'), ('update', 'Updated'), ('delete', 'Deleted')], max_length=10)),
|
||||
('model', models.CharField(max_length=100)),
|
||||
('object_id', models.CharField(max_length=50)),
|
||||
('changes', models.JSONField(blank=True, encoder=risks.models.auditlog.SafeJSONEncoder, null=True)),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='audit_logs', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Auditlog',
|
||||
'verbose_name_plural': 'Auditlogs',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('message', models.TextField()),
|
||||
('kind', models.CharField(choices=[('risk.created', 'Risk created'), ('risk.updated', 'Risk updated'), ('risk.deleted', 'Risk deleted'), ('risk.review_required', 'Risk review required'), ('risk.review_completed', 'Risk review completed'), ('control.created', 'Control created'), ('control.updated', 'Control updated'), ('control.deleted', 'Control deleted'), ('residual.created', 'Residual created'), ('residual.updated', 'Residual updated'), ('residual.deleted', 'Residual deleted'), ('residual.review_required', 'Residual review required'), ('residual.review_completed', 'Residual review completed'), ('incident.created', 'Incident created'), ('incident.updated', 'Incident updated'), ('incident.deleted', 'Incident deleted'), ('user.created', 'User created'), ('user.deleted', 'User deleted')], default='', max_length=40)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('read', models.BooleanField(default=False)),
|
||||
('sent', models.BooleanField(default=False)),
|
||||
('object_id', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('target_url', models.CharField(blank=True, max_length=500, null=True)),
|
||||
('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='notifications', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Notification',
|
||||
'verbose_name_plural': 'Notifications',
|
||||
},
|
||||
),
|
||||
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')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Risk',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('asset', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('process', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('category', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('title', models.CharField(max_length=255, verbose_name='Title')),
|
||||
('description', models.TextField(blank=True, max_length=225, null=True, verbose_name='Description')),
|
||||
('asset', models.CharField(blank=True, max_length=255, null=True, verbose_name='Asset')),
|
||||
('process', models.CharField(blank=True, max_length=255, null=True, verbose_name='Process')),
|
||||
('category', models.CharField(blank=True, max_length=255, null=True, verbose_name='Category')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('effects', models.TextField(blank=True, null=True, verbose_name='Effects')),
|
||||
('status', 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')),
|
||||
('cia', multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('1', 'Confidentiality'), ('2', 'Integrity'), ('3', 'Availability')], max_length=100, null=True)),
|
||||
('likelihood', models.IntegerField(choices=[(1, 'Very low – occurs less than once every 5 years'), (2, 'Low – once every 1–5 years'), (3, 'Likely – once per year or more'), (4, 'Very likely – multiple times per year/monthly')], default=1)),
|
||||
('impact', models.IntegerField(choices=[(1, 'Low (< 1,000 € – minor operational impact)'), (2, 'Medium (1,000–5,000 € – local impact)'), (3, 'High (5,000–15,000 € – team-level impact)'), (4, 'Severe (50,000–100,000 € – regional impact)'), (5, 'Critical (> 100,000 € – existential threat)')], default=1)),
|
||||
('impact', 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)),
|
||||
('score', models.IntegerField(editable=False)),
|
||||
('level', models.CharField(editable=False, max_length=50)),
|
||||
('follow_up', models.DateField(blank=True, null=True)),
|
||||
('confidentiality', models.BooleanField(default=False)),
|
||||
('integrity', models.BooleanField(default=False)),
|
||||
('availability', models.BooleanField(default=False)),
|
||||
('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_risks', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Risk',
|
||||
'verbose_name_plural': 'Risks',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ResidualRisk',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('likelihood', models.IntegerField(choices=[(1, 'Very low – occurs less than once every 5 years'), (2, 'Low – once every 1–5 years'), (3, 'Likely – once per year or more'), (4, 'Very likely – multiple times per year/monthly')], default=1)),
|
||||
('impact', 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)),
|
||||
('score', models.IntegerField(editable=False)),
|
||||
('level', models.CharField(editable=False, max_length=50)),
|
||||
('review_required', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('risk', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='residual_risk', to='risks.risk')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Residual Risk',
|
||||
'verbose_name_plural': 'Residual Risks',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Incident',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255, verbose_name='Title')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
|
||||
('date_reported', models.DateField(blank=True, null=True, verbose_name='Date reported')),
|
||||
('status', models.CharField(choices=[('open', 'Opened'), ('in_progress', 'In Progress'), ('closed', 'Closed')], max_length=12)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('reported_by', 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')),
|
||||
('related_risks', models.ManyToManyField(blank=True, related_name='incidents', to='risks.risk')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Incident',
|
||||
'verbose_name_plural': 'Incidents',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Control',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('title', models.CharField(max_length=255, verbose_name='Title')),
|
||||
('status', models.CharField(choices=[('planned', 'Planned'), ('in_progress', 'In progress'), ('completed', 'Completed'), ('verified', 'Verified'), ('rejected', 'Rejected')], default='planned', max_length=20)),
|
||||
('due_date', models.DateField(blank=True, null=True)),
|
||||
('description', models.TextField(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)),
|
||||
('responsible', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='responsible_controls', to=settings.AUTH_USER_MODEL)),
|
||||
('risk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='controls', to='risks.risk')),
|
||||
('risks', models.ManyToManyField(blank=True, related_name='controls', to='risks.risk')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Control',
|
||||
'verbose_name_plural': 'Controls',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-06 10:52
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('risks', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ResidualRisk',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('likelihood', models.IntegerField(choices=[(1, 'Very low – occurs less than once every 5 years'), (2, 'Low – once every 1–5 years'), (3, 'Likely – once per year or more'), (4, 'Very likely – multiple times per year/monthly')], default=1)),
|
||||
('impact', models.IntegerField(choices=[(1, 'Low (< 1,000 € – minor operational impact)'), (2, 'Medium (1,000–5,000 € – local impact)'), (3, 'High (5,000–15,000 € – team-level impact)'), (4, 'Severe (50,000–100,000 € – regional impact)'), (5, 'Critical (> 100,000 € – existential threat)')], default=1)),
|
||||
('score', models.IntegerField(editable=False)),
|
||||
('level', models.CharField(editable=False, max_length=50)),
|
||||
('risk', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='residual_risk', to='risks.risk')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-06 11:39
|
||||
|
||||
import multiselectfield.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('risks', '0002_residualrisk'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='risk',
|
||||
name='cia',
|
||||
field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[(1, 'Confidentiality'), (2, 'Integrity'), (3, 'Availability')], max_length=100, null=True),
|
||||
),
|
||||
]
|
|
@ -1,44 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-07 09:52
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('risks', '0003_risk_cia'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='risk',
|
||||
name='availability',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='risk',
|
||||
name='confidentiality',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='risk',
|
||||
name='integrity',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='residualrisk',
|
||||
name='review_required',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AuditLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action', models.CharField(choices=[('create', 'Created'), ('update', 'Updated'), ('delete', 'Deleted')], max_length=10)),
|
||||
('model', models.CharField(max_length=100)),
|
||||
('object_id', models.CharField(max_length=50)),
|
||||
('changes', models.JSONField(blank=True, null=True)),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='audit_logs', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,27 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-07 10:37
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('risks', '0004_remove_risk_availability_remove_risk_confidentiality_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Incidents',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('date_reported', models.DateField(blank=True, null=True)),
|
||||
('status', models.CharField(choices=[('open', 'Opened'), ('in_progress', 'In Progress'), ('close', 'Closed')], max_length=12)),
|
||||
('related_risks', models.ManyToManyField(blank=True, related_name='incidents', to='risks.risk')),
|
||||
('reported_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incidents', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,17 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-07 16:55
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('risks', '0005_incidents'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='Incidents',
|
||||
new_name='Incident',
|
||||
),
|
||||
]
|
|
@ -1,26 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-07 18:31
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('risks', '0006_rename_incidents_incident'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('message', models.TextField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('read', models.BooleanField(default=False)),
|
||||
('send', models.BooleanField(default=False)),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='notifications', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-07 19:59
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('risks', '0007_notification'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='notification',
|
||||
old_name='send',
|
||||
new_name='sent',
|
||||
),
|
||||
]
|
|
@ -1,25 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-08 09:15
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('risks', '0008_rename_send_notification_sent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='risk',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='risk',
|
||||
name='updatet_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
|
@ -1,39 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-08 09:44
|
||||
|
||||
import multiselectfield.db.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('risks', '0009_risk_created_at_risk_updatet_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='residualrisk',
|
||||
name='impact',
|
||||
field=models.IntegerField(choices=[('1', 'Low (< 1,000 € – minor operational impact)'), ('2', 'Medium (1,000–5,000 € – local impact)'), ('3', 'High (5,000–15,000 € – team-level impact)'), ('4', 'Severe (50,000–100,000 € – regional impact)'), ('5', 'Critical (> 100,000 € – existential threat)')], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='residualrisk',
|
||||
name='likelihood',
|
||||
field=models.IntegerField(choices=[('1', 'Very low – occurs less than once every 5 years'), ('2', 'Low – once every 1–5 years'), ('3', 'Likely – once per year or more'), ('4', 'Very likely – multiple times per year/monthly')], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='risk',
|
||||
name='cia',
|
||||
field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('1', 'Confidentiality'), ('2', 'Integrity'), ('3', 'Availability')], max_length=100, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='risk',
|
||||
name='impact',
|
||||
field=models.IntegerField(choices=[('1', 'Low (< 1,000 € – minor operational impact)'), ('2', 'Medium (1,000–5,000 € – local impact)'), ('3', 'High (5,000–15,000 € – team-level impact)'), ('4', 'Severe (50,000–100,000 € – regional impact)'), ('5', 'Critical (> 100,000 € – existential threat)')], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='risk',
|
||||
name='likelihood',
|
||||
field=models.IntegerField(choices=[('1', 'Very low – occurs less than once every 5 years'), ('2', 'Low – once every 1–5 years'), ('3', 'Likely – once per year or more'), ('4', 'Very likely – multiple times per year/monthly')], default=1),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-08 09:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('risks', '0010_alter_residualrisk_impact_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='risk',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, max_length=225, null=True),
|
||||
),
|
||||
]
|
|
@ -1,33 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-08 09:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('risks', '0011_risk_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='residualrisk',
|
||||
name='impact',
|
||||
field=models.IntegerField(choices=[(1, 'Low (< 1,000 € – minor operational impact)'), (2, 'Medium (1,000–5,000 € – local impact)'), (3, 'High (5,000–15,000 € – team-level impact)'), (4, 'Severe (50,000–100,000 € – regional impact)'), (5, 'Critical (> 100,000 € – existential threat)')], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='residualrisk',
|
||||
name='likelihood',
|
||||
field=models.IntegerField(choices=[(1, 'Very low – occurs less than once every 5 years'), (2, 'Low – once every 1–5 years'), (3, 'Likely – once per year or more'), (4, 'Very likely – multiple times per year/monthly')], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='risk',
|
||||
name='impact',
|
||||
field=models.IntegerField(choices=[(1, 'Low (< 1,000 € – minor operational impact)'), (2, 'Medium (1,000–5,000 € – local impact)'), (3, 'High (5,000–15,000 € – team-level impact)'), (4, 'Severe (50,000–100,000 € – regional impact)'), (5, 'Critical (> 100,000 € – existential threat)')], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='risk',
|
||||
name='likelihood',
|
||||
field=models.IntegerField(choices=[(1, 'Very low – occurs less than once every 5 years'), (2, 'Low – once every 1–5 years'), (3, 'Likely – once per year or more'), (4, 'Very likely – multiple times per year/monthly')], default=1),
|
||||
),
|
||||
]
|
|
@ -1,52 +0,0 @@
|
|||
# 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),
|
||||
),
|
||||
]
|
|
@ -1,21 +0,0 @@
|
|||
# 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"),
|
||||
),
|
||||
]
|
|
@ -1,20 +0,0 @@
|
|||
# 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
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,32 +0,0 @@
|
|||
# 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",
|
||||
),
|
||||
]
|
|
@ -1,24 +0,0 @@
|
|||
# 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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,108 +0,0 @@
|
|||
# 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"),
|
||||
),
|
||||
]
|
|
@ -1,16 +0,0 @@
|
|||
# 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"},
|
||||
),
|
||||
]
|
|
@ -1,40 +0,0 @@
|
|||
# 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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,71 +0,0 @@
|
|||
# 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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-10 10:53
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("risks", "0021_risk_status_notificationpreference"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="notification",
|
||||
options={
|
||||
"verbose_name": "Notification",
|
||||
"verbose_name_plural": "Notifications",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,86 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-10 12:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("risks", "0022_alter_notification_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="NotificationRule",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"kind",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("risk.created", "Risk created"),
|
||||
("risk.updated", "Risk updated"),
|
||||
("risk.deleted", "Risk deleted"),
|
||||
("risk.review_required", "Risk review required"),
|
||||
("risk.review_completed", "Risk review completed"),
|
||||
("control.created", "Control created"),
|
||||
("control.updated", "Control updated"),
|
||||
("control.deleted", "Control deleted"),
|
||||
("residual.created", "Residual created"),
|
||||
("residual.updated", "Residual updated"),
|
||||
("residual.deleted", "Residual deleted"),
|
||||
("residual.review_required", "Residual review required"),
|
||||
("residual.review_completed", "Residual review completed"),
|
||||
("incident.created", "Incident created"),
|
||||
("incident.updated", "Incident updated"),
|
||||
("incident.deleted", "Incident deleted"),
|
||||
("user.created", "User created"),
|
||||
("user.deleted", "User deleted"),
|
||||
],
|
||||
max_length=40,
|
||||
unique=True,
|
||||
verbose_name="Event",
|
||||
),
|
||||
),
|
||||
(
|
||||
"enabled_in_app",
|
||||
models.BooleanField(default=True, verbose_name="Show in app"),
|
||||
),
|
||||
(
|
||||
"enabled_email",
|
||||
models.BooleanField(default=False, verbose_name="Send via email"),
|
||||
),
|
||||
(
|
||||
"to_owner",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
verbose_name="Send to owner/responsible/reporter (if available)",
|
||||
),
|
||||
),
|
||||
(
|
||||
"to_staff",
|
||||
models.BooleanField(
|
||||
default=False, verbose_name="Send to all staff"
|
||||
),
|
||||
),
|
||||
(
|
||||
"extra_recipients",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
verbose_name="Extra recipients (emails, comma or newline separated)",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Notification rule",
|
||||
"verbose_name_plural": "Notification rules",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,17 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-10 12:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("risks", "0023_notificationrule"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="risk",
|
||||
name="effects",
|
||||
field=models.TextField(blank=True, null=True, verbose_name="Effects"),
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-10 12:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("risks", "0024_risk_effects"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="control",
|
||||
name="risks",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, null=True, related_name="controls", to="risks.risk"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-10 12:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("risks", "0025_alter_control_risks"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="control",
|
||||
name="risks",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name="controls", to="risks.risk"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,57 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-12 10:44
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
("risks", "0026_alter_control_risks"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="notification",
|
||||
name="content_type",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="contenttypes.contenttype",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="notification",
|
||||
name="kind",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("risk.created", "Risk created"),
|
||||
("risk.updated", "Risk updated"),
|
||||
("risk.deleted", "Risk deleted"),
|
||||
("risk.review_required", "Risk review required"),
|
||||
("risk.review_completed", "Risk review completed"),
|
||||
("control.created", "Control created"),
|
||||
("control.updated", "Control updated"),
|
||||
("control.deleted", "Control deleted"),
|
||||
("residual.created", "Residual created"),
|
||||
("residual.updated", "Residual updated"),
|
||||
("residual.deleted", "Residual deleted"),
|
||||
("residual.review_required", "Residual review required"),
|
||||
("residual.review_completed", "Residual review completed"),
|
||||
("incident.created", "Incident created"),
|
||||
("incident.updated", "Incident updated"),
|
||||
("incident.deleted", "Incident deleted"),
|
||||
("user.created", "User created"),
|
||||
("user.deleted", "User deleted"),
|
||||
],
|
||||
default="",
|
||||
max_length=40,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="notification",
|
||||
name="object_id",
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -1,17 +0,0 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-16 12:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("risks", "0027_notification_content_type_notification_kind_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="notification",
|
||||
name="target_url",
|
||||
field=models.CharField(blank=True, max_length=500, null=True),
|
||||
),
|
||||
]
|
443
risks/models.py
443
risks/models.py
|
@ -1,443 +0,0 @@
|
|||
import datetime
|
||||
import json
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from multiselectfield import MultiSelectField
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SafeJSONEncoder
|
||||
# ---------------------------------------------------------------------------
|
||||
class SafeJSONEncoder(DjangoJSONEncoder):
|
||||
"""JSON encoder that can handle datetime.date properly."""
|
||||
def default(self, obj):
|
||||
if isinstance(obj, datetime.date):
|
||||
return obj.isoformat()
|
||||
return super().default(obj)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# User
|
||||
# ---------------------------------------------------------------------------
|
||||
class User(AbstractUser):
|
||||
"""Custom user model to support both local and SSO users."""
|
||||
is_sso_user = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def risks_owned(self):
|
||||
"""All risks where the user is the risk owner."""
|
||||
return self.owned_risks.all()
|
||||
|
||||
@property
|
||||
def controls_responsible(self):
|
||||
"""All controls where the user is responsible."""
|
||||
return self.responsible_controls.all()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Risk
|
||||
# ---------------------------------------------------------------------------
|
||||
class Risk(models.Model):
|
||||
|
||||
class Meta:
|
||||
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")),
|
||||
(3, _("Likely – once per year or more")),
|
||||
(4, _("Very likely – multiple times per year/monthly")),
|
||||
]
|
||||
IMPACT_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)")),
|
||||
]
|
||||
CIA_CHOICES = [
|
||||
("1", _("Confidentiality")),
|
||||
("2", _("Integrity")),
|
||||
("3", _("Availability")),
|
||||
]
|
||||
|
||||
# Basic information
|
||||
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)
|
||||
effects = models.TextField(_("Effects"), blank=True, null=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)
|
||||
|
||||
# Risk evaluation before controls
|
||||
likelihood = models.IntegerField(choices=LIKELIHOOD_CHOICES, default=1)
|
||||
impact = models.IntegerField(choices=IMPACT_CHOICES, default=1)
|
||||
|
||||
# Calculated fields
|
||||
score = models.IntegerField(editable=False)
|
||||
level = models.CharField(max_length=50, editable=False)
|
||||
|
||||
# Ownership
|
||||
owner = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name="owned_risks"
|
||||
)
|
||||
|
||||
# Reminder / follow-up date
|
||||
follow_up = models.DateField(blank=True, null=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Mark for review if likelihood/impact changed
|
||||
if self.pk:
|
||||
old = Risk.objects.get(pk=self.pk)
|
||||
if old.likelihood != self.likelihood or old.impact != self.impact:
|
||||
self.review_required = True
|
||||
self.status = "review_required"
|
||||
|
||||
# Calculate risk score and level
|
||||
self.score = self.likelihood * self.impact
|
||||
if self.score <= 4:
|
||||
self.level = "Low"
|
||||
elif self.score <= 8:
|
||||
self.level = "Medium"
|
||||
elif self.score <= 12:
|
||||
self.level = "High"
|
||||
else:
|
||||
self.level = "Critical"
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title} (Score: {self.score}, Level: {self.level})"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Residual Risk
|
||||
# ---------------------------------------------------------------------------
|
||||
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, related_name="residual_risk")
|
||||
likelihood = models.IntegerField(choices=Risk.LIKELIHOOD_CHOICES, default=1)
|
||||
impact = models.IntegerField(choices=Risk.IMPACT_CHOICES, default=1)
|
||||
score = models.IntegerField(editable=False)
|
||||
level = models.CharField(max_length=50, editable=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):
|
||||
if self.pk:
|
||||
old = ResidualRisk.objects.get(pk=self.pk)
|
||||
if old.likelihood != self.likelihood or old.impact != self.impact:
|
||||
self.review_required = True
|
||||
self.status = "review_required"
|
||||
|
||||
# Calculate residual risk score and level
|
||||
self.score = self.likelihood * self.impact
|
||||
if self.score <= 4:
|
||||
self.level = "Low"
|
||||
elif self.score <= 8:
|
||||
self.level = "Medium"
|
||||
elif self.score <= 12:
|
||||
self.level = "High"
|
||||
else:
|
||||
self.level = "Critical"
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"Residual Risk for {self.risk.title} (Score: {self.score}, Level: {self.level})"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Control
|
||||
# ---------------------------------------------------------------------------
|
||||
class Control(models.Model):
|
||||
"""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")),
|
||||
]
|
||||
|
||||
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(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name="responsible_controls"
|
||||
)
|
||||
description = models.TextField(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
|
||||
risks = models.ManyToManyField(Risk, related_name="controls", blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title} ({self.get_status_display()})"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AuditLog
|
||||
# ---------------------------------------------------------------------------
|
||||
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"),
|
||||
("delete", "Deleted"),
|
||||
]
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True, blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="audit_logs"
|
||||
)
|
||||
action = models.CharField(max_length=10, choices=ACTION_CHOICES)
|
||||
model = models.CharField(max_length=100)
|
||||
object_id = models.CharField(max_length=50)
|
||||
changes = models.JSONField(null=True, blank=True, encoder=SafeJSONEncoder)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.timestamp}] {self.user} {self.action} {self.model}({self.object_id})"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Incident
|
||||
# ---------------------------------------------------------------------------
|
||||
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")),
|
||||
]
|
||||
|
||||
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)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# NotificationKind
|
||||
# ---------------------------------------------------------------------------
|
||||
class NotificationKind(models.TextChoices):
|
||||
"""Event types for notifications."""
|
||||
RISK_CREATED = "risk.created", _("Risk created")
|
||||
RISK_UPDATED = "risk.updated", _("Risk updated")
|
||||
RISK_DELETED = "risk.deleted", _("Risk deleted")
|
||||
RISK_REVIEW_REQUIRED = "risk.review_required", _("Risk review required")
|
||||
RISK_REVIEW_COMPLETED = "risk.review_completed", _("Risk review completed")
|
||||
|
||||
CONTROL_CREATED = "control.created", _("Control created")
|
||||
CONTROL_UPDATED = "control.updated", _("Control updated")
|
||||
CONTROL_DELETED = "control.deleted", _("Control deleted")
|
||||
|
||||
RESIDUAL_CREATED = "residual.created", _("Residual created")
|
||||
RESIDUAL_UPDATED = "residual.updated", _("Residual updated")
|
||||
RESIDUAL_DELETED = "residual.deleted", _("Residual deleted")
|
||||
RESIDUAL_REVIEW_REQUIRED = "residual.review_required", _("Residual review required")
|
||||
RESIDUAL_REVIEW_COMPLETED = "residual.review_completed", _("Residual review completed")
|
||||
|
||||
INCIDENT_CREATED = "incident.created", _("Incident created")
|
||||
INCIDENT_UPDATED = "incident.updated", _("Incident updated")
|
||||
INCIDENT_DELETED = "incident.deleted", _("Incident deleted")
|
||||
|
||||
USER_CREATED = "user.created", _("User created")
|
||||
USER_DELETED = "user.deleted", _("User deleted")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Notification
|
||||
# ---------------------------------------------------------------------------
|
||||
class Notification(models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Notification")
|
||||
verbose_name_plural = _("Notifications")
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True, blank=True,
|
||||
related_name="notifications"
|
||||
)
|
||||
message = models.TextField()
|
||||
kind = models.CharField(max_length=40, choices=NotificationKind.choices, default="")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
read = models.BooleanField(default=False) # Read in WebApp
|
||||
sent = models.BooleanField(default=False) # Sent via Mail (optional)
|
||||
|
||||
# Optional relation to any object
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True)
|
||||
object_id = models.PositiveIntegerField(null=True, blank=True)
|
||||
related_object = GenericForeignKey("content_type", "object_id")
|
||||
target_url = models.CharField(max_length=500, blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
user_display = self.user.username if self.user else "System"
|
||||
return f"{user_display}: {self.message[:50]}..."
|
||||
|
||||
def get_link(self):
|
||||
"""Return URL to the related object if available."""
|
||||
if not self.related_object:
|
||||
return None
|
||||
model_name = self.content_type.model
|
||||
if model_name == "risk":
|
||||
return reverse("risks:show_risk", args=[self.object_id])
|
||||
if model_name == "control":
|
||||
return reverse("risks:show_control", args=[self.object_id])
|
||||
if model_name == "incident":
|
||||
return reverse("risks:show_incident", args=[self.object_id])
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# NotificationPreference
|
||||
# ---------------------------------------------------------------------------
|
||||
class NotificationPreference(models.Model):
|
||||
"""User-specific notification preferences."""
|
||||
|
||||
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 True if user wants notifications for this event code."""
|
||||
return bool(getattr(self, event_code, False))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# NotificationRule
|
||||
# ---------------------------------------------------------------------------
|
||||
class NotificationRule(models.Model):
|
||||
"""Global rules: Which events trigger in-app and/or email notifications."""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Notification rule")
|
||||
verbose_name_plural = _("Notification rules")
|
||||
|
||||
kind = models.CharField(
|
||||
_("Event"),
|
||||
max_length=40,
|
||||
choices=NotificationKind.choices,
|
||||
unique=True,
|
||||
)
|
||||
enabled_in_app = models.BooleanField(_("Show in app"), default=True)
|
||||
enabled_email = models.BooleanField(_("Send via email"), default=False)
|
||||
|
||||
# Recipient groups
|
||||
to_owner = models.BooleanField(
|
||||
_("Send to owner/responsible/reporter (if available)"),
|
||||
default=True,
|
||||
)
|
||||
to_staff = models.BooleanField(_("Send to all staff"), default=False)
|
||||
extra_recipients = models.TextField(
|
||||
_("Extra recipients (emails, comma or newline separated)"),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.get_kind_display() or self.kind
|
13
risks/models/__init__.py
Normal file
13
risks/models/__init__.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from .auditlog import AuditLog
|
||||
from .control import Control
|
||||
from .incident import Incident
|
||||
from .notification import Notification
|
||||
from .notification_kind import NotificationKind
|
||||
from .notification_preference import NotificationPreference
|
||||
from .notification_rule import NotificationRule
|
||||
from .residual_risk import ResidualRisk
|
||||
from .risk import Risk
|
||||
from .user import User
|
||||
|
||||
|
||||
__all__ = ["AuditLog", "Control", "Incident", "Notification", "NotificationKind", "NotificationPreference", "NotificationRule", "ResidualRisk", "Risk", "User"]
|
48
risks/models/auditlog.py
Normal file
48
risks/models/auditlog.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .safe_json_encoder import SafeJSONEncoder
|
||||
import datetime
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SafeJSONEncoder
|
||||
# ---------------------------------------------------------------------------
|
||||
class SafeJSONEncoder(DjangoJSONEncoder):
|
||||
"""JSON encoder that can handle datetime.date properly."""
|
||||
def default(self, obj):
|
||||
if isinstance(obj, datetime.date):
|
||||
return obj.isoformat()
|
||||
return super().default(obj)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AuditLog
|
||||
# ---------------------------------------------------------------------------
|
||||
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"),
|
||||
("delete", "Deleted"),
|
||||
]
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True, blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="audit_logs"
|
||||
)
|
||||
action = models.CharField(max_length=10, choices=ACTION_CHOICES)
|
||||
model = models.CharField(max_length=100)
|
||||
object_id = models.CharField(max_length=50)
|
||||
changes = models.JSONField(null=True, blank=True, encoder=SafeJSONEncoder)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.timestamp}] {self.user} {self.action} {self.model}({self.object_id})"
|
41
risks/models/control.py
Normal file
41
risks/models/control.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Control
|
||||
# ---------------------------------------------------------------------------
|
||||
class Control(models.Model):
|
||||
"""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")),
|
||||
]
|
||||
|
||||
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(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name="responsible_controls"
|
||||
)
|
||||
description = models.TextField(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
|
||||
risks = models.ManyToManyField("Risk", related_name="controls", blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title} ({self.get_status_display()})"
|
35
risks/models/incident.py
Normal file
35
risks/models/incident.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Incident
|
||||
# ---------------------------------------------------------------------------
|
||||
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")),
|
||||
]
|
||||
|
||||
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)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
51
risks/models/notification.py
Normal file
51
risks/models/notification.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
from django.db import models
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .notification_kind import NotificationKind
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Notification
|
||||
# ---------------------------------------------------------------------------
|
||||
class Notification(models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Notification")
|
||||
verbose_name_plural = _("Notifications")
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True, blank=True,
|
||||
related_name="notifications"
|
||||
)
|
||||
message = models.TextField()
|
||||
kind = models.CharField(max_length=40, choices=NotificationKind.choices, default="")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
read = models.BooleanField(default=False) # Read in WebApp
|
||||
sent = models.BooleanField(default=False) # Sent via Mail (optional)
|
||||
|
||||
# Optional relation to any object
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True)
|
||||
object_id = models.PositiveIntegerField(null=True, blank=True)
|
||||
related_object = GenericForeignKey("content_type", "object_id")
|
||||
target_url = models.CharField(max_length=500, blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
user_display = self.user.username if self.user else "System"
|
||||
return f"{user_display}: {self.message[:50]}..."
|
||||
|
||||
def get_link(self):
|
||||
"""Return URL to the related object if available."""
|
||||
if not self.related_object:
|
||||
return None
|
||||
model_name = self.content_type.model
|
||||
if model_name == "risk":
|
||||
return reverse("risks:show_risk", args=[self.object_id])
|
||||
if model_name == "control":
|
||||
return reverse("risks:show_control", args=[self.object_id])
|
||||
if model_name == "incident":
|
||||
return reverse("risks:show_incident", args=[self.object_id])
|
||||
return None
|
32
risks/models/notification_kind.py
Normal file
32
risks/models/notification_kind.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# NotificationKind
|
||||
# ---------------------------------------------------------------------------
|
||||
class NotificationKind(models.TextChoices):
|
||||
"""Event types for notifications."""
|
||||
RISK_CREATED = "risk.created", _("Risk created")
|
||||
RISK_UPDATED = "risk.updated", _("Risk updated")
|
||||
RISK_DELETED = "risk.deleted", _("Risk deleted")
|
||||
RISK_REVIEW_REQUIRED = "risk.review_required", _("Risk review required")
|
||||
RISK_REVIEW_COMPLETED = "risk.review_completed", _("Risk review completed")
|
||||
|
||||
CONTROL_CREATED = "control.created", _("Control created")
|
||||
CONTROL_UPDATED = "control.updated", _("Control updated")
|
||||
CONTROL_DELETED = "control.deleted", _("Control deleted")
|
||||
|
||||
RESIDUAL_CREATED = "residual.created", _("Residual created")
|
||||
RESIDUAL_UPDATED = "residual.updated", _("Residual updated")
|
||||
RESIDUAL_DELETED = "residual.deleted", _("Residual deleted")
|
||||
RESIDUAL_REVIEW_REQUIRED = "residual.review_required", _("Residual review required")
|
||||
RESIDUAL_REVIEW_COMPLETED = "residual.review_completed", _("Residual review completed")
|
||||
|
||||
INCIDENT_CREATED = "incident.created", _("Incident created")
|
||||
INCIDENT_UPDATED = "incident.updated", _("Incident updated")
|
||||
INCIDENT_DELETED = "incident.deleted", _("Incident deleted")
|
||||
|
||||
USER_CREATED = "user.created", _("User created")
|
||||
USER_DELETED = "user.deleted", _("User deleted")
|
||||
|
||||
|
54
risks/models/notification_preference.py
Normal file
54
risks/models/notification_preference.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# NotificationPreference
|
||||
# ---------------------------------------------------------------------------
|
||||
class NotificationPreference(models.Model):
|
||||
"""User-specific notification preferences."""
|
||||
|
||||
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 True if user wants notifications for this event code."""
|
||||
return bool(getattr(self, event_code, False))
|
36
risks/models/notification_rule.py
Normal file
36
risks/models/notification_rule.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .notification_kind import NotificationKind
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# NotificationRule
|
||||
# ---------------------------------------------------------------------------
|
||||
class NotificationRule(models.Model):
|
||||
"""Global rules: Which events trigger in-app and/or email notifications."""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Notification rule")
|
||||
verbose_name_plural = _("Notification rules")
|
||||
|
||||
kind = models.CharField(
|
||||
_("Event"),
|
||||
max_length=40,
|
||||
choices=NotificationKind.choices,
|
||||
unique=True,
|
||||
)
|
||||
enabled_in_app = models.BooleanField(_("Show in app"), default=True)
|
||||
enabled_email = models.BooleanField(_("Send via email"), default=False)
|
||||
|
||||
# Recipient groups
|
||||
to_owner = models.BooleanField(
|
||||
_("Send to owner/responsible/reporter (if available)"),
|
||||
default=True,
|
||||
)
|
||||
to_staff = models.BooleanField(_("Send to all staff"), default=False)
|
||||
extra_recipients = models.TextField(
|
||||
_("Extra recipients (emails, comma or newline separated)"),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.get_kind_display() or self.kind
|
45
risks/models/residual_risk.py
Normal file
45
risks/models/residual_risk.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .risk import Risk
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Residual Risk
|
||||
# ---------------------------------------------------------------------------
|
||||
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, related_name="residual_risk")
|
||||
likelihood = models.IntegerField(choices=Risk.LIKELIHOOD_CHOICES, default=1)
|
||||
impact = models.IntegerField(choices=Risk.IMPACT_CHOICES, default=1)
|
||||
score = models.IntegerField(editable=False)
|
||||
level = models.CharField(max_length=50, editable=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):
|
||||
if self.pk:
|
||||
old = ResidualRisk.objects.get(pk=self.pk)
|
||||
if old.likelihood != self.likelihood or old.impact != self.impact:
|
||||
self.review_required = True
|
||||
self.status = "review_required"
|
||||
|
||||
# Calculate residual risk score and level
|
||||
self.score = self.likelihood * self.impact
|
||||
if self.score <= 4:
|
||||
self.level = "Low"
|
||||
elif self.score <= 8:
|
||||
self.level = "Medium"
|
||||
elif self.score <= 12:
|
||||
self.level = "High"
|
||||
else:
|
||||
self.level = "Critical"
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"Residual Risk for {self.risk.title} (Score: {self.score}, Level: {self.level})"
|
100
risks/models/risk.py
Normal file
100
risks/models/risk.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from multiselectfield import MultiSelectField
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Risk
|
||||
# ---------------------------------------------------------------------------
|
||||
class Risk(models.Model):
|
||||
|
||||
class Meta:
|
||||
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")),
|
||||
(3, _("Likely – once per year or more")),
|
||||
(4, _("Very likely – multiple times per year/monthly")),
|
||||
]
|
||||
IMPACT_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)")),
|
||||
]
|
||||
CIA_CHOICES = [
|
||||
("1", _("Confidentiality")),
|
||||
("2", _("Integrity")),
|
||||
("3", _("Availability")),
|
||||
]
|
||||
|
||||
# Basic information
|
||||
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)
|
||||
effects = models.TextField(_("Effects"), blank=True, null=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)
|
||||
|
||||
# Risk evaluation before controls
|
||||
likelihood = models.IntegerField(choices=LIKELIHOOD_CHOICES, default=1)
|
||||
impact = models.IntegerField(choices=IMPACT_CHOICES, default=1)
|
||||
|
||||
# Calculated fields
|
||||
score = models.IntegerField(editable=False)
|
||||
level = models.CharField(max_length=50, editable=False)
|
||||
|
||||
# Ownership
|
||||
owner = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name="owned_risks"
|
||||
)
|
||||
|
||||
# Reminder / follow-up date
|
||||
follow_up = models.DateField(blank=True, null=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Mark for review if likelihood/impact changed
|
||||
if self.pk:
|
||||
old = Risk.objects.get(pk=self.pk)
|
||||
if old.likelihood != self.likelihood or old.impact != self.impact:
|
||||
self.review_required = True
|
||||
self.status = "review_required"
|
||||
|
||||
# Calculate risk score and level
|
||||
self.score = self.likelihood * self.impact
|
||||
if self.score <= 4:
|
||||
self.level = "Low"
|
||||
elif self.score <= 8:
|
||||
self.level = "Medium"
|
||||
elif self.score <= 12:
|
||||
self.level = "High"
|
||||
else:
|
||||
self.level = "Critical"
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title} (Score: {self.score}, Level: {self.level})"
|
12
risks/models/safe_json_encoder.py
Normal file
12
risks/models/safe_json_encoder.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
import datetime
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SafeJSONEncoder
|
||||
# ---------------------------------------------------------------------------
|
||||
class SafeJSONEncoder(DjangoJSONEncoder):
|
||||
"""JSON encoder that can handle datetime.date properly."""
|
||||
def default(self, obj):
|
||||
if isinstance(obj, datetime.date):
|
||||
return obj.isoformat()
|
||||
return super().default(obj)
|
19
risks/models/user.py
Normal file
19
risks/models/user.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# User
|
||||
# ---------------------------------------------------------------------------
|
||||
class User(AbstractUser):
|
||||
"""Custom user model to support both local and SSO users."""
|
||||
is_sso_user = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def risks_owned(self):
|
||||
"""All risks where the user is the risk owner."""
|
||||
return self.owned_risks.all()
|
||||
|
||||
@property
|
||||
def controls_responsible(self):
|
||||
"""All controls where the user is responsible."""
|
||||
return self.responsible_controls.all()
|
Loading…
Add table
Reference in a new issue