Coverage for product_risk_suite / risk_assessment / models.py: 97%
236 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-13 23:42 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-13 23:42 +0000
1from django.db import models
2from django.utils.translation import gettext_lazy as _
3from django.core.validators import MaxValueValidator, MinValueValidator
4from django.contrib.auth.models import User
5from django.core.exceptions import ValidationError
7from datetime import datetime, timedelta
8from enum import Enum
10class Asset(models.Model):
11 id = models.AutoField(primary_key=True)
12 name = models.CharField(max_length=200, unique=True)
14 def __str__(self):
15 return self.name
17class LiveCycle(models.Model):
18 id = models.AutoField(primary_key=True)
19 name = models.CharField(max_length=200, unique=True)
21 def __str__(self):
22 return self.name
24class Origin(models.Model):
25 id = models.AutoField(primary_key=True)
26 name = models.CharField(max_length=200, unique=True)
28 def __str__(self):
29 return self.name
31class SecurityRequirement(models.Model):
32 id = models.AutoField(primary_key=True)
33 norm_short = models.CharField(max_length=255)
34 norm_long = models.CharField(max_length=255)
35 description_short = models.CharField(max_length=255)
36 description_long = models.TextField()
37 slug = models.SlugField(null=False, unique=True)
39 class Meta:
40 unique_together = ('norm_short', 'description_short',)
42 def __str__(self):
43 return f"{self.norm_short}: {self.description_short}"
45class Stride(models.Model):
46 id = models.AutoField(primary_key=True)
47 STRIDE_CHOICES = {
48 "S": _("Spoofing"),
49 "T": _("Tampering"),
50 "R": _("Repudiation"),
51 "I": _("Information Disclosure"),
52 "D": _("Denial of Service"),
53 "E": _("Elevation of Privileges"),
54 }
56 name = models.CharField(max_length=1, choices=STRIDE_CHOICES, unique=True)
58 def __str__(self):
59 return self.name
61 def save(self, *args, **kwargs):
62 self.full_clean() # Calls clean() and validates all fields
63 super().save(*args, **kwargs)
65 def clean(self):
66 if self.name not in self.STRIDE_CHOICES:
67 raise ValidationError(
68 {"name": f"'{self.name}' is an invalid STRIDE value."}
69 )
70 super().clean()
72class SuggestedMitigationValidation(models.Model):
73 id = models.AutoField(primary_key=True)
74 suggested_mitigation = models.TextField(null=True, blank=True)
75 suggested_validation = models.TextField(null=True, blank=True)
77 def __str__(self):
78 return f"{self.suggested_mitigation[:32]} - {self.suggested_validation[:32]}"
81class Risk(models.Model):
82 id = models.AutoField(primary_key=True)
83 custom_id = models.CharField(max_length=255, unique=True, default="RISK-")
84 asset = models.ForeignKey(Asset, on_delete=models.PROTECT)
85 origin = models.ForeignKey(Origin, on_delete=models.PROTECT)
86 live_cycle = models.ForeignKey(LiveCycle, on_delete=models.PROTECT)
87 stride = models.ManyToManyField(Stride)
88 title = models.CharField(max_length=255)
89 description = models.TextField()
91 suggested_mitigation_validation = models.ForeignKey(SuggestedMitigationValidation, null=True, blank=True, on_delete=models.SET_NULL)
93 def __str__(self):
94 return f"{self.custom_id}: {self.title}"
96 @property
97 def stride_str(self):
98 strides = [stride.name for stride in self.stride.all()]
99 return ", ".join(strides)
101 @property
102 def list_stride(self):
103 return [str(Stride.STRIDE_CHOICES[stride.name]) for stride in self.stride.all()]
105class Risk5x5(Enum):
106 LOW = 0
107 MID = 1
108 HIGH = 2
110class SeverityName(Enum):
111 Negligible = 1
112 Minor = 2
113 Moderate = 3
114 Major = 4
115 Critical = 5
117 @staticmethod
118 def to_human_str(value):
119 sn = SeverityName(value)
120 match sn:
121 case SeverityName.Negligible: return "Negligible"
122 case SeverityName.Minor: return "Minor"
123 case SeverityName.Moderate: return "Moderate"
124 case SeverityName.Major: return "Major"
125 case SeverityName.Critical: return "Critical"
126 return str(value)
128class SeverityExample(models.Model):
129 id = models.AutoField(primary_key=True)
130 severity_of_impact = models.IntegerField(default=1, validators=[MaxValueValidator(5), MinValueValidator(1)], unique=True)
131 examples = models.TextField(null=True, blank=True)
133 def __str__(self):
134 return f"{SeverityName.to_human_str(self.severity_of_impact)} - Examples"
136class LikelihoodName(Enum):
137 VeryLow = 1
138 Low = 2
139 Medium = 3
140 High = 4
141 VeryHigh = 5
143 @staticmethod
144 def to_human_str(value):
145 li = LikelihoodName(value)
146 match li:
147 case LikelihoodName.VeryLow: return "Very low"
148 case LikelihoodName.Low: return "Low"
149 case LikelihoodName.Medium: return "Medium"
150 case LikelihoodName.High: return "High"
151 case LikelihoodName.VeryHigh: return "Very high"
152 return str(value)
154class LikelihoodExample(models.Model):
155 id = models.AutoField(primary_key=True)
156 likelihood_of_occurrence = models.IntegerField(default=1, validators=[MaxValueValidator(5), MinValueValidator(1)], unique=True)
157 examples = models.TextField(null=True, blank=True)
159 def __str__(self):
160 return f"{LikelihoodName.to_human_str(self.likelihood_of_occurrence)} - Examples"
162def _RiskRating__loo_help_list():
163 str = "<ul>"
164 for i in range(1, 6):
165 str = str + f"<li>{i}: {LikelihoodName.to_human_str(i)}</li>"
166 str = str + "</ul>"
167 return str
169def _RiskRating__sev_help_list():
170 str = "<ul>"
171 for i in range(1, 6):
172 str = str + f"<li>{i}: {SeverityName.to_human_str(i)}</li>"
173 str = str + "</ul>"
174 return str
176class RiskRating(models.Model):
177 id = models.AutoField(primary_key=True)
178 likelihood_of_occurrence = models.IntegerField(default=1, validators=[MaxValueValidator(5), MinValueValidator(1)], help_text=__loo_help_list())
179 severity_of_impact = models.IntegerField(default=1, validators=[MaxValueValidator(5), MinValueValidator(1)], help_text=__sev_help_list())
181 @staticmethod
182 def risk_level(code) -> Risk5x5:
183 if code >= 15:
184 return Risk5x5.HIGH
185 if code >= 7:
186 return Risk5x5.MID
187 return Risk5x5.LOW
189 @staticmethod
190 def risk_level_to_color(risk_level: Risk5x5):
191 match risk_level:
192 case Risk5x5.HIGH:
193 return "danger" # red
194 case Risk5x5.MID:
195 return "warning" # yellow
196 case Risk5x5.LOW:
197 return "success" # green
198 return "danger" # fallback red
200 @staticmethod
201 def color_class(code):
202 if code == 1 or code == 2:
203 return RiskRating.risk_level_to_color(Risk5x5.LOW)
204 if code == 3 or code == 4:
205 return RiskRating.risk_level_to_color(Risk5x5.MID)
206 return RiskRating.risk_level_to_color(Risk5x5.HIGH)
209 def risk_color_class(self, code):
210 return self.risk_level_to_color(self.risk_level(code))
212 @staticmethod
213 def calc_risk(likelihood_of_occurrence, severity_of_impact):
214 return likelihood_of_occurrence * severity_of_impact
216 @property
217 def risk(self):
218 return RiskRating.calc_risk(self.likelihood_of_occurrence, self.severity_of_impact)
220 def __str__(self):
221 return f"{LikelihoodName.to_human_str(self.likelihood_of_occurrence)} ({self.likelihood_of_occurrence}) * {SeverityName.to_human_str(self.severity_of_impact)} ({self.severity_of_impact}) = {self.risk}"
223 @property
224 def color_likelihood_of_occurrence(self):
225 return self.color_class(self.likelihood_of_occurrence)
226 @property
227 def color_severity_of_impact(self):
228 return self.color_class(self.severity_of_impact)
229 @property
230 def color_risk(self):
231 return self.risk_color_class(self.risk)
233class Status(models.Model):
234 id = models.AutoField(primary_key=True)
235 STATUS_CHOICES = {
236 "Open": True,
237 "On going": True ,
238 "Finished": False ,
239 "Rejected": False ,
240 }
242 status = models.CharField(max_length=255, choices=STATUS_CHOICES, unique=True)
244 def __str__(self):
245 return self.status
247 @property
248 def is_todo(self):
249 return self.STATUS_CHOICES[self.status]
251 @staticmethod
252 def web_idx_name(choice):
253 match choice:
254 case "Open": return "open"
255 case "On going": return "on_going"
256 case "Finished": return "finished"
257 case "Rejected": return "rejected"
258 return "unknown"
260 def save(self, *args, **kwargs):
261 self.full_clean() # Calls clean() and validates all fields
262 super().save(*args, **kwargs)
264 def clean(self):
265 if self.status not in self.STATUS_CHOICES:
266 raise ValidationError(
267 {"name": f"'{self.status}' is an invalid status value."}
268 )
269 super().clean()
271class Evidence(models.Model):
272 id = models.AutoField(primary_key=True)
273 due_date = models.DateField()
274 responsible = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
275 status = models.ForeignKey(Status, on_delete=models.PROTECT)
276 evidence = models.TextField(null=True, blank=True)
277 evidence_link = models.TextField(null=True, blank=True)
279 def __str__(self):
280 if self.status.is_todo:
281 if self.evidence:
282 return f"{self.status} responsible: {self.responsible} due: {self.due_date} evidence: {self.evidence}"
283 return f"{self.status} responsible: {self.responsible} due: {self.due_date}"
284 else:
285 return f"{self.status} evidence: {self.evidence}"
287 @property
288 def color_due_date(self):
289 if not self.status.is_todo:
290 return "success" #green
291 in_4_weeks = datetime.today() + timedelta(weeks=4)
292 if self.due_date > in_4_weeks.date():
293 return "warning" # yellow
294 return "danger" # red
296class RiskMitigation(models.Model):
297 id = models.AutoField(primary_key=True)
298 security_requirements = models.ManyToManyField(SecurityRequirement)
299 title = models.CharField(max_length=255, null=True, blank=True)
300 mitigation = models.TextField(null=True, blank=True)
301 rational = models.TextField(null=True, blank=True)
303 @property
304 def list_security_requirements(self):
305 return [{ "str": str(req), "slug": req.slug } for req in self.security_requirements.all()]
307 def __str__(self):
308 if self.title:
309 return self.title
310 return f"{self.mitigation}"