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.models
|
||||||
import django.contrib.auth.validators
|
import django.contrib.auth.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
|
import multiselectfield.db.fields
|
||||||
|
import risks.models.auditlog
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
@ -14,9 +16,26 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('auth', '0012_alter_user_first_name_max_length'),
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
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(
|
migrations.CreateModel(
|
||||||
name='User',
|
name='User',
|
||||||
fields=[
|
fields=[
|
||||||
|
@ -44,36 +63,145 @@ class Migration(migrations.Migration):
|
||||||
('objects', django.contrib.auth.models.UserManager()),
|
('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(
|
migrations.CreateModel(
|
||||||
name='Risk',
|
name='Risk',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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')),
|
||||||
('asset', models.CharField(blank=True, max_length=255, null=True)),
|
('description', models.TextField(blank=True, max_length=225, null=True, verbose_name='Description')),
|
||||||
('process', models.CharField(blank=True, max_length=255, null=True)),
|
('asset', models.CharField(blank=True, max_length=255, null=True, verbose_name='Asset')),
|
||||||
('category', models.CharField(blank=True, max_length=255, null=True)),
|
('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)),
|
('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)),
|
('score', models.IntegerField(editable=False)),
|
||||||
('level', models.CharField(editable=False, max_length=50)),
|
('level', models.CharField(editable=False, max_length=50)),
|
||||||
('follow_up', models.DateField(blank=True, null=True)),
|
('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)),
|
('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(
|
migrations.CreateModel(
|
||||||
name='Control',
|
name='Control',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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)),
|
('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)),
|
('due_date', models.DateField(blank=True, null=True)),
|
||||||
('description', models.TextField(blank=True, null=True)),
|
('description', models.TextField(blank=True, null=True)),
|
||||||
('wiki_link', models.URLField(blank=True, null=True)),
|
('wiki_link', models.URLField(blank=True, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
('responsible', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='responsible_controls', to=settings.AUTH_USER_MODEL)),
|
('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