Coverage for product_risk_suite/product/admin.py: 93%
111 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.contrib import admin, messages
2from django.forms import SelectMultiple
3from django.db import models
5from .models import Product, ProductRiskAnalysis, ProductRiskEntry
6from risk_assessment.models import Risk
7from threat_model.shared_models import ThreatModelConnectionName
8from guardian.admin import GuardedModelAdmin
10class ProductAdmin(GuardedModelAdmin):
11 list_display = ["title", "description"]
12 prepopulated_fields = {"slug": ("title",)}
14class ProductRiskAnalysisAdmin(admin.ModelAdmin):
15 filter_horizontal = ['risk_entries']
17 list_display = ["name", "n_entries"]
18 prepopulated_fields = {"slug": ("name",)}
20 @admin.display(description='N Entries')
21 def n_entries(self, obj):
22 return len(obj.risk_entries.all())
24def upgrade_to_latest_risk_version(modeladmin, request, queryset):
25 upgraded = 0
26 already_current = 0
27 for entry in queryset.select_related('risk'):
28 if entry.risk.is_current:
29 already_current += 1
30 continue
31 try:
32 latest = Risk.objects.get(custom_id=entry.risk.custom_id, is_current=True)
33 entry.risk = latest
34 entry.save()
35 upgraded += 1
36 except Risk.DoesNotExist:
37 modeladmin.message_user(
38 request,
39 f"No current version found for {entry.risk.custom_id} — skipped.",
40 messages.ERROR,
41 )
43 if upgraded:
44 modeladmin.message_user(request, f"Upgraded {upgraded} entry/entries to the latest risk version.", messages.SUCCESS)
45 if already_current:
46 modeladmin.message_user(request, f"{already_current} entry/entries were already on the current version.", messages.INFO)
48upgrade_to_latest_risk_version.short_description = "Upgrade to latest risk version"
51class ProductRiskEntryAdmin(admin.ModelAdmin):
52 readonly_fields = ('created_at_display', 'last_change_display')
53 ordering = ['risk__custom_id']
55 def created_at_display(self, obj):
56 return obj.created_at.strftime('%Y-%m-%d %H:%M:%S') if obj.created_at else '-'
57 created_at_display.short_description = 'Created at'
59 def last_change_display(self, obj):
60 return obj.last_change.strftime('%Y-%m-%d %H:%M:%S') if obj.last_change else '-'
61 last_change_display.short_description = 'Last update'
63 def get_form(self, request, obj=None, **kwargs):
64 form = super().get_form(request, obj, **kwargs)
65 if obj is None:
66 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."
67 else:
68 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
69 return form
71 def formfield_for_manytomany(self, db_field, request, **kwargs):
72 if db_field.name == "svg_id":
73 # Get the current ProductRiskEntry instance (if editing)
74 obj = request.resolver_match.kwargs.get('object_id')
75 if obj:
76 try:
77 instance = ProductRiskEntry.objects.get(pk=obj)
78 ras = ProductRiskAnalysis.objects.get(risk_entries=instance)
79 products = Product.objects.get(analyzes=ras)
80 connection_names = ThreatModelConnectionName.objects.filter(
81 threatmodel__product=products
82 ).distinct().order_by("tech_name")
83 kwargs['queryset'] = connection_names
84 except (ProductRiskEntry.DoesNotExist, AttributeError):
85 pass
86 except (ProductRiskAnalysis.DoesNotExist, AttributeError):
87 pass
88 except (Product.DoesNotExist, AttributeError):
89 pass
90 else:
91 # for a new instance ALL available tm link names are listed
92 pass
93 return super().formfield_for_manytomany(db_field, request, **kwargs)
95 filter_horizontal = ['evidences','svg_id']
96 actions = [upgrade_to_latest_risk_version]
98 list_display = [
99 "risk_Id",
100 "risk_outdated",
101 "risk_title",
102 "risk_asset",
103 "risk_origin",
104 "risk_stride",
105 "tm_linked",
106 "risk_initial",
107 "risk_accepted",
108 "risk_mitigated",
109 "risk_after_mitigation",
110 ]
112 @admin.display(description='Risk Id')
113 def risk_Id(self, obj):
114 return obj.risk.custom_id
116 @admin.display(description='Current version', boolean=True)
117 def risk_outdated(self, obj):
118 return not obj.risk.has_newer_version
120 @admin.display(description='risk title')
121 def risk_title(self, obj):
122 return obj.risk.title
124 @admin.display(description='Asset')
125 def risk_asset(self, obj):
126 return obj.risk.asset
128 @admin.display(description='Origin')
129 def risk_origin(self, obj):
130 return obj.risk.origin
132 @admin.display(description='Stride')
133 def risk_stride(self, obj):
134 return obj.risk.stride_str
136 @admin.display(description='Risk Accepted')
137 def risk_accepted(self, obj):
138 return obj.risk_accepted
140 @admin.display(description='TM Linked')
141 def tm_linked(self, obj):
142 return len(obj.svg_id.all()) > 0
143 tm_linked.boolean = True
145 @admin.display(description='Risk Initial')
146 def risk_initial(self, obj):
147 return obj.risk_rating_initial.risk
149 @admin.display(description='Mitigated')
150 def risk_mitigated(self, obj):
151 return obj.risk_mitigation != None
153 @admin.display(description='Risk after Mitigation')
154 def risk_after_mitigation(self, obj):
155 if obj.risk_rating_after_mitigation:
156 return obj.risk_rating_after_mitigation.risk
157 return None
159admin.site.register(Product, ProductAdmin)
160admin.site.register(ProductRiskAnalysis, ProductRiskAnalysisAdmin)
161admin.site.register(ProductRiskEntry, ProductRiskEntryAdmin)