diff --git a/dojo/db_migrations/0278_vulnerability_id_upper_index.py b/dojo/db_migrations/0278_vulnerability_id_upper_index.py new file mode 100644 index 00000000000..e029139593e --- /dev/null +++ b/dojo/db_migrations/0278_vulnerability_id_upper_index.py @@ -0,0 +1,43 @@ +"""Add a case-insensitive expression index on Vulnerability_Id.vulnerability_id. + +Enables fast UPPER(vulnerability_id) → finding lookups for CVE-keyed enrichment/threat +intel. Built CONCURRENTLY (non-atomic migration) so it does not lock dojo_vulnerability_id +on large instances. CREATE ... IF NOT EXISTS makes it idempotent and safe to co-exist with +any downstream (e.g. Pro) migration that created the same-named index first. +""" +from django.db import migrations, models +from django.db.models.functions import Upper + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("dojo", "0277_seed_deduplication_execution_mode"), + ] + + operations = [ + # RunSQL (IF NOT EXISTS) does the idempotent DB work; state_operations keeps Django's + # model state in sync with the Meta index so future makemigrations is a no-op. + migrations.SeparateDatabaseAndState( + database_operations=[ + migrations.RunSQL( + sql=( + "CREATE INDEX CONCURRENTLY IF NOT EXISTS dojo_vulnid_upper_idx " + "ON dojo_vulnerability_id (UPPER(vulnerability_id), finding_id)" + ), + reverse_sql="DROP INDEX CONCURRENTLY IF EXISTS dojo_vulnid_upper_idx", + ), + ], + state_operations=[ + migrations.AddIndex( + model_name="vulnerability_id", + index=models.Index( + Upper("vulnerability_id"), + "finding", + name="dojo_vulnid_upper_idx", + ), + ), + ], + ), + ] diff --git a/dojo/finding/models.py b/dojo/finding/models.py index 8a55d3df9e6..053ed5f1b98 100644 --- a/dojo/finding/models.py +++ b/dojo/finding/models.py @@ -12,6 +12,7 @@ from django.conf import settings from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models +from django.db.models.functions import Upper from django.urls import reverse from django.utils import timezone from django.utils.functional import cached_property @@ -1367,6 +1368,19 @@ class Vulnerability_Id(models.Model): finding = models.ForeignKey("dojo.Finding", editable=False, on_delete=models.CASCADE) vulnerability_id = models.TextField(max_length=50, blank=False, null=False) + class Meta: + indexes = [ + # Case-insensitive vulnerability-id → finding lookups (e.g. threat-intel / + # exploit-enrichment keyed by CVE). Scanner-provided ids are not case-normalized, + # so the index and any query must both fold with UPPER(). finding_id is included + # so the changed-key → finding fan-out is an index-only scan. + models.Index( + Upper("vulnerability_id"), + "finding", + name="dojo_vulnid_upper_idx", + ), + ] + def __str__(self): return self.vulnerability_id