77import warnings
88from contextlib import suppress
99from datetime import datetime , timedelta
10- from decimal import Decimal
1110from pathlib import Path
1211from typing import TYPE_CHECKING
1312from urllib .parse import urlparse
@@ -746,7 +745,12 @@ def clean(self):
746745 raise ValidationError (msg )
747746
748747
749- from dojo .product_type .models import Product_Type # noqa: E402 -- re-export; mid-file as Product FK uses it below
748+ from dojo .product .models import ( # noqa: E402 -- re-export; class-body FKs below reference these
749+ Product ,
750+ Product_API_Scan_Configuration , # noqa: F401 -- re-export
751+ Product_Line , # noqa: F401 -- re-export
752+ )
753+ from dojo .product_type .models import Product_Type # noqa: E402, F401 -- re-export
750754from dojo .test .models import ( # noqa: E402 -- re-export; class-body FKs below reference these
751755 IMPORT_ACTIONS , # noqa: F401 -- re-export
752756 IMPORT_CLOSED_FINDING , # noqa: F401 -- re-export
@@ -760,14 +764,6 @@ def clean(self):
760764)
761765
762766
763- class Product_Line (models .Model ):
764- name = models .CharField (max_length = 300 )
765- description = models .CharField (max_length = 2000 )
766-
767- def __str__ (self ):
768- return self .name
769-
770-
771767class Report_Type (models .Model ):
772768 name = models .CharField (max_length = 255 )
773769
@@ -957,257 +953,6 @@ def get_summary(self):
957953 return f"{ self .name } - Critical: { self .critical } , High: { self .high } , Medium: { self .medium } , Low: { self .low } "
958954
959955
960- class Product (BaseModel ):
961- WEB_PLATFORM = "web"
962- IOT = "iot"
963- DESKTOP_PLATFORM = "desktop"
964- MOBILE_PLATFORM = "mobile"
965- WEB_SERVICE_PLATFORM = "web service"
966- PLATFORM_CHOICES = (
967- (WEB_SERVICE_PLATFORM , _ ("API" )),
968- (DESKTOP_PLATFORM , _ ("Desktop" )),
969- (IOT , _ ("Internet of Things" )),
970- (MOBILE_PLATFORM , _ ("Mobile" )),
971- (WEB_PLATFORM , _ ("Web" )),
972- )
973-
974- CONSTRUCTION = "construction"
975- PRODUCTION = "production"
976- RETIREMENT = "retirement"
977- LIFECYCLE_CHOICES = (
978- (CONSTRUCTION , _ ("Construction" )),
979- (PRODUCTION , _ ("Production" )),
980- (RETIREMENT , _ ("Retirement" )),
981- )
982-
983- THIRD_PARTY_LIBRARY_ORIGIN = "third party library"
984- PURCHASED_ORIGIN = "purchased"
985- CONTRACTOR_ORIGIN = "contractor"
986- INTERNALLY_DEVELOPED_ORIGIN = "internal"
987- OPEN_SOURCE_ORIGIN = "open source"
988- OUTSOURCED_ORIGIN = "outsourced"
989- ORIGIN_CHOICES = (
990- (THIRD_PARTY_LIBRARY_ORIGIN , _ ("Third Party Library" )),
991- (PURCHASED_ORIGIN , _ ("Purchased" )),
992- (CONTRACTOR_ORIGIN , _ ("Contractor Developed" )),
993- (INTERNALLY_DEVELOPED_ORIGIN , _ ("Internally Developed" )),
994- (OPEN_SOURCE_ORIGIN , _ ("Open Source" )),
995- (OUTSOURCED_ORIGIN , _ ("Outsourced" )),
996- )
997-
998- VERY_HIGH_CRITICALITY = "very high"
999- HIGH_CRITICALITY = "high"
1000- MEDIUM_CRITICALITY = "medium"
1001- LOW_CRITICALITY = "low"
1002- VERY_LOW_CRITICALITY = "very low"
1003- NONE_CRITICALITY = "none"
1004- BUSINESS_CRITICALITY_CHOICES = (
1005- (VERY_HIGH_CRITICALITY , _ ("Very High" )),
1006- (HIGH_CRITICALITY , _ ("High" )),
1007- (MEDIUM_CRITICALITY , _ ("Medium" )),
1008- (LOW_CRITICALITY , _ ("Low" )),
1009- (VERY_LOW_CRITICALITY , _ ("Very Low" )),
1010- (NONE_CRITICALITY , _ ("None" )),
1011- )
1012-
1013- name = models .CharField (max_length = 255 , unique = True )
1014- description = models .CharField (max_length = 4000 )
1015-
1016- product_manager = models .ForeignKey (Dojo_User , null = True , blank = True ,
1017- related_name = "product_manager" , on_delete = models .RESTRICT )
1018- technical_contact = models .ForeignKey (Dojo_User , null = True , blank = True ,
1019- related_name = "technical_contact" , on_delete = models .RESTRICT )
1020- team_manager = models .ForeignKey (Dojo_User , null = True , blank = True ,
1021- related_name = "team_manager" , on_delete = models .RESTRICT )
1022-
1023- prod_type = models .ForeignKey (Product_Type , related_name = "prod_type" ,
1024- null = False , blank = False , on_delete = models .CASCADE )
1025- sla_configuration = models .ForeignKey (SLA_Configuration ,
1026- related_name = "sla_config" ,
1027- null = False ,
1028- blank = False ,
1029- default = 1 ,
1030- on_delete = models .RESTRICT )
1031- tid = models .IntegerField (default = 0 , editable = False )
1032- authorized_users = models .ManyToManyField (Dojo_User , related_name = "authorized_products" , blank = True )
1033- prod_numeric_grade = models .IntegerField (null = True , blank = True )
1034-
1035- # Metadata
1036- business_criticality = models .CharField (max_length = 9 , choices = BUSINESS_CRITICALITY_CHOICES , blank = True , null = True )
1037- platform = models .CharField (max_length = 11 , choices = PLATFORM_CHOICES , blank = True , null = True )
1038- lifecycle = models .CharField (max_length = 12 , choices = LIFECYCLE_CHOICES , blank = True , null = True )
1039- origin = models .CharField (max_length = 19 , choices = ORIGIN_CHOICES , blank = True , null = True )
1040- user_records = models .PositiveIntegerField (blank = True , null = True , help_text = _ ("Estimate the number of user records within the application." ))
1041- revenue = models .DecimalField (max_digits = 15 , decimal_places = 2 , blank = True , null = True , validators = [MinValueValidator (Decimal ("0.00" ))], help_text = _ ("Estimate the application's revenue." ))
1042- external_audience = models .BooleanField (default = False , help_text = _ ("Specify if the application is used by people outside the organization." ))
1043- internet_accessible = models .BooleanField (default = False , help_text = _ ("Specify if the application is accessible from the public internet." ))
1044- regulations = models .ManyToManyField (Regulation , blank = True )
1045-
1046- tags = TagField (blank = True , force_lowercase = True , help_text = _ ("Add tags that help describe this product. Choose from the list or add new tags. Press Enter key to add." ))
1047- enable_product_tag_inheritance = models .BooleanField (
1048- default = False ,
1049- blank = False ,
1050- verbose_name = _ ("Enable Product Tag Inheritance" ),
1051- help_text = _ ("Enables product tag inheritance. Any tags added on a product will automatically be added to all Engagements, Tests, and Findings" ))
1052- enable_simple_risk_acceptance = models .BooleanField (default = False , help_text = _ ("Allows simple risk acceptance by checking/unchecking a checkbox." ))
1053- enable_full_risk_acceptance = models .BooleanField (default = True , help_text = _ ("Allows full risk acceptance using a risk acceptance form, expiration date, uploaded proof, etc." ))
1054-
1055- disable_sla_breach_notifications = models .BooleanField (
1056- default = False ,
1057- blank = False ,
1058- verbose_name = _ ("Disable SLA breach notifications" ),
1059- help_text = _ ("Disable SLA breach notifications if configured in the global settings" ))
1060- async_updating = models .BooleanField (default = False ,
1061- help_text = _ ("Findings under this Product or SLA configuration are asynchronously being updated" ))
1062-
1063- class Meta :
1064- ordering = ("name" ,)
1065-
1066- def __str__ (self ):
1067- return self .name
1068-
1069- def save (self , * args , ** kwargs ):
1070- # get the product's sla config before saving (if this is an existing product)
1071- initial_sla_config = None
1072- if self .pk is not None :
1073- initial_sla_config = getattr (Product .objects .get (pk = self .pk ), "sla_configuration" , None )
1074- # if initial sla config exists and async finding update is already running, revert sla config before saving
1075- if initial_sla_config and self .async_updating :
1076- self .sla_configuration = initial_sla_config
1077-
1078- super ().save (* args , ** kwargs )
1079-
1080- # if the initial sla config exists and async finding update is not running
1081- if initial_sla_config is not None and not self .async_updating :
1082- # get the new sla config from the saved product
1083- new_sla_config = getattr (self , "sla_configuration" , None )
1084- # if the sla config has changed, update finding sla expiration dates within this product
1085- if new_sla_config and (initial_sla_config != new_sla_config ):
1086- # set the async updating flag to true for this product
1087- self .async_updating = True
1088- super ().save (* args , ** kwargs )
1089- # set the async updating flag to true for the sla config assigned to this product
1090- sla_config = getattr (self , "sla_configuration" , None )
1091- if sla_config :
1092- sla_config .async_updating = True
1093- super (SLA_Configuration , sla_config ).save ()
1094- # launch the async task to update all finding sla expiration dates
1095- from dojo .sla_config .helpers import async_update_sla_expiration_dates_sla_config_sync # noqa: I001, PLC0415 circular import
1096- from dojo .celery_dispatch import dojo_dispatch_task # noqa: PLC0415 circular import
1097-
1098- dojo_dispatch_task (
1099- async_update_sla_expiration_dates_sla_config_sync ,
1100- sla_config .id ,
1101- [self .id ],
1102- )
1103- # The async task refetches and resets async_updating on its own copies.
1104- # Mirror that on this in-memory product and the in-memory sla_config so a
1105- # subsequent save() on either does not trigger their lock-revert paths.
1106- self .async_updating = False
1107- if sla_config :
1108- sla_config .async_updating = False
1109-
1110- def get_absolute_url (self ):
1111- return reverse ("view_product" , args = [str (self .id )])
1112-
1113- @cached_property
1114- def findings_count (self ):
1115- try :
1116- # if prefetched, it's already there
1117- return self .active_finding_count
1118- except AttributeError :
1119- # ideally it's always prefetched and we can remove this code in the future
1120- self .active_finding_count = Finding .objects .filter (active = True ,
1121- test__engagement__product = self ).count ()
1122- return self .active_finding_count
1123-
1124- @cached_property
1125- def findings_active_verified_count (self ):
1126- try :
1127- # if prefetched, it's already there
1128- return self .active_verified_finding_count
1129- except AttributeError :
1130- # ideally it's always prefetched and we can remove this code in the future
1131- self .active_verified_finding_count = Finding .objects .filter (active = True ,
1132- verified = True ,
1133- test__engagement__product = self ).count ()
1134- return self .active_verified_finding_count
1135-
1136- # TODO: Delete this after the move to Locations
1137- @cached_property
1138- def endpoint_host_count (self ):
1139- # active_endpoints is (should be) prefetched
1140- endpoints = getattr (self , "active_endpoints" , None )
1141-
1142- hosts = []
1143- for e in endpoints :
1144- if e .host in hosts :
1145- continue
1146- hosts .append (e .host )
1147-
1148- return len (hosts )
1149-
1150- # TODO: Delete this after the move to Locations
1151- @cached_property
1152- def endpoint_count (self ):
1153- # active_endpoints is (should be) prefetched
1154- endpoints = getattr (self , "active_endpoints" , None )
1155- if endpoints :
1156- return len (self .active_endpoints )
1157- return 0
1158-
1159- def open_findings (self , start_date = None , end_date = None ):
1160- if start_date is None or end_date is None :
1161- return {}
1162-
1163- from dojo .utils import get_system_setting # noqa: PLC0415 circular import
1164- findings = Finding .objects .filter (test__engagement__product = self ,
1165- mitigated__isnull = True ,
1166- false_p = False ,
1167- duplicate = False ,
1168- out_of_scope = False ,
1169- date__range = [start_date ,
1170- end_date ])
1171-
1172- if get_system_setting ("enforce_verified_status" , True ) or get_system_setting ("enforce_verified_status_metrics" , True ):
1173- findings = findings .filter (verified = True )
1174-
1175- critical = findings .filter (severity = "Critical" ).count ()
1176- high = findings .filter (severity = "High" ).count ()
1177- medium = findings .filter (severity = "Medium" ).count ()
1178- low = findings .filter (severity = "Low" ).count ()
1179-
1180- return {"Critical" : critical ,
1181- "High" : high ,
1182- "Medium" : medium ,
1183- "Low" : low ,
1184- "Total" : (critical + high + medium + low )}
1185-
1186- def get_breadcrumbs (self ):
1187- return [{"title" : str (self ),
1188- "url" : reverse ("view_product" , args = (self .id ,))}]
1189-
1190- @property
1191- def get_product_type (self ):
1192- return self .prod_type if self .prod_type is not None else "unknown"
1193-
1194- # only used in APIv2 serializers.py, should be deprecated or at least prefetched
1195- def open_findings_list (self ):
1196- findings = Finding .objects .filter (test__engagement__product = self , active = True ).values_list ("id" , flat = True )
1197- return list (findings )
1198-
1199- @property
1200- def has_jira_configured (self ):
1201- from dojo .jira import services as jira_services # noqa: PLC0415 circular import
1202- return jira_services .has_configured (self )
1203-
1204- def violates_sla (self ):
1205- findings = Finding .objects .filter (test__engagement__product = self ,
1206- active = True ,
1207- sla_expiration_date__lt = timezone .now ().date ())
1208- return findings .count () > 0
1209-
1210-
1211956class Tool_Type (models .Model ):
1212957 name = models .CharField (max_length = 200 )
1213958 description = models .CharField (max_length = 2000 , null = True , blank = True )
@@ -1248,31 +993,6 @@ def __str__(self):
1248993 return self .name
1249994
1250995
1251- class Product_API_Scan_Configuration (models .Model ):
1252- product = models .ForeignKey (Product , null = False , blank = False , on_delete = models .CASCADE )
1253- tool_configuration = models .ForeignKey (Tool_Configuration , null = False , blank = False , on_delete = models .CASCADE )
1254- service_key_1 = models .CharField (max_length = 200 , null = True , blank = True )
1255- service_key_2 = models .CharField (max_length = 200 , null = True , blank = True )
1256- service_key_3 = models .CharField (max_length = 200 , null = True , blank = True )
1257-
1258- def __str__ (self ):
1259- name = self .tool_configuration .name
1260- if self .service_key_1 or self .service_key_2 or self .service_key_3 :
1261- name += f" ({ self .details } )"
1262- return name
1263-
1264- @property
1265- def details (self ):
1266- details = ""
1267- if self .service_key_1 :
1268- details += f"{ self .service_key_1 } "
1269- if self .service_key_2 :
1270- details += f" | { self .service_key_2 } "
1271- if self .service_key_3 :
1272- details += f" | { self .service_key_3 } "
1273- return details
1274-
1275-
1276996# declare form here as we can't import forms.py due to circular imports not even locally
1277997class ToolConfigForm_Admin (forms .ModelForm ):
1278998 password = forms .CharField (widget = forms .PasswordInput , required = False )
@@ -3948,7 +3668,6 @@ def __str__(self):
39483668admin .site .register (Endpoint_Params )
39493669admin .site .register (Endpoint_Status )
39503670admin .site .register (Endpoint )
3951- admin .site .register (Product )
39523671admin .site .register (UserContactInfo )
39533672admin .site .register (Notes )
39543673admin .site .register (Note_Type )
@@ -3986,10 +3705,8 @@ def __str__(self):
39863705
39873706admin .site .register (Contact )
39883707admin .site .register (NoteHistory )
3989- admin .site .register (Product_Line )
39903708admin .site .register (Report_Type )
39913709admin .site .register (DojoMeta )
3992- admin .site .register (Product_API_Scan_Configuration )
39933710admin .site .register (Development_Environment )
39943711admin .site .register (Finding_Template )
39953712admin .site .register (Vulnerability_Id )
0 commit comments