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

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 

6 

7from datetime import datetime, timedelta 

8from enum import Enum 

9 

10class Asset(models.Model): 

11 id = models.AutoField(primary_key=True) 

12 name = models.CharField(max_length=200, unique=True) 

13 

14 def __str__(self): 

15 return self.name 

16 

17class LiveCycle(models.Model): 

18 id = models.AutoField(primary_key=True) 

19 name = models.CharField(max_length=200, unique=True) 

20 

21 def __str__(self): 

22 return self.name 

23 

24class Origin(models.Model): 

25 id = models.AutoField(primary_key=True) 

26 name = models.CharField(max_length=200, unique=True) 

27 

28 def __str__(self): 

29 return self.name 

30 

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) 

38 

39 class Meta: 

40 unique_together = ('norm_short', 'description_short',) 

41 

42 def __str__(self): 

43 return f"{self.norm_short}: {self.description_short}" 

44 

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 } 

55 

56 name = models.CharField(max_length=1, choices=STRIDE_CHOICES, unique=True) 

57 

58 def __str__(self): 

59 return self.name 

60 

61 def save(self, *args, **kwargs): 

62 self.full_clean() # Calls clean() and validates all fields 

63 super().save(*args, **kwargs) 

64 

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() 

71 

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) 

76 

77 def __str__(self): 

78 return f"{self.suggested_mitigation[:32]} - {self.suggested_validation[:32]}" 

79 

80 

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() 

90 

91 suggested_mitigation_validation = models.ForeignKey(SuggestedMitigationValidation, null=True, blank=True, on_delete=models.SET_NULL) 

92 

93 def __str__(self): 

94 return f"{self.custom_id}: {self.title}" 

95 

96 @property 

97 def stride_str(self): 

98 strides = [stride.name for stride in self.stride.all()] 

99 return ", ".join(strides) 

100 

101 @property 

102 def list_stride(self): 

103 return [str(Stride.STRIDE_CHOICES[stride.name]) for stride in self.stride.all()] 

104 

105class Risk5x5(Enum): 

106 LOW = 0 

107 MID = 1 

108 HIGH = 2 

109 

110class SeverityName(Enum): 

111 Negligible = 1 

112 Minor = 2 

113 Moderate = 3 

114 Major = 4 

115 Critical = 5 

116 

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) 

127 

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) 

132 

133 def __str__(self): 

134 return f"{SeverityName.to_human_str(self.severity_of_impact)} - Examples" 

135 

136class LikelihoodName(Enum): 

137 VeryLow = 1 

138 Low = 2 

139 Medium = 3 

140 High = 4 

141 VeryHigh = 5 

142 

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) 

153 

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) 

158 

159 def __str__(self): 

160 return f"{LikelihoodName.to_human_str(self.likelihood_of_occurrence)} - Examples" 

161 

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 

168 

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 

175 

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()) 

180 

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 

188 

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 

199 

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) 

207 

208 

209 def risk_color_class(self, code): 

210 return self.risk_level_to_color(self.risk_level(code)) 

211 

212 @staticmethod 

213 def calc_risk(likelihood_of_occurrence, severity_of_impact): 

214 return likelihood_of_occurrence * severity_of_impact 

215 

216 @property 

217 def risk(self): 

218 return RiskRating.calc_risk(self.likelihood_of_occurrence, self.severity_of_impact) 

219 

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}" 

222 

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) 

232 

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 } 

241 

242 status = models.CharField(max_length=255, choices=STATUS_CHOICES, unique=True) 

243 

244 def __str__(self): 

245 return self.status 

246 

247 @property 

248 def is_todo(self): 

249 return self.STATUS_CHOICES[self.status] 

250 

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" 

259 

260 def save(self, *args, **kwargs): 

261 self.full_clean() # Calls clean() and validates all fields 

262 super().save(*args, **kwargs) 

263 

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() 

270 

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) 

278 

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}" 

286 

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 

295 

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) 

302 

303 @property 

304 def list_security_requirements(self): 

305 return [{ "str": str(req), "slug": req.slug } for req in self.security_requirements.all()] 

306 

307 def __str__(self): 

308 if self.title: 

309 return self.title 

310 return f"{self.mitigation}"