Coverage for product_risk_suite/risk_assessment/admin.py: 95%

151 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-16 14:44 +0000

1from django.contrib import admin, messages 

2from django import forms 

3from django.core.exceptions import PermissionDenied 

4from django.db import models 

5from django_summernote.widgets import SummernoteWidget 

6from django.http import HttpResponseRedirect 

7from django.template.response import TemplateResponse 

8from django.templatetags.static import static 

9from django.urls import path, reverse 

10from django.utils.html import format_html 

11 

12from .models import * 

13 

14# fixtures 

15class AssetAdmin(admin.ModelAdmin): 

16 list_display = ["name"] 

17 

18class OriginAdmin(admin.ModelAdmin): 

19 list_display = ["name"] 

20 

21class LifeCycleAdmin(admin.ModelAdmin): 

22 list_display = ["name"] 

23 

24class SecurityRequirementAdmin(admin.ModelAdmin): 

25 list_display = ["norm", "short", "description"] 

26 prepopulated_fields = {"slug": ("norm_short", "description_short")} 

27 

28 formfield_overrides = { 

29 models.TextField: { 'widget': SummernoteWidget()} 

30 } 

31 

32 @admin.display(description='Norm') 

33 def norm(self, obj): 

34 return obj.norm_short 

35 

36 @admin.display(description='Short') 

37 def short(self, obj): 

38 return obj.description_short 

39 

40 @admin.display(description='Description') 

41 def description(self, obj): 

42 return obj.description_long 

43 

44admin.site.register(Asset, AssetAdmin) 

45admin.site.register(Origin, OriginAdmin) 

46admin.site.register(LifeCycle, LifeCycleAdmin) 

47admin.site.register(SecurityRequirement, SecurityRequirementAdmin) 

48 

49 

50# risks 

51 

52class RiskAdminForm(forms.ModelForm): 

53 create_new_version = forms.BooleanField( 

54 required=False, 

55 label="Save as new version", 

56 help_text=( 

57 "Leave unchecked for minor in-place fixes (typos, wording). " 

58 "Check to preserve the current version and save your changes as a new one — " 

59 "product entries pointing to this risk will show an outdated indicator until upgraded." 

60 ), 

61 ) 

62 

63 class Meta: 

64 model = Risk 

65 fields = '__all__' 

66 

67 

68_FIELDSETS_CREATE = [ 

69 (None, { 

70 'fields': [ 

71 'custom_id', 'asset', 'origin', 'life_cycle', 'stride', 

72 'title', 'description', 'suggested_mitigation_validation', 

73 ], 

74 }), 

75] 

76 

77_FIELDSETS_EDIT = [ 

78 (None, { 

79 'fields': [ 

80 ('custom_id', 'version', 'is_current'), 

81 'asset', 'origin', 'life_cycle', 'stride', 

82 'title', 'description', 'suggested_mitigation_validation', 

83 ], 

84 }), 

85 ('Version control', { 

86 'fields': ['create_new_version'], 

87 }), 

88] 

89 

90 

91class RiskAdmin(admin.ModelAdmin): 

92 form = RiskAdminForm 

93 formfield_overrides = { 

94 models.TextField: {'widget': SummernoteWidget()} 

95 } 

96 readonly_fields = ['version', 'is_current'] 

97 

98 list_display = ["custom_id", "version", "is_current", "asset", "life_cycle", "origin", "stride_str", "title"] 

99 list_filter = ["is_current"] 

100 ordering = ["custom_id", "-version"] 

101 

102 def get_fieldsets(self, request, obj=None): 

103 if obj is None: 

104 return _FIELDSETS_CREATE 

105 return _FIELDSETS_EDIT 

106 

107 @admin.display(description='Threat (Stride)') 

108 def stride_str(self, obj): 

109 return obj.stride_str 

110 

111 def save_model(self, request, obj, form, change): 

112 if change and form.cleaned_data.get('create_new_version'): 

113 original = Risk.objects.get(pk=obj.pk) 

114 new_risk = Risk( 

115 custom_id=original.custom_id, 

116 version=original.version + 1, 

117 is_current=True, 

118 asset=obj.asset, 

119 origin=obj.origin, 

120 life_cycle=obj.life_cycle, 

121 title=obj.title, 

122 description=obj.description, 

123 suggested_mitigation_validation=obj.suggested_mitigation_validation, 

124 ) 

125 new_risk.save() 

126 original.is_current = False 

127 original.save() 

128 request._new_version_pk = new_risk.pk 

129 else: 

130 super().save_model(request, obj, form, change) 

131 

132 def save_related(self, request, form, formsets, change): 

133 if hasattr(request, '_new_version_pk'): 

134 new_risk = Risk.objects.get(pk=request._new_version_pk) 

135 new_risk.stride.set(form.cleaned_data.get('stride', [])) 

136 else: 

137 super().save_related(request, form, formsets, change) 

138 

139 def response_change(self, request, obj): 

140 if hasattr(request, '_new_version_pk'): 

141 pk = request._new_version_pk 

142 new_risk = Risk.objects.get(pk=pk) 

143 del request._new_version_pk 

144 self.message_user( 

145 request, 

146 f"New version {new_risk.custom_id} v{new_risk.version} created.", 

147 messages.SUCCESS, 

148 ) 

149 return HttpResponseRedirect( 

150 reverse('admin:risk_assessment_risk_change', args=[pk]) 

151 ) 

152 return super().response_change(request, obj) 

153 

154 

155class RiskMitigationAdmin(admin.ModelAdmin): 

156 filter_horizontal = ['security_requirements'] 

157 formfield_overrides = { 

158 models.TextField: { 'widget': SummernoteWidget()} 

159 } 

160 

161 list_display = ["title", "mitigation", "rational"] 

162 

163class RiskRatingAdmin(admin.ModelAdmin): 

164 list_display = ["risk_str"] 

165 ordering = ['likelihood_of_occurrence', 'severity_of_impact'] 

166 

167 def risk_str(self, obj): 

168 return str(obj) 

169 

170def update_evidences(modeladmin, request, queryset): 

171 evidence_ids = queryset.values_list('id', flat=True) 

172 return HttpResponseRedirect(reverse('evidence_product_update') + '?ids=' + ','.join(str(p) for p in evidence_ids)) 

173 

174update_evidences.short_description = "Update Evidences" 

175 

176 

177def run_scraper_now(modeladmin, request, queryset): 

178 from scraper.services import run_scraper_for_evidence 

179 

180 succeeded = failed = skipped = 0 

181 for evidence in queryset: 

182 result = run_scraper_for_evidence(evidence) 

183 if result is True: 

184 succeeded += 1 

185 elif result is False: 

186 failed += 1 

187 else: 

188 skipped += 1 

189 

190 modeladmin.message_user( 

191 request, 

192 f"Scraper sync: {succeeded} succeeded, {failed} failed, {skipped} skipped (no scraper assigned).", 

193 messages.SUCCESS if failed == 0 else messages.WARNING, 

194 ) 

195 

196run_scraper_now.short_description = "Run scraper now for selected evidences" 

197 

198 

199class EvidenceAdmin(admin.ModelAdmin): 

200 list_display = ["product", "status", "responsible", "evidence", "evidence_link", "scrape_status", "last_successful_scrape_at"] 

201 ordering = ['status','evidence'] 

202 actions = [update_evidences, run_scraper_now] 

203 # only the scraper (scraper.services.run_scraper_for_evidence) is allowed to set these 

204 readonly_fields = ["last_successful_scrape_at", "last_scrape_failed"] 

205 

206 @admin.display(description="Last scrape") 

207 def scrape_status(self, obj): 

208 if obj.scraper_id is None: 

209 return "" 

210 icon = "icon-no.svg" if obj.last_scrape_failed else "icon-yes.svg" 

211 return format_html('<img src="{}" alt="{}">', static(f"admin/img/{icon}"), not obj.last_scrape_failed) 

212 

213 def get_urls(self): 

214 custom = [ 

215 path( 

216 "test-scraper/", 

217 self.admin_site.admin_view(self.test_scraper_view), 

218 name="risk_assessment_evidence_test_scraper", 

219 ), 

220 ] 

221 return custom + super().get_urls() 

222 

223 def test_scraper_view(self, request): 

224 if request.method != "POST" or not self.has_change_permission(request): 

225 raise PermissionDenied 

226 

227 from scraper.models import Scraper 

228 from scraper.services import test_scraper 

229 

230 context = { 

231 **self.admin_site.each_context(request), 

232 "title": "Scraper test result", 

233 } 

234 

235 scraper_id = request.POST.get("scraper") 

236 if not scraper_id: 

237 context["error"] = "No scraper selected on the form — pick one and try again." 

238 return TemplateResponse(request, "admin/risk_assessment/evidence/test_scraper_result.html", context) 

239 

240 try: 

241 scraper = Scraper.objects.get(pk=scraper_id) 

242 except Scraper.DoesNotExist: 

243 context["error"] = "Selected scraper no longer exists." 

244 return TemplateResponse(request, "admin/risk_assessment/evidence/test_scraper_result.html", context) 

245 

246 url = (request.POST.get("scrape_url") or request.POST.get("evidence_link") or "").strip() 

247 context["scraper"] = scraper 

248 context["result"] = test_scraper(scraper, url) 

249 return TemplateResponse(request, "admin/risk_assessment/evidence/test_scraper_result.html", context) 

250 

251admin.site.register(Risk, RiskAdmin) 

252admin.site.register(RiskMitigation, RiskMitigationAdmin) 

253admin.site.register(RiskRating, RiskRatingAdmin) 

254admin.site.register(Evidence, EvidenceAdmin) 

255 

256# examples 

257class SeverityExampleAdmin(admin.ModelAdmin): 

258 formfield_overrides = { 

259 models.TextField: { 'widget': SummernoteWidget()} 

260 } 

261 ordering = ['severity_of_impact'] 

262 

263class LikelihoodExampleAdmin(admin.ModelAdmin): 

264 formfield_overrides = { 

265 models.TextField: { 'widget': SummernoteWidget()} 

266 } 

267 ordering = ['likelihood_of_occurrence'] 

268 

269admin.site.register(SeverityExample, SeverityExampleAdmin) 

270admin.site.register(LikelihoodExample, LikelihoodExampleAdmin) 

271 

272class SuggestedMitigationValidationAdmin(admin.ModelAdmin): 

273 formfield_overrides = { 

274 models.TextField: { 'widget': SummernoteWidget(attrs={'summernote': {'height': '200px'}})} 

275 } 

276 ordering = ['suggested_mitigation'] 

277 

278admin.site.register(SuggestedMitigationValidation, SuggestedMitigationValidationAdmin)