Skip to content

Commit 568154e

Browse files
Maffoochclaude
andcommitted
Vulnerability id entity-only cutover: drop legacy Vulnerability_Id
Read vulnerability ids exclusively from the Vulnerability entity + FindingVulnerabilityReference through-table. Remove the read flag and all flag-off branches, delete the legacy Vulnerability_Id model, and add migration 0287 dropping dojo_vulnerability_id (0286 backfills the entities first). - MF2: backfill no longer inserts cve-only references (preserves hash parity) - MF4: UniqueConstraint(finding, order) on the reference table - Reimporter reads finding_vulnerability_id_strings (fixes a latent N+1) - Repoint tests to the entity store; remove legacy-model coverage; delete the migrate_cve / verify_vulnerability_ids commands NOTE: test_tag_inheritance_perf EXPECTED_* query counts need recalibration under watson-enabled CI after the dual-write removal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6b369a7 commit 568154e

27 files changed

Lines changed: 237 additions & 1065 deletions

dojo/api_v2/prefetch/registrations.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
from dojo.engagement.queries import get_authorized_engagements
3838
from dojo.finding.queries import (
3939
get_authorized_findings,
40-
get_authorized_vulnerability_ids,
4140
)
4241
from dojo.finding_group.queries import get_authorized_finding_groups
4342
from dojo.github.models import GITHUB_Issue, GITHUB_PKey
@@ -94,7 +93,6 @@
9493
Tool_Product_Settings,
9594
Tool_Type,
9695
UserContactInfo,
97-
Vulnerability_Id,
9896
)
9997
from dojo.notifications.models import Notification_Webhooks, Notifications
10098
from dojo.product.queries import (
@@ -223,7 +221,6 @@
223221
# Models where we can simply fall back to a `get_authorized_*` method to check auth
224222
for model, helper in (
225223
(Finding_Group, get_authorized_finding_groups),
226-
(Vulnerability_Id, get_authorized_vulnerability_ids),
227224
(Vulnerability, get_authorized_vulnerability_id_entities),
228225
(FindingVulnerabilityReference, get_authorized_finding_vulnerability_references),
229226
):

dojo/apps.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,8 @@ def register_watson_models(app_config):
113113
watson.register(app_config.get_model("Location"))
114114
watson.register(app_config.get_model("Engagement"), fields=get_model_fields_with_extra(app_config.get_model("Engagement"), ("id", "product__name")), store=("product__name", ))
115115
watson.register(app_config.get_model("App_Analysis"))
116-
# Watson stays on the legacy Vulnerability_Id table (unconditionally dual-written), so global
117-
# search keeps working identically in both flag states with no buildwatson reindex. Moving the
118-
# watson index (and the classic search view) to FindingVulnerabilityReference is a legacy-drop
119-
# phase concern, not this reads-only flag flip.
120-
watson.register(app_config.get_model("Vulnerability_Id"), store=("finding__test__engagement__product__name", ))
116+
# The legacy Vulnerability_Id table was retired (entity-only cutover); the classic watson-backed
117+
# vuln-id search is gone. The Vue global search covers vulnerability ids via the entity store.
121118

122119
# YourModel = app_config.get_model("YourModel")
123120
# watson.register(YourModel)

dojo/authorization/query_registrations.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
Test,
3939
Test_Import,
4040
Tool_Product_Settings,
41-
Vulnerability_Id,
4241
)
4342
from dojo.request_cache import cache_for_request_or_task
4443
from dojo.vulnerability.models import FindingVulnerabilityReference, Vulnerability
@@ -405,20 +404,6 @@ def _get_authorized_findings(permission, queryset=None, user=None):
405404
register_auth_filter("finding.get_authorized_findings_for_queryset", _get_authorized_findings)
406405

407406

408-
def _get_authorized_vulnerability_ids(permission, queryset=None, user=None):
409-
user = _resolve_user(user)
410-
qs = queryset if queryset is not None else Vulnerability_Id.objects.all()
411-
if user is None or getattr(user, "is_anonymous", False):
412-
return qs.none()
413-
if _is_unrestricted(user, permission_to_action(permission)):
414-
return qs
415-
return qs.filter(finding__test__engagement__product__id__in=_authorized_product_ids(user))
416-
417-
418-
register_auth_filter("finding.get_authorized_vulnerability_ids", _get_authorized_vulnerability_ids)
419-
register_auth_filter("finding.get_authorized_vulnerability_ids_for_queryset", _get_authorized_vulnerability_ids)
420-
421-
422407
def _get_authorized_vulnerability_id_entities(permission, queryset=None, user=None):
423408
user = _resolve_user(user)
424409
qs = queryset if queryset is not None else Vulnerability.objects.all()
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Entity-only cutover: drop the legacy Vulnerability_Id store. Vulnerability ids now live entirely in
2+
# the Vulnerability entity + FindingVulnerabilityReference through-table (created in 0285, backfilled
3+
# from dojo_vulnerability_id in 0286 — which runs BEFORE this drop). ⚠️ DESTRUCTIVE: drops
4+
# dojo_vulnerability_id and its data. On upgrading installs, ensure 0286's backfill (or
5+
# `manage.py migrate_vulnerability_ids` pre-run) has completed before this migration removes the source.
6+
7+
from django.db import migrations
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
dependencies = [
13+
("dojo", "0286_backfill_vulnerability_id_entities"),
14+
]
15+
16+
operations = [
17+
migrations.DeleteModel(
18+
name="Vulnerability_Id",
19+
),
20+
]

dojo/finding/admin.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
Finding,
77
Finding_Group,
88
Finding_Template,
9-
Vulnerability_Id,
109
)
1110

1211

@@ -26,12 +25,6 @@ class FindingTemplateAdmin(admin.ModelAdmin):
2625
"""Admin support for the Finding_Template model."""
2726

2827

29-
@admin.register(Vulnerability_Id)
30-
class VulnerabilityIdAdmin(admin.ModelAdmin):
31-
32-
"""Admin support for the Vulnerability_Id model."""
33-
34-
3528
@admin.register(Finding_Group)
3629
class FindingGroupAdmin(admin.ModelAdmin):
3730

dojo/finding/api/serializer.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
Test,
4444
Test_Type,
4545
User,
46-
Vulnerability_Id,
46+
Vulnerability,
4747
)
4848
from dojo.notifications.helper import async_create_notification
4949
from dojo.user.queries import get_authorized_users
@@ -297,7 +297,9 @@ def get_jira(self, obj):
297297

298298
class VulnerabilityIdSerializer(serializers.ModelSerializer):
299299
class Meta:
300-
model = Vulnerability_Id
300+
# Schema-only (OpenAPI) shape for VulnerabilityIdsField; points at the Vulnerability entity,
301+
# which carries the same ``vulnerability_id`` string column.
302+
model = Vulnerability
301303
fields = ["vulnerability_id"]
302304

303305

dojo/finding/models.py

Lines changed: 10 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525

2626
from dojo.base_models.base import BaseModel
2727
from dojo.finding.cwe import cwe_label, finding_cwe_labels
28-
from dojo.finding.vulnerability_id import resolve_vulnerability_id_type
2928

3029
# get_current_date/tomorrow/copy_model_util are defined early in dojo.models, before the
3130
# re-export that loads this module — so this resolves despite the partial circular load, and
@@ -846,18 +845,11 @@ def _get_unsaved_vulnerability_ids(finding) -> str:
846845

847846
def _get_saved_vulnerability_ids(finding) -> str:
848847
if finding.id is not None:
849-
from dojo.vulnerability.queries import use_entity_reads # noqa: PLC0415
850-
if use_entity_reads():
851-
# Entity store: the prefetch-honoring reverse relation (vulnerability_references
852-
# + select_related("vulnerability")) so Prefetch is honored — no N+1 in dedupe.
853-
vulnerability_id_str_list = [ref.vulnerability.vulnerability_id for ref in finding.vulnerability_references.all()]
854-
else:
855-
# Use the reverse relation (vulnerability_id_set) rather than a fresh
856-
# Vulnerability_Id.objects.filter(...) so prefetch_related("vulnerability_id_set")
857-
# is honored — avoids an N+1 (COUNT + SELECT per finding) during dedupe/hashcode.
858-
vulnerability_id_str_list = [str(vulnerability_id) for vulnerability_id in finding.vulnerability_id_set.all()]
848+
# Entity store: the prefetch-honoring reverse relation (vulnerability_references
849+
# + select_related("vulnerability")) so Prefetch is honored — no N+1 in dedupe.
850+
vulnerability_id_str_list = [ref.vulnerability.vulnerability_id for ref in finding.vulnerability_references.all()]
859851
deduplicationLogger.debug("get_vulnerability_ids after the finding was saved. Vulnerability references count: " + str(len(vulnerability_id_str_list)))
860-
# sort vulnerability_ids strings (no dedupe — byte parity with the legacy store)
852+
# sort vulnerability_ids strings (no dedupe — ordering is irrelevant to the hash)
861853
return "".join(sorted(vulnerability_id_str_list))
862854
return ""
863855

@@ -1397,14 +1389,9 @@ def get_references_with_links(self):
13971389

13981390
@cached_property
13991391
def vulnerability_ids(self):
1400-
from dojo.vulnerability.queries import use_entity_reads # noqa: PLC0415
1401-
# Get vulnerability ids from database and convert to list of strings
1402-
if use_entity_reads():
1403-
# Entity store: reverse relation is ordered by FindingVulnerabilityReference.Meta.ordering.
1404-
vulnerability_ids = [ref.vulnerability.vulnerability_id for ref in self.vulnerability_references.all()]
1405-
else:
1406-
vulnerability_ids_model = self.vulnerability_id_set.all()
1407-
vulnerability_ids = [vulnerability_id.vulnerability_id for vulnerability_id in vulnerability_ids_model]
1392+
# Get vulnerability ids from database and convert to list of strings.
1393+
# Entity store: reverse relation is ordered by FindingVulnerabilityReference.Meta.ordering.
1394+
vulnerability_ids = [ref.vulnerability.vulnerability_id for ref in self.vulnerability_references.all()]
14081395

14091396
# Synchronize the cve field with the unsaved_vulnerability_ids
14101397
# We do this to be as flexible as possible to handle the fields until
@@ -1452,39 +1439,9 @@ def set_hash_code(self, dedupe_option):
14521439
deduplicationLogger.debug("Hash_code computed for finding: %s: %s", finding_id, self.hash_code)
14531440

14541441

1455-
class Vulnerability_Id(models.Model):
1456-
finding = models.ForeignKey("dojo.Finding", editable=False, on_delete=models.CASCADE)
1457-
vulnerability_id = models.TextField(max_length=50, blank=False, null=False)
1458-
# Autodetected from the id prefix (CVE, GHSA, ...); NULL when there is no non-numeric
1459-
# prefix. Denormalized/indexed so type-scoped queries (e.g. GROUP BY type) stay cheap.
1460-
vulnerability_id_type = models.CharField(max_length=20, null=True, blank=True, editable=False, db_index=True)
1461-
1462-
class Meta:
1463-
constraints = [
1464-
models.UniqueConstraint(fields=["finding", "vulnerability_id"], name="unique_finding_vulnerability_id"),
1465-
]
1466-
indexes = [
1467-
# Leading on vulnerability_id (the unique constraint's index leads on finding), for the
1468-
# vulnerability-id Explorer's GROUP BY vulnerability_id / lookups by exact id.
1469-
models.Index(fields=["vulnerability_id"], name="dojo_vuln_id_lookup_idx"),
1470-
# Global search (pro/search/): weighted tsvector FTS + trigram fuzzy match.
1471-
GinIndex(
1472-
SearchVector("vulnerability_id", weight="A", config="english"),
1473-
name="dojo_vulnerability_id_fts_gin",
1474-
),
1475-
GinIndex(fields=["vulnerability_id"], opclasses=["gin_trgm_ops"], name="dojo_vuln_id_trgm"),
1476-
]
1477-
1478-
def __str__(self):
1479-
return self.vulnerability_id
1480-
1481-
def save(self, *args, **kwargs):
1482-
# bulk_create paths set the type at construction; this covers save()/get_or_create.
1483-
self.vulnerability_id_type = resolve_vulnerability_id_type(self.vulnerability_id)
1484-
super().save(*args, **kwargs)
1485-
1486-
def get_absolute_url(self):
1487-
return reverse("view_finding", args=[str(self.finding.id)])
1442+
# The legacy Vulnerability_Id model was removed in the entity-only cutover; vulnerability ids now
1443+
# live in the Vulnerability entity + FindingVulnerabilityReference through-table (dojo/vulnerability/).
1444+
# The dojo_vulnerability_id table is dropped by migration 0287.
14881445

14891446

14901447
class Finding_CWE(models.Model):

dojo/finding/queries.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ def get_auth_filter(key): return None
1717
Endpoint_Status,
1818
Finding,
1919
Test_Import_Finding_Action,
20-
Vulnerability_Id,
2120
)
2221
from dojo.request_cache import cache_for_request_or_task
2322
from dojo.vulnerability.queries import vulnerability_id_prefetch
@@ -41,22 +40,6 @@ def get_authorized_findings_for_queryset(permission, queryset, user=None):
4140
return Finding.objects.all().order_by("id") if queryset is None else queryset
4241

4342

44-
# Cached: all parameters are hashable, no dynamic queryset filtering
45-
@cache_for_request_or_task
46-
def get_authorized_vulnerability_ids(permission, user=None):
47-
impl = get_auth_filter("finding.get_authorized_vulnerability_ids")
48-
if impl:
49-
return impl(permission, user=user)
50-
return Vulnerability_Id.objects.all()
51-
52-
53-
def get_authorized_vulnerability_ids_for_queryset(permission, queryset, user=None):
54-
impl = get_auth_filter("finding.get_authorized_vulnerability_ids_for_queryset")
55-
if impl:
56-
return impl(permission, queryset, user=user)
57-
return queryset
58-
59-
6043
def prefetch_for_findings(findings, prefetch_type="all", *, exclude_untouched=True):
6144
"""
6245
Unified prefetch function for findings across the application.

dojo/importers/base_importer.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ def __init__(
8282
and will raise a `NotImplemented` exception
8383
"""
8484
ImporterOptions.__init__(self, *args, **kwargs)
85-
# Unconditional dual-write seam: buffers legacy Vulnerability_Id rows AND the new
86-
# entity/reference rows, flushed together at the batch boundary.
85+
# Write seam: buffers the Vulnerability entity + FindingVulnerabilityReference rows,
86+
# flushed at the batch boundary.
8787
self.vulnerability_id_manager = VulnerabilityIdManager()
8888
self.pending_cwes: list[Finding_CWE] = []
8989
self.pending_cwe_deletes: list[int] = []
@@ -891,7 +891,7 @@ def store_vulnerability_ids(
891891
finding: Finding,
892892
) -> Finding:
893893
"""
894-
Accumulate Vulnerability_Id objects for bulk insert at the batch boundary.
894+
Accumulate a finding's vulnerability-id references for bulk insert at the batch boundary.
895895
Call flush_vulnerability_ids() to persist.
896896
"""
897897
self.sanitize_vulnerability_ids(finding)
@@ -926,8 +926,8 @@ def reconcile_cwes(self, finding: Finding) -> None:
926926
self.pending_cwes.extend([Finding_CWE(finding=finding, cwe=cwe) for cwe in new_cwes])
927927

928928
def flush_vulnerability_ids(self) -> None:
929-
"""Flush the dual-write vulnerability-id buffers, then the Finding_CWE buffers, and clear."""
930-
# Legacy Vulnerability_Id rows + entity/reference rows, in one transaction.
929+
"""Flush the vulnerability-id buffers, then the Finding_CWE buffers, and clear."""
930+
# Vulnerability entity + FindingVulnerabilityReference rows, in one transaction.
931931
self.vulnerability_id_manager.flush()
932932
# CWE buffers ride the same flush boundary as before (not owned by the manager).
933933
if self.pending_cwe_deletes:

dojo/management/commands/migrate_cve.py

Lines changed: 0 additions & 56 deletions
This file was deleted.

0 commit comments

Comments
 (0)