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
« 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
12from .models import *
14# fixtures
15class AssetAdmin(admin.ModelAdmin):
16 list_display = ["name"]
18class OriginAdmin(admin.ModelAdmin):
19 list_display = ["name"]
21class LifeCycleAdmin(admin.ModelAdmin):
22 list_display = ["name"]
24class SecurityRequirementAdmin(admin.ModelAdmin):
25 list_display = ["norm", "short", "description"]
26 prepopulated_fields = {"slug": ("norm_short", "description_short")}
28 formfield_overrides = {
29 models.TextField: { 'widget': SummernoteWidget()}
30 }
32 @admin.display(description='Norm')
33 def norm(self, obj):
34 return obj.norm_short
36 @admin.display(description='Short')
37 def short(self, obj):
38 return obj.description_short
40 @admin.display(description='Description')
41 def description(self, obj):
42 return obj.description_long
44admin.site.register(Asset, AssetAdmin)
45admin.site.register(Origin, OriginAdmin)
46admin.site.register(LifeCycle, LifeCycleAdmin)
47admin.site.register(SecurityRequirement, SecurityRequirementAdmin)
50# risks
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 )
63 class Meta:
64 model = Risk
65 fields = '__all__'
68_FIELDSETS_CREATE = [
69 (None, {
70 'fields': [
71 'custom_id', 'asset', 'origin', 'life_cycle', 'stride',
72 'title', 'description', 'suggested_mitigation_validation',
73 ],
74 }),
75]
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]
91class RiskAdmin(admin.ModelAdmin):
92 form = RiskAdminForm
93 formfield_overrides = {
94 models.TextField: {'widget': SummernoteWidget()}
95 }
96 readonly_fields = ['version', 'is_current']
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"]
102 def get_fieldsets(self, request, obj=None):
103 if obj is None:
104 return _FIELDSETS_CREATE
105 return _FIELDSETS_EDIT
107 @admin.display(description='Threat (Stride)')
108 def stride_str(self, obj):
109 return obj.stride_str
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)
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)
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)
155class RiskMitigationAdmin(admin.ModelAdmin):
156 filter_horizontal = ['security_requirements']
157 formfield_overrides = {
158 models.TextField: { 'widget': SummernoteWidget()}
159 }
161 list_display = ["title", "mitigation", "rational"]
163class RiskRatingAdmin(admin.ModelAdmin):
164 list_display = ["risk_str"]
165 ordering = ['likelihood_of_occurrence', 'severity_of_impact']
167 def risk_str(self, obj):
168 return str(obj)
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))
174update_evidences.short_description = "Update Evidences"
177def run_scraper_now(modeladmin, request, queryset):
178 from scraper.services import run_scraper_for_evidence
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
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 )
196run_scraper_now.short_description = "Run scraper now for selected evidences"
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"]
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)
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()
223 def test_scraper_view(self, request):
224 if request.method != "POST" or not self.has_change_permission(request):
225 raise PermissionDenied
227 from scraper.models import Scraper
228 from scraper.services import test_scraper
230 context = {
231 **self.admin_site.each_context(request),
232 "title": "Scraper test result",
233 }
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)
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)
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)
251admin.site.register(Risk, RiskAdmin)
252admin.site.register(RiskMitigation, RiskMitigationAdmin)
253admin.site.register(RiskRating, RiskRatingAdmin)
254admin.site.register(Evidence, EvidenceAdmin)
256# examples
257class SeverityExampleAdmin(admin.ModelAdmin):
258 formfield_overrides = {
259 models.TextField: { 'widget': SummernoteWidget()}
260 }
261 ordering = ['severity_of_impact']
263class LikelihoodExampleAdmin(admin.ModelAdmin):
264 formfield_overrides = {
265 models.TextField: { 'widget': SummernoteWidget()}
266 }
267 ordering = ['likelihood_of_occurrence']
269admin.site.register(SeverityExample, SeverityExampleAdmin)
270admin.site.register(LikelihoodExample, LikelihoodExampleAdmin)
272class SuggestedMitigationValidationAdmin(admin.ModelAdmin):
273 formfield_overrides = {
274 models.TextField: { 'widget': SummernoteWidget(attrs={'summernote': {'height': '200px'}})}
275 }
276 ordering = ['suggested_mitigation']
278admin.site.register(SuggestedMitigationValidation, SuggestedMitigationValidationAdmin)