Coverage for product_risk_suite/product/admin.py: 92%
123 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 import forms
2from django.contrib import admin, messages
3from django.contrib.auth import get_user_model
4from django.contrib.auth.models import Group
5from django.forms import SelectMultiple
6from django.db import models
7from django.utils.translation import gettext_lazy as _
9from .models import Product, ProductRiskAnalysis, ProductRiskEntry
10from risk_assessment.models import Risk
11from threat_model.shared_models import ThreatModelConnectionName
12from guardian.admin import GuardedModelAdmin
15class ObjPermUserSelectForm(forms.Form):
16 user = forms.ModelChoiceField(
17 label=_("User identification"),
18 queryset=get_user_model()._default_manager.order_by(get_user_model().USERNAME_FIELD),
19 )
22class ObjPermGroupSelectForm(forms.Form):
23 group = forms.ModelChoiceField(
24 label=_("Group"),
25 queryset=Group.objects.order_by("name"),
26 )
29class ProductAdmin(GuardedModelAdmin):
30 list_display = ["title", "description"]
31 prepopulated_fields = {"slug": ("title",)}
33 def get_obj_perms_user_select_form(self, request):
34 return ObjPermUserSelectForm
36 def get_obj_perms_group_select_form(self, request):
37 return ObjPermGroupSelectForm
39class ProductRiskAnalysisAdmin(admin.ModelAdmin):
40 filter_horizontal = ['risk_entries']
42 list_display = ["name", "n_entries"]
43 prepopulated_fields = {"slug": ("name",)}
45 @admin.display(description='N Entries')
46 def n_entries(self, obj):
47 return len(obj.risk_entries.all())
49def upgrade_to_latest_risk_version(modeladmin, request, queryset):
50 upgraded = 0
51 already_current = 0
52 for entry in queryset.select_related('risk'):
53 if entry.risk.is_current:
54 already_current += 1
55 continue
56 try:
57 latest = Risk.objects.get(custom_id=entry.risk.custom_id, is_current=True)
58 entry.risk = latest
59 entry.save()
60 upgraded += 1
61 except Risk.DoesNotExist:
62 modeladmin.message_user(
63 request,
64 f"No current version found for {entry.risk.custom_id} — skipped.",
65 messages.ERROR,
66 )
68 if upgraded:
69 modeladmin.message_user(request, f"Upgraded {upgraded} entry/entries to the latest risk version.", messages.SUCCESS)
70 if already_current:
71 modeladmin.message_user(request, f"{already_current} entry/entries were already on the current version.", messages.INFO)
73upgrade_to_latest_risk_version.short_description = "Upgrade to latest risk version"
76class ProductRiskEntryAdmin(admin.ModelAdmin):
77 readonly_fields = ('created_at_display', 'last_change_display')
78 ordering = ['risk__custom_id']
80 def created_at_display(self, obj):
81 return obj.created_at.strftime('%Y-%m-%d %H:%M:%S') if obj.created_at else '-'
82 created_at_display.short_description = 'Created at'
84 def last_change_display(self, obj):
85 return obj.last_change.strftime('%Y-%m-%d %H:%M:%S') if obj.last_change else '-'
86 last_change_display.short_description = 'Last update'
88 def get_form(self, request, obj=None, **kwargs):
89 form = super().get_form(request, obj, **kwargs)
90 if obj is None:
91 form.base_fields['svg_id'].help_text = "Here all available IDs are listed, if you do not know which matches, assign this entry to a product analysis and that to a product, then this list is reduced."
92 else:
93 form.base_fields['svg_id'].help_text = "Select one or more threat model connections this risk applies to." + "<br>" + form.base_fields['svg_id'].help_text
94 return form
96 def formfield_for_manytomany(self, db_field, request, **kwargs):
97 if db_field.name == "svg_id":
98 # Get the current ProductRiskEntry instance (if editing)
99 obj = request.resolver_match.kwargs.get('object_id')
100 if obj:
101 try:
102 instance = ProductRiskEntry.objects.get(pk=obj)
103 ras = ProductRiskAnalysis.objects.get(risk_entries=instance)
104 products = Product.objects.get(analyzes=ras)
105 connection_names = ThreatModelConnectionName.objects.filter(
106 threatmodel__product=products
107 ).distinct().order_by("tech_name")
108 kwargs['queryset'] = connection_names
109 except (ProductRiskEntry.DoesNotExist, AttributeError):
110 pass
111 except (ProductRiskAnalysis.DoesNotExist, AttributeError):
112 pass
113 except (Product.DoesNotExist, AttributeError):
114 pass
115 else:
116 # for a new instance ALL available tm link names are listed
117 pass
118 return super().formfield_for_manytomany(db_field, request, **kwargs)
120 filter_horizontal = ['evidences','svg_id']
121 actions = [upgrade_to_latest_risk_version]
123 list_display = [
124 "risk_Id",
125 "risk_outdated",
126 "risk_title",
127 "risk_asset",
128 "risk_origin",
129 "risk_stride",
130 "tm_linked",
131 "risk_initial",
132 "risk_accepted",
133 "risk_mitigated",
134 "risk_after_mitigation",
135 ]
137 @admin.display(description='Risk Id')
138 def risk_Id(self, obj):
139 return obj.risk.custom_id
141 @admin.display(description='Current version', boolean=True)
142 def risk_outdated(self, obj):
143 return not obj.risk.has_newer_version
145 @admin.display(description='risk title')
146 def risk_title(self, obj):
147 return obj.risk.title
149 @admin.display(description='Asset')
150 def risk_asset(self, obj):
151 return obj.risk.asset
153 @admin.display(description='Origin')
154 def risk_origin(self, obj):
155 return obj.risk.origin
157 @admin.display(description='Stride')
158 def risk_stride(self, obj):
159 return obj.risk.stride_str
161 @admin.display(description='Risk Accepted')
162 def risk_accepted(self, obj):
163 return obj.risk_accepted
165 @admin.display(description='TM Linked')
166 def tm_linked(self, obj):
167 return len(obj.svg_id.all()) > 0
168 tm_linked.boolean = True
170 @admin.display(description='Risk Initial')
171 def risk_initial(self, obj):
172 return obj.risk_rating_initial.risk
174 @admin.display(description='Mitigated')
175 def risk_mitigated(self, obj):
176 return obj.risk_mitigation != None
178 @admin.display(description='Risk after Mitigation')
179 def risk_after_mitigation(self, obj):
180 if obj.risk_rating_after_mitigation:
181 return obj.risk_rating_after_mitigation.risk
182 return None
184admin.site.register(Product, ProductAdmin)
185admin.site.register(ProductRiskAnalysis, ProductRiskAnalysisAdmin)
186admin.site.register(ProductRiskEntry, ProductRiskEntryAdmin)