Coverage for product_risk_suite/risk_assessment/models.py: 99%
254 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 12:45 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 12:45 +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
9import logging
11logger = logging.getLogger(f"risk_assessment.{__name__}")
13def truncate(s: str, max_len: int = 50) -> str:
14 if s is None:
15 return ""
16 return s if len(s) <= max_len else s[:max_len - 3] + "..."
18class Asset(models.Model):
19 id = models.AutoField(primary_key=True)
20 name = models.CharField(max_length=200, unique=True)
22 def __str__(self):
23 return self.name
25class LifeCycle(models.Model):
26 id = models.AutoField(primary_key=True)
27 name = models.CharField(max_length=200, unique=True)
29 def __str__(self):
30 return self.name
32class Origin(models.Model):
33 id = models.AutoField(primary_key=True)
34 name = models.CharField(max_length=200, unique=True)
36 def __str__(self):
37 return self.name
39class SecurityRequirement(models.Model):
40 id = models.AutoField(primary_key=True)
41 norm_short = models.CharField(max_length=255)
42 norm_long = models.CharField(max_length=255)
43 description_short = models.CharField(max_length=255)
44 description_long = models.TextField()
45 slug = models.SlugField(null=False, unique=True)
47 class Meta:
48 unique_together = ('norm_short', 'description_short',)
49 ordering = ['norm_short', 'description_short']
51 def __str__(self):
52 return f"{self.norm_short}: {self.description_short}"
54class Stride(models.Model):
55 id = models.AutoField(primary_key=True)
56 STRIDE_CHOICES = {
57 "S": _("Spoofing"),
58 "T": _("Tampering"),
59 "R": _("Repudiation"),
60 "I": _("Information Disclosure"),
61 "D": _("Denial of Service"),
62 "E": _("Elevation of Privileges"),
63 }
65 name = models.CharField(max_length=1, choices=STRIDE_CHOICES, unique=True)
67 def __str__(self):
68 return self.name
70 def save(self, *args, **kwargs):
71 self.full_clean() # Calls clean() and validates all fields
72 super().save(*args, **kwargs)
74 def clean(self):
75 if self.name not in self.STRIDE_CHOICES:
76 raise ValidationError(
77 {"name": f"'{self.name}' is an invalid STRIDE value."}
78 )
79 super().clean()
81class SuggestedMitigationValidation(models.Model):
82 id = models.AutoField(primary_key=True)
83 suggested_mitigation = models.TextField(null=True, blank=True)
84 suggested_validation = models.TextField(null=True, blank=True)
86 def __str__(self):
87 return f"{truncate(self.suggested_mitigation, 32)} - {truncate(self.suggested_validation, 32)}"
90class Risk(models.Model):
91 id = models.AutoField(primary_key=True)
92 custom_id = models.CharField(max_length=255, default="RISK-")
93 version = models.PositiveIntegerField(default=1)
94 is_current = models.BooleanField(default=True)
95 asset = models.ForeignKey(Asset, on_delete=models.PROTECT)
96 origin = models.ForeignKey(Origin, on_delete=models.PROTECT)
97 life_cycle = models.ForeignKey(LifeCycle, on_delete=models.PROTECT)
98 stride = models.ManyToManyField(Stride)
99 title = models.CharField(max_length=255)
100 description = models.TextField()
102 suggested_mitigation_validation = models.ForeignKey(SuggestedMitigationValidation, null=True, blank=True, on_delete=models.SET_NULL)
104 class Meta:
105 unique_together = ('custom_id', 'version')
107 def __str__(self):
108 return f"{self.custom_id} (v{self.version}): {self.title}"
110 @property
111 def has_newer_version(self):
112 return not self.is_current
114 @property
115 def stride_str(self):
116 strides = [stride.name for stride in self.stride.all()]
117 return ", ".join(strides)
119 @property
120 def list_stride(self):
121 return [str(Stride.STRIDE_CHOICES[stride.name]) for stride in self.stride.all()]
123class Risk5x5(Enum):
124 LOW = 0
125 MID = 1
126 HIGH = 2
128class SeverityName(Enum):
129 Negligible = 1
130 Minor = 2
131 Moderate = 3
132 Major = 4
133 Critical = 5
135 @staticmethod
136 def to_human_str(value):
137 try:
138 sn = SeverityName(value)
139 match sn:
140 case SeverityName.Negligible: return "Negligible"
141 case SeverityName.Minor: return "Minor"
142 case SeverityName.Moderate: return "Moderate"
143 case SeverityName.Major: return "Major"
144 case SeverityName.Critical: return "Critical"
145 except ValueError as ve:
146 logger.error(ve)
147 return str(value)
149class SeverityExample(models.Model):
150 id = models.AutoField(primary_key=True)
151 severity_of_impact = models.IntegerField(default=1, validators=[MaxValueValidator(5), MinValueValidator(1)], unique=True)
152 examples = models.TextField(null=True, blank=True)
154 def __str__(self):
155 return f"{SeverityName.to_human_str(self.severity_of_impact)} - Examples"
157class LikelihoodName(Enum):
158 VeryLow = 1
159 Low = 2
160 Medium = 3
161 High = 4
162 VeryHigh = 5
164 @staticmethod
165 def to_human_str(value):
166 li = LikelihoodName(value)
167 match li:
168 case LikelihoodName.VeryLow: return "Very low"
169 case LikelihoodName.Low: return "Low"
170 case LikelihoodName.Medium: return "Medium"
171 case LikelihoodName.High: return "High"
172 case LikelihoodName.VeryHigh: return "Very high"
173 return str(value)
175class LikelihoodExample(models.Model):
176 id = models.AutoField(primary_key=True)
177 likelihood_of_occurrence = models.IntegerField(default=1, validators=[MaxValueValidator(5), MinValueValidator(1)], unique=True)
178 examples = models.TextField(null=True, blank=True)
180 def __str__(self):
181 return f"{LikelihoodName.to_human_str(self.likelihood_of_occurrence)} - Examples"
183def _RiskRating__loo_help_list():
184 str = "<ul>"
185 for i in range(1, 6):
186 str = str + f"<li>{i}: {LikelihoodName.to_human_str(i)}</li>"
187 str = str + "</ul>"
188 return str
190def _RiskRating__sev_help_list():
191 str = "<ul>"
192 for i in range(1, 6):
193 str = str + f"<li>{i}: {SeverityName.to_human_str(i)}</li>"
194 str = str + "</ul>"
195 return str
197class RiskRating(models.Model):
198 id = models.AutoField(primary_key=True)
199 likelihood_of_occurrence = models.IntegerField(default=1, validators=[MaxValueValidator(5), MinValueValidator(1)], help_text=__loo_help_list())
200 severity_of_impact = models.IntegerField(default=1, validators=[MaxValueValidator(5), MinValueValidator(1)], help_text=__sev_help_list())
202 @staticmethod
203 def risk_level(code) -> Risk5x5:
204 if code >= 15:
205 return Risk5x5.HIGH
206 if code >= 7:
207 return Risk5x5.MID
208 return Risk5x5.LOW
210 @staticmethod
211 def risk_level_to_color(risk_level: Risk5x5):
212 match risk_level:
213 case Risk5x5.HIGH:
214 return "danger" # red
215 case Risk5x5.MID:
216 return "warning" # yellow
217 case Risk5x5.LOW:
218 return "success" # green
219 return "danger" # fallback red
221 @staticmethod
222 def color_class(code):
223 if code == 1 or code == 2:
224 return RiskRating.risk_level_to_color(Risk5x5.LOW)
225 if code == 3 or code == 4:
226 return RiskRating.risk_level_to_color(Risk5x5.MID)
227 return RiskRating.risk_level_to_color(Risk5x5.HIGH)
230 def risk_color_class(self, code):
231 return self.risk_level_to_color(self.risk_level(code))
233 @staticmethod
234 def calc_risk(likelihood_of_occurrence, severity_of_impact):
235 return likelihood_of_occurrence * severity_of_impact
237 @property
238 def risk(self):
239 return RiskRating.calc_risk(self.likelihood_of_occurrence, self.severity_of_impact)
241 def __str__(self):
242 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}"
244 @property
245 def color_likelihood_of_occurrence(self):
246 return self.color_class(self.likelihood_of_occurrence)
247 @property
248 def color_severity_of_impact(self):
249 return self.color_class(self.severity_of_impact)
250 @property
251 def color_risk(self):
252 return self.risk_color_class(self.risk)
254class Status(models.Model):
255 id = models.AutoField(primary_key=True)
256 STATUS_CHOICES = {
257 "Open": True,
258 "On going": True ,
259 "Finished": False ,
260 "Rejected": False ,
261 }
263 status = models.CharField(max_length=255, choices=STATUS_CHOICES, unique=True)
265 def __str__(self):
266 return self.status
268 @property
269 def is_todo(self):
270 return self.STATUS_CHOICES[self.status]
272 @staticmethod
273 def web_idx_name(choice):
274 match choice:
275 case "Open": return "open"
276 case "On going": return "on_going"
277 case "Finished": return "finished"
278 case "Rejected": return "rejected"
279 return "unknown"
281 def save(self, *args, **kwargs):
282 self.full_clean() # Calls clean() and validates all fields
283 super().save(*args, **kwargs)
285 def clean(self):
286 if self.status not in self.STATUS_CHOICES:
287 raise ValidationError(
288 {"name": f"'{self.status}' is an invalid status value."}
289 )
290 super().clean()
292class Evidence(models.Model):
293 id = models.AutoField(primary_key=True)
294 due_date = models.DateField()
295 responsible = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
296 status = models.ForeignKey(Status, on_delete=models.PROTECT)
297 evidence = models.TextField(null=True, blank=True)
298 evidence_link = models.TextField(null=True, blank=True)
300 product = models.ForeignKey('product.Product', blank=True, null=True, on_delete=models.SET_NULL)
302 def __str__(self):
303 if self.status.is_todo:
304 if self.evidence:
305 return f"{self.status} responsible: {self.responsible} due: {self.due_date} evidence: {self.evidence}"
306 return f"{self.status} responsible: {self.responsible} due: {self.due_date}"
307 else:
308 return f"{self.status} evidence: {self.evidence}"
310 @property
311 def color_due_date(self):
312 if not self.status.is_todo:
313 return "success" #green
314 in_4_weeks = datetime.today() + timedelta(weeks=4)
315 if self.due_date > in_4_weeks.date():
316 return "warning" # yellow
317 return "danger" # red
319class RiskMitigation(models.Model):
320 id = models.AutoField(primary_key=True)
321 security_requirements = models.ManyToManyField(SecurityRequirement, blank=True)
322 title = models.CharField(max_length=255, null=True, blank=True)
323 mitigation = models.TextField(null=True, blank=True)
324 rational = models.TextField(null=True, blank=True)
326 @property
327 def list_security_requirements(self):
328 return [{ "str": str(req), "slug": req.slug } for req in self.security_requirements.all()]
330 def __str__(self):
331 if self.title:
332 return self.title
333 return f"{self.mitigation}"