Skip to content

Commit d5ce0b8

Browse files
refactor(test): extract Test/Test_Type/Test_Import models into dojo/test/
Phase 1 of module reorg per AGENTS.md. Move Test, Test_Type, Test_Import, Test_Import_Finding_Action + admin registrations into dojo/test/{models,admin}.py. Cross-module FKs use string refs to avoid circular imports; IMPORT_* action constants single-sourced in dojo/test/models.py with re-export in dojo/models.py. No migration change.
1 parent 5d80a01 commit d5ce0b8

4 files changed

Lines changed: 321 additions & 266 deletions

File tree

dojo/models.py

Lines changed: 12 additions & 266 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from django.core.files.base import ContentFile
2727
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator, validate_ipv46_address
2828
from django.db import connection, models
29-
from django.db.models import Count, F, JSONField, Q
29+
from django.db.models import Count, F, Q
3030
from django.db.models.expressions import Case, When
3131
from django.db.models.functions import Lower
3232
from django.urls import reverse
@@ -66,18 +66,6 @@
6666
# default template with all values set to 0
6767
DEFAULT_STATS = {sev.lower(): dict.fromkeys(STATS_FIELDS, 0) for sev in SEVERITIES}
6868

69-
IMPORT_CREATED_FINDING = "N"
70-
IMPORT_CLOSED_FINDING = "C"
71-
IMPORT_REACTIVATED_FINDING = "R"
72-
IMPORT_UNTOUCHED_FINDING = "U"
73-
74-
IMPORT_ACTIONS = [
75-
(IMPORT_CREATED_FINDING, "created"),
76-
(IMPORT_CLOSED_FINDING, "closed"),
77-
(IMPORT_REACTIVATED_FINDING, "reactivated"),
78-
(IMPORT_UNTOUCHED_FINDING, "untouched"),
79-
]
80-
8169

8270
def _get_annotations_for_statistics():
8371
annotations = {stats_field.lower(): Count(Case(When(**{stats_field: True}, then=1))) for stats_field in STATS_FIELDS if stats_field != "total"}
@@ -759,6 +747,17 @@ def clean(self):
759747

760748

761749
from dojo.product_type.models import Product_Type # noqa: E402 -- re-export; mid-file as Product FK uses it below
750+
from dojo.test.models import ( # noqa: E402 -- re-export; class-body FKs below reference these
751+
IMPORT_ACTIONS, # noqa: F401 -- re-export
752+
IMPORT_CLOSED_FINDING, # noqa: F401 -- re-export
753+
IMPORT_CREATED_FINDING, # noqa: F401 -- re-export
754+
IMPORT_REACTIVATED_FINDING, # noqa: F401 -- re-export
755+
IMPORT_UNTOUCHED_FINDING, # noqa: F401 -- re-export
756+
Test,
757+
Test_Import, # noqa: F401 -- re-export
758+
Test_Import_Finding_Action, # noqa: F401 -- re-export
759+
Test_Type,
760+
)
762761

763762

764763
class Product_Line(models.Model):
@@ -773,26 +772,6 @@ class Report_Type(models.Model):
773772
name = models.CharField(max_length=255)
774773

775774

776-
class Test_Type(models.Model):
777-
name = models.CharField(max_length=200, unique=True)
778-
static_tool = models.BooleanField(default=False)
779-
dynamic_tool = models.BooleanField(default=False)
780-
active = models.BooleanField(default=True)
781-
dynamically_generated = models.BooleanField(
782-
default=False,
783-
help_text=_("Set to True for test types that are created at import time"))
784-
785-
class Meta:
786-
ordering = ("name",)
787-
788-
def __str__(self):
789-
return self.name
790-
791-
def get_breadcrumbs(self):
792-
return [{"title": str(self),
793-
"url": None}]
794-
795-
796775
class DojoMeta(models.Model):
797776
name = models.CharField(max_length=120)
798777
value = models.CharField(max_length=300)
@@ -1978,235 +1957,6 @@ class Meta:
19781957
ordering = ("-created", )
19791958

19801959

1981-
class Test(models.Model):
1982-
engagement = models.ForeignKey(Engagement, editable=False, on_delete=models.CASCADE)
1983-
lead = models.ForeignKey(Dojo_User, editable=True, null=True, blank=True, on_delete=models.RESTRICT)
1984-
test_type = models.ForeignKey(Test_Type, on_delete=models.CASCADE)
1985-
scan_type = models.TextField(null=True)
1986-
title = models.CharField(max_length=255, null=True, blank=True)
1987-
description = models.TextField(null=True, blank=True)
1988-
target_start = models.DateTimeField()
1989-
target_end = models.DateTimeField()
1990-
percent_complete = models.IntegerField(null=True, blank=True,
1991-
editable=True)
1992-
notes = models.ManyToManyField(Notes, blank=True,
1993-
editable=False)
1994-
files = models.ManyToManyField(FileUpload, blank=True, editable=False)
1995-
environment = models.ForeignKey(Development_Environment, null=True,
1996-
blank=False, on_delete=models.RESTRICT)
1997-
1998-
updated = models.DateTimeField(auto_now=True, null=True)
1999-
created = models.DateTimeField(auto_now_add=True, null=True)
2000-
2001-
tags = TagField(blank=True, force_lowercase=True, help_text=_("Add tags that help describe this test. Choose from the list or add new tags. Press Enter key to add."))
2002-
inherited_tags = TagField(blank=True, force_lowercase=True, help_text=_("Internal use tags sepcifically for maintaining parity with product. This field will be present as a subset in the tags field"))
2003-
2004-
version = models.CharField(max_length=100, null=True, blank=True)
2005-
2006-
build_id = models.CharField(editable=True, max_length=150,
2007-
null=True, blank=True, help_text=_("Build ID that was tested, a reimport may update this field."), verbose_name=_("Build ID"))
2008-
commit_hash = models.CharField(editable=True, max_length=150,
2009-
null=True, blank=True, help_text=_("Commit hash tested, a reimport may update this field."), verbose_name=_("Commit Hash"))
2010-
branch_tag = models.CharField(editable=True, max_length=150,
2011-
null=True, blank=True, help_text=_("Tag or branch that was tested, a reimport may update this field."), verbose_name=_("Branch/Tag"))
2012-
api_scan_configuration = models.ForeignKey(Product_API_Scan_Configuration, null=True, editable=True, blank=True, on_delete=models.CASCADE, verbose_name=_("API Scan Configuration"))
2013-
2014-
class Meta:
2015-
indexes = [
2016-
models.Index(fields=["engagement", "test_type"]),
2017-
]
2018-
2019-
def __init__(self, *args, **kwargs):
2020-
super().__init__(*args, **kwargs)
2021-
self.unsaved_metadata: list = []
2022-
2023-
def __str__(self):
2024-
if self.title:
2025-
return f"{self.title} ({self.test_type})"
2026-
return str(self.test_type)
2027-
2028-
def get_absolute_url(self):
2029-
return reverse("view_test", args=[str(self.id)])
2030-
2031-
def test_type_name(self) -> str:
2032-
return self.test_type.name
2033-
2034-
def get_breadcrumbs(self):
2035-
bc = self.engagement.get_breadcrumbs()
2036-
bc += [{"title": str(self),
2037-
"url": reverse("view_test", args=(self.id,))}]
2038-
return bc
2039-
2040-
def copy(self, engagement=None):
2041-
copy = copy_model_util(self)
2042-
# Save the necessary ManyToMany relationships
2043-
old_notes = list(self.notes.all())
2044-
old_files = list(self.files.all())
2045-
old_tags = list(self.tags.all())
2046-
old_findings = list(Finding.objects.filter(test=self))
2047-
if engagement:
2048-
copy.engagement = engagement
2049-
# Save the object before setting any ManyToMany relationships
2050-
copy.save()
2051-
# Copy the notes
2052-
for notes in old_notes:
2053-
copy.notes.add(notes.copy())
2054-
# Copy the files
2055-
for files in old_files:
2056-
copy.files.add(files.copy())
2057-
# Copy the Findings
2058-
for finding in old_findings:
2059-
finding.copy(test=copy)
2060-
# Assign any tags
2061-
copy.tags.set(old_tags)
2062-
2063-
return copy
2064-
2065-
# only used by bulk risk acceptance api
2066-
@property
2067-
def unaccepted_open_findings(self):
2068-
from dojo.utils import get_system_setting # noqa: PLC0415 circular import
2069-
findings = Finding.objects.filter(risk_accepted=False, active=True, duplicate=False, test=self)
2070-
if get_system_setting("enforce_verified_status", True) or get_system_setting("enforce_verified_status_metrics", True):
2071-
findings = findings.filter(verified=True)
2072-
2073-
return findings
2074-
2075-
def accept_risks(self, accepted_risks):
2076-
self.engagement.risk_acceptance.add(*accepted_risks)
2077-
2078-
@property
2079-
def deduplication_algorithm(self):
2080-
deduplicationAlgorithm = settings.DEDUPE_ALGO_LEGACY
2081-
2082-
if hasattr(settings, "DEDUPLICATION_ALGORITHM_PER_PARSER"):
2083-
if (self.test_type.name in settings.DEDUPLICATION_ALGORITHM_PER_PARSER):
2084-
deduplicationLogger.debug(f"using DEDUPLICATION_ALGORITHM_PER_PARSER for test_type.name: {self.test_type.name}")
2085-
deduplicationAlgorithm = settings.DEDUPLICATION_ALGORITHM_PER_PARSER[self.test_type.name]
2086-
elif (self.scan_type in settings.DEDUPLICATION_ALGORITHM_PER_PARSER):
2087-
deduplicationLogger.debug(f"using DEDUPLICATION_ALGORITHM_PER_PARSER for scan_type: {self.scan_type}")
2088-
deduplicationAlgorithm = settings.DEDUPLICATION_ALGORITHM_PER_PARSER[self.scan_type]
2089-
else:
2090-
deduplicationLogger.debug("Section DEDUPLICATION_ALGORITHM_PER_PARSER not found in settings.dist.py")
2091-
2092-
deduplicationLogger.debug(f"DEDUPLICATION_ALGORITHM_PER_PARSER is: {deduplicationAlgorithm}")
2093-
return deduplicationAlgorithm
2094-
2095-
@property
2096-
def hash_code_fields(self):
2097-
"""Retrieve OS HASH_CODE_FIELDS_PER_SCANNER settings. Be aware when calling this to make sure Pro doesn't use these OS seetings"""
2098-
hashCodeFields = None
2099-
2100-
if hasattr(settings, "HASHCODE_FIELDS_PER_SCANNER"):
2101-
if (self.test_type.name in settings.HASHCODE_FIELDS_PER_SCANNER):
2102-
deduplicationLogger.debug(f"using HASHCODE_FIELDS_PER_SCANNER for test_type.name: {self.test_type.name}")
2103-
hashCodeFields = settings.HASHCODE_FIELDS_PER_SCANNER[self.test_type.name]
2104-
elif (self.scan_type in settings.HASHCODE_FIELDS_PER_SCANNER):
2105-
deduplicationLogger.debug(f"using HASHCODE_FIELDS_PER_SCANNER for scan_type: {self.scan_type}")
2106-
hashCodeFields = settings.HASHCODE_FIELDS_PER_SCANNER[self.scan_type]
2107-
else:
2108-
deduplicationLogger.warning(f"test_type name {self.test_type.name} and scan_type {self.scan_type} not found in HASHCODE_FIELDS_PER_SCANNER")
2109-
else:
2110-
deduplicationLogger.debug("Section HASHCODE_FIELDS_PER_SCANNER not found in settings.dist.py")
2111-
2112-
hash_code_fields_always = getattr(settings, "HASH_CODE_FIELDS_ALWAYS", [])
2113-
deduplicationLogger.debug(f"HASHCODE_FIELDS_PER_SCANNER is: {hashCodeFields} + HASH_CODE_FIELDS_ALWAYS: {hash_code_fields_always}")
2114-
2115-
return hashCodeFields
2116-
2117-
@property
2118-
def hash_code_allows_null_cwe(self):
2119-
hashCodeAllowsNullCwe = True
2120-
2121-
if hasattr(settings, "HASHCODE_ALLOWS_NULL_CWE"):
2122-
if (self.test_type.name in settings.HASHCODE_ALLOWS_NULL_CWE):
2123-
deduplicationLogger.debug(f"using HASHCODE_ALLOWS_NULL_CWE for test_type.name: {self.test_type.name}")
2124-
hashCodeAllowsNullCwe = settings.HASHCODE_ALLOWS_NULL_CWE[self.test_type.name]
2125-
elif (self.scan_type in settings.HASHCODE_ALLOWS_NULL_CWE):
2126-
deduplicationLogger.debug(f"using HASHCODE_ALLOWS_NULL_CWE for scan_type: {self.scan_type}")
2127-
hashCodeAllowsNullCwe = settings.HASHCODE_ALLOWS_NULL_CWE[self.scan_type]
2128-
else:
2129-
deduplicationLogger.debug("Section HASHCODE_ALLOWS_NULL_CWE not found in settings.dist.py")
2130-
2131-
deduplicationLogger.debug(f"HASHCODE_ALLOWS_NULL_CWE is: {hashCodeAllowsNullCwe}")
2132-
return hashCodeAllowsNullCwe
2133-
2134-
def delete(self, *args, product_grading_option=True, **kwargs):
2135-
logger.debug("%d test delete", self.id)
2136-
super().delete(*args, **kwargs)
2137-
if product_grading_option:
2138-
with suppress(Test.DoesNotExist, Engagement.DoesNotExist, Product.DoesNotExist):
2139-
# Suppressing a potential issue created from async delete removing
2140-
# related objects in a separate task
2141-
from dojo.utils import perform_product_grading # noqa: PLC0415 circular import
2142-
perform_product_grading(self.engagement.product)
2143-
2144-
@property
2145-
def statistics(self):
2146-
"""Queries the database, no prefetching, so could be slow for lists of model instances"""
2147-
return _get_statistics_for_queryset(Finding.objects.filter(test=self), _get_annotations_for_statistics)
2148-
2149-
2150-
class Test_Import(TimeStampedModel):
2151-
2152-
IMPORT_TYPE = "import"
2153-
REIMPORT_TYPE = "reimport"
2154-
2155-
test = models.ForeignKey(Test, editable=False, null=False, blank=False, on_delete=models.CASCADE)
2156-
findings_affected = models.ManyToManyField("Finding", through="Test_Import_Finding_Action")
2157-
import_settings = JSONField(null=True)
2158-
type = models.CharField(max_length=64, null=False, blank=False, default="unknown")
2159-
2160-
version = models.CharField(max_length=100, null=True, blank=True)
2161-
build_id = models.CharField(editable=True, max_length=150,
2162-
null=True, blank=True, help_text=_("Build ID that was tested, a reimport may update this field."), verbose_name=_("Build ID"))
2163-
commit_hash = models.CharField(editable=True, max_length=150,
2164-
null=True, blank=True, help_text=_("Commit hash tested, a reimport may update this field."), verbose_name=_("Commit Hash"))
2165-
branch_tag = models.CharField(editable=True, max_length=150,
2166-
null=True, blank=True, help_text=_("Tag or branch that was tested, a reimport may update this field."), verbose_name=_("Branch/Tag"))
2167-
2168-
def get_queryset(self):
2169-
logger.debug("prefetch test_import counts")
2170-
super_query = super().get_queryset()
2171-
super_query = super_query.annotate(created_findings_count=Count("findings", filter=Q(test_import_finding_action__action=IMPORT_CREATED_FINDING)))
2172-
super_query = super_query.annotate(closed_findings_count=Count("findings", filter=Q(test_import_finding_action__action=IMPORT_CLOSED_FINDING)))
2173-
super_query = super_query.annotate(reactivated_findings_count=Count("findings", filter=Q(test_import_finding_action__action=IMPORT_REACTIVATED_FINDING)))
2174-
return super_query.annotate(untouched_findings_count=Count("findings", filter=Q(test_import_finding_action__action=IMPORT_UNTOUCHED_FINDING)))
2175-
2176-
class Meta:
2177-
ordering = ("-id",)
2178-
indexes = [
2179-
models.Index(fields=["created", "test", "type"]),
2180-
]
2181-
2182-
def __str__(self):
2183-
return self.created.strftime("%Y-%m-%d %H:%M:%S")
2184-
2185-
@property
2186-
def statistics(self):
2187-
"""Queries the database, no prefetching, so could be slow for lists of model instances"""
2188-
stats = {}
2189-
for action in IMPORT_ACTIONS:
2190-
stats[action[1].lower()] = _get_statistics_for_queryset(Finding.objects.filter(test_import_finding_action__test_import=self, test_import_finding_action__action=action[0]), _get_annotations_for_statistics)
2191-
return stats
2192-
2193-
2194-
class Test_Import_Finding_Action(TimeStampedModel):
2195-
test_import = models.ForeignKey(Test_Import, editable=False, null=False, blank=False, on_delete=models.CASCADE)
2196-
finding = models.ForeignKey("Finding", editable=False, null=False, blank=False, on_delete=models.CASCADE)
2197-
action = models.CharField(max_length=100, null=True, blank=True, choices=IMPORT_ACTIONS)
2198-
2199-
class Meta:
2200-
indexes = [
2201-
models.Index(fields=["finding", "action", "test_import"]),
2202-
]
2203-
unique_together = (("test_import", "finding"))
2204-
ordering = ("test_import", "action", "finding")
2205-
2206-
def __str__(self):
2207-
return f"{self.finding.id}: {self.action}"
2208-
2209-
22101960
class Finding(BaseModel):
22111961
# Fields loaded when performing deduplication (used by get_finding_models_for_deduplication
22121962
# and build_candidate_scope_queryset to restrict the SELECT to only what is needed).
@@ -4352,14 +4102,12 @@ def __str__(self):
43524102
admin.site.register(Languages)
43534103
admin.site.register(Language_Type)
43544104
admin.site.register(App_Analysis)
4355-
admin.site.register(Test)
43564105
admin.site.register(Finding, FindingAdmin)
43574106
admin.site.register(FileUpload)
43584107
admin.site.register(FileAccessToken)
43594108
admin.site.register(Engagement)
43604109
admin.site.register(Risk_Acceptance)
43614110
admin.site.register(Check_List)
4362-
admin.site.register(Test_Type)
43634111
admin.site.register(Endpoint_Params)
43644112
admin.site.register(Endpoint_Status)
43654113
admin.site.register(Endpoint)
@@ -4414,6 +4162,4 @@ def __str__(self):
44144162
admin.site.register(BannerConf)
44154163
admin.site.register(Tool_Product_History)
44164164
admin.site.register(General_Survey)
4417-
admin.site.register(Test_Import)
4418-
admin.site.register(Test_Import_Finding_Action)
44194165
admin.site.register(Finding_Group)

dojo/test/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import dojo.test.admin # noqa: F401

dojo/test/admin.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from django.contrib import admin
2+
3+
from dojo.test.models import Test, Test_Import, Test_Type
4+
5+
6+
@admin.register(Test_Type)
7+
class Test_TypeAdmin(admin.ModelAdmin):
8+
9+
"""Admin support for the Test_Type model."""
10+
11+
12+
@admin.register(Test)
13+
class TestAdmin(admin.ModelAdmin):
14+
15+
"""Admin support for the Test model."""
16+
17+
18+
@admin.register(Test_Import)
19+
class Test_ImportAdmin(admin.ModelAdmin):
20+
21+
"""Admin support for the Test_Import model."""

0 commit comments

Comments
 (0)