Skip to content

Commit 2e586e2

Browse files
Maffoochclaude
andcommitted
feat(vuln-id): rename to Vulnerability, fix cve-only + reimporter bugs, add cutover-safety tests
Review response (#15331) plus prep for the one-way legacy retirement. Rename (per review): VulnerabilityId -> Vulnerability; module dojo/vulnerability_id/ -> dojo/vulnerability/; table dojo_vulnerabilityid -> dojo_vulnerability (migration 0285 amended in place). String column stays vulnerability_id; reference FK stays `vulnerability`. Added vocabulary + porting-trap docstrings. Must-fixes: - MF4: UniqueConstraint(finding, order) on FindingVulnerabilityReference (model + migration 0285). - MF2: verify_vulnerability_ids downgrades order-0 != cve to a non-fatal warning when cve is not among the finding's ids (unreachable without breaking hash parity); stays a hard gate for genuine ordering drift. Bug fixes surfaced by the new tests: - cve-only hash-parity break: backfill step 4 fabricated a cve-only reference the live dual-write never creates, so entity reads diverged from legacy (dedup churn on deploy). Removed; the reference set now mirrors the legacy row set exactly. cve stays an orphan registry entity. - reimporter N+1: reconcile read the non-prefetched legacy relation under flag-on. Now reads through the seam (matches the prefetch). Perf re-baselined: -5 queries per reimport across 24 assertions. Reader ports toward the cutover (route through the flag seam, reversible): Finding.copy, reimporter reconcile. Tests (fixture-free, both flag states): legacy-independence (poison), dedup differential, real-scanner corpus (8 parsers), enumerable shapes (cve-only/null-cve/unicode/quotes/copy), MF2 warning, MF4 constraint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b2e4c2b commit 2e586e2

31 files changed

Lines changed: 718 additions & 249 deletions

dojo/api_v2/prefetch/registrations.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,11 @@
110110
from dojo.test.queries import get_authorized_test_imports, get_authorized_tests
111111
from dojo.tool_product.queries import get_authorized_tool_product_settings
112112
from dojo.url.models import URL
113-
from dojo.vulnerability_id.models import (
113+
from dojo.vulnerability.models import (
114114
FindingVulnerabilityReference,
115-
VulnerabilityId,
115+
Vulnerability,
116116
)
117-
from dojo.vulnerability_id.queries import (
117+
from dojo.vulnerability.queries import (
118118
get_authorized_finding_vulnerability_references,
119119
get_authorized_vulnerability_id_entities,
120120
)
@@ -224,7 +224,7 @@
224224
for model, helper in (
225225
(Finding_Group, get_authorized_finding_groups),
226226
(Vulnerability_Id, get_authorized_vulnerability_ids),
227-
(VulnerabilityId, get_authorized_vulnerability_id_entities),
227+
(Vulnerability, get_authorized_vulnerability_id_entities),
228228
(FindingVulnerabilityReference, get_authorized_finding_vulnerability_references),
229229
):
230230
register(model, discard_user(helper), "view")

dojo/authorization/query_registrations.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
Vulnerability_Id,
4242
)
4343
from dojo.request_cache import cache_for_request_or_task
44-
from dojo.vulnerability_id.models import FindingVulnerabilityReference, VulnerabilityId
44+
from dojo.vulnerability.models import FindingVulnerabilityReference, Vulnerability
4545

4646

4747
def _resolve_user(user):
@@ -421,7 +421,7 @@ def _get_authorized_vulnerability_ids(permission, queryset=None, user=None):
421421

422422
def _get_authorized_vulnerability_id_entities(permission, queryset=None, user=None):
423423
user = _resolve_user(user)
424-
qs = queryset if queryset is not None else VulnerabilityId.objects.all()
424+
qs = queryset if queryset is not None else Vulnerability.objects.all()
425425
if user is None or getattr(user, "is_anonymous", False):
426426
return qs.none()
427427
if _is_unrestricted(user, permission_to_action(permission)):
Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
# Vulnerability ID entity tables: the normalized VulnerabilityId registry and the ordered
2-
# Finding -> VulnerabilityId reference table. Both tables are CREATED EMPTY here, so every index
1+
# Vulnerability ID entity tables: the normalized Vulnerability registry and the ordered
2+
# Finding -> Vulnerability reference table. Both tables are CREATED EMPTY here, so every index
33
# (including the two GIN indexes) is built on zero rows — the DDL is effectively instant and needs
44
# no AddIndexConcurrently. The data backfill runs separately in 0286 (atomic=False, resumable).
55
#
66
# >50M-row fallback (documented, NOT the default path): on installs where the subsequent 0286
77
# backfill would insert tens of millions of reference rows, GIN maintenance during the backfill
8-
# dominates. Such installs can strip the two GinIndex entries from the VulnerabilityId Meta here,
8+
# dominates. Such installs can strip the two GinIndex entries from the Vulnerability Meta here,
99
# let 0286 populate the tables, then add both GIN indexes with CREATE INDEX CONCURRENTLY in a
1010
# follow-up migration. Not needed for typical installs.
1111

@@ -20,41 +20,41 @@
2020
class Migration(migrations.Migration):
2121

2222
dependencies = [
23-
('dojo', '0284_backfill_finding_cwe'),
23+
("dojo", "0284_backfill_finding_cwe"),
2424
]
2525

2626
operations = [
2727
migrations.CreateModel(
28-
name='VulnerabilityId',
28+
name="Vulnerability",
2929
fields=[
30-
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
31-
('vulnerability_id', models.TextField(unique=True, verbose_name='Vulnerability Id')),
32-
('vulnerability_id_type', models.CharField(blank=True, db_index=True, editable=False, max_length=20, null=True)),
33-
('epss_score', models.FloatField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])),
34-
('epss_percentile', models.FloatField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])),
35-
('known_exploited', models.BooleanField(blank=True, default=None, null=True)),
36-
('ransomware_used', models.BooleanField(blank=True, default=None, null=True)),
37-
('kev_date', models.DateField(blank=True, default=None, null=True)),
38-
('created', models.DateTimeField(auto_now_add=True)),
39-
('updated', models.DateTimeField(auto_now=True)),
30+
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
31+
("vulnerability_id", models.TextField(unique=True, verbose_name="Vulnerability Id")),
32+
("vulnerability_id_type", models.CharField(blank=True, db_index=True, editable=False, max_length=20, null=True)),
33+
("epss_score", models.FloatField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])),
34+
("epss_percentile", models.FloatField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])),
35+
("known_exploited", models.BooleanField(blank=True, default=None, null=True)),
36+
("ransomware_used", models.BooleanField(blank=True, default=None, null=True)),
37+
("kev_date", models.DateField(blank=True, default=None, null=True)),
38+
("created", models.DateTimeField(auto_now_add=True)),
39+
("updated", models.DateTimeField(auto_now=True)),
4040
],
4141
options={
42-
'indexes': [django.contrib.postgres.indexes.GinIndex(django.contrib.postgres.search.SearchVector('vulnerability_id', config='english', weight='A'), name='dojo_vulnid_entity_fts_gin'), django.contrib.postgres.indexes.GinIndex(fields=['vulnerability_id'], name='dojo_vulnid_entity_trgm', opclasses=['gin_trgm_ops']), models.Index(django.db.models.functions.text.Upper('vulnerability_id'), name='dojo_vulnid_entity_upper')],
42+
"indexes": [django.contrib.postgres.indexes.GinIndex(django.contrib.postgres.search.SearchVector("vulnerability_id", config="english", weight="A"), name="dojo_vulnid_entity_fts_gin"), django.contrib.postgres.indexes.GinIndex(fields=["vulnerability_id"], name="dojo_vulnid_entity_trgm", opclasses=["gin_trgm_ops"]), models.Index(django.db.models.functions.text.Upper("vulnerability_id"), name="dojo_vulnid_entity_upper")],
4343
},
4444
),
4545
migrations.CreateModel(
46-
name='FindingVulnerabilityReference',
46+
name="FindingVulnerabilityReference",
4747
fields=[
48-
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
49-
('order', models.PositiveSmallIntegerField(default=0, help_text="Position in the finding's id list; 0 is the primary identifier.")),
50-
('created', models.DateTimeField(auto_now_add=True)),
51-
('finding', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='vulnerability_references', to='dojo.finding')),
52-
('vulnerability', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='finding_references', to='dojo.vulnerabilityid')),
48+
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
49+
("order", models.PositiveSmallIntegerField(default=0, help_text="Position in the finding's id list; 0 is the primary identifier.")),
50+
("created", models.DateTimeField(auto_now_add=True)),
51+
("finding", models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name="vulnerability_references", to="dojo.finding")),
52+
("vulnerability", models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name="finding_references", to="dojo.vulnerability")),
5353
],
5454
options={
55-
'ordering': ['order'],
56-
'indexes': [models.Index(fields=['finding', 'order'], name='dojo_findin_finding_d1a7a8_idx'), models.Index(fields=['vulnerability'], name='dojo_findin_vulnera_13169c_idx')],
57-
'constraints': [models.UniqueConstraint(fields=('finding', 'vulnerability'), name='unique_finding_vulnerability')],
55+
"ordering": ["order"],
56+
"indexes": [models.Index(fields=["finding", "order"], name="dojo_findin_finding_d1a7a8_idx"), models.Index(fields=["vulnerability"], name="dojo_findin_vulnera_13169c_idx")],
57+
"constraints": [models.UniqueConstraint(fields=("finding", "vulnerability"), name="unique_finding_vulnerability"), models.UniqueConstraint(fields=("finding", "order"), name="unique_finding_order")],
5858
},
5959
),
6060
]

dojo/db_migrations/0286_backfill_vulnerability_id_entities.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
# Data-only backfill of the VulnerabilityId entity + FindingVulnerabilityReference tables created
1+
# Data-only backfill of the Vulnerability entity + FindingVulnerabilityReference tables created
22
# in 0285. atomic=False so each set-based statement (and each finding_id window in the reference
33
# build) commits on its own — a crash mid-backfill resumes where it left off, and ON CONFLICT DO
44
# NOTHING everywhere makes any re-run a no-op on completed work. Insert-only into the two NEW
55
# tables: dojo_vulnerability_id and dojo_finding are never rewritten, locked, or deleted, so
66
# concurrent readers/writers are not blocked.
77
#
8-
# The actual work lives in dojo.vulnerability_id.backfill.run_backfill, shared with the
8+
# The actual work lives in dojo.vulnerability.backfill.run_backfill, shared with the
99
# `migrate_vulnerability_ids` management command (big installs pre-run the command on the previous
1010
# release; this migration is then an idempotent catch-up during the deploy).
1111
import logging
@@ -23,7 +23,8 @@ def backfill_vulnerability_id_entities(apps, schema_editor):
2323
connection.vendor,
2424
)
2525
return
26-
from dojo.vulnerability_id.backfill import run_backfill # noqa: PLC0415 -- keep entity import lazy/out of migration graph build
26+
from dojo.vulnerability.backfill import run_backfill # noqa: PLC0415 -- keep entity import lazy/out of migration graph build
27+
2728
run_backfill(logger=logger)
2829

2930

dojo/filters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
)
5656
from dojo.product_type.queries import get_authorized_product_types
5757
from dojo.utils import get_system_setting, is_finding_groups_enabled, truncate_timezone_aware
58-
from dojo.vulnerability_id.queries import finding_ids_with_vulnerability_ids
58+
from dojo.vulnerability.queries import finding_ids_with_vulnerability_ids
5959

6060
logger = logging.getLogger(__name__)
6161

dojo/finding/api/serializer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ def get_attribute(self, instance):
323323
return instance
324324

325325
def to_representation(self, finding):
326-
from dojo.vulnerability_id.queries import finding_vulnerability_id_strings # noqa: PLC0415 -- flag seam
326+
from dojo.vulnerability.queries import finding_vulnerability_id_strings # noqa: PLC0415 -- flag seam
327327
return [{"vulnerability_id": value} for value in finding_vulnerability_id_strings(finding)]
328328

329329
def to_internal_value(self, data):

dojo/finding/deduplication.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from dojo.celery import app
1111
from dojo.models import Endpoint_Status, Finding, System_Settings
12-
from dojo.vulnerability_id.queries import vulnerability_id_prefetch
12+
from dojo.vulnerability.queries import vulnerability_id_prefetch
1313

1414
logger = logging.getLogger(__name__)
1515
deduplicationLogger = logging.getLogger("dojo.specific-loggers.deduplication")

dojo/finding/helper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
get_object_or_none,
5656
to_str_typed,
5757
)
58-
from dojo.vulnerability_id.manager import persist_for_finding
58+
from dojo.vulnerability.manager import persist_for_finding
5959

6060
logger = logging.getLogger(__name__)
6161
deduplicationLogger = logging.getLogger("dojo.specific-loggers.deduplication")

dojo/finding/models.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -707,9 +707,13 @@ def copy(self, test=None):
707707
copy.tags.set(old_tags)
708708
# Copy the vulnerability ids and CWEs (relation rows aren't copied by copy_model_util).
709709
# Route vulnerability ids through the dual-write seam so the copy gets legacy rows AND
710-
# entity references (copy.cve was already carried over by copy_model_util).
711-
from dojo.vulnerability_id.manager import persist_for_finding # noqa: PLC0415 -- avoid import cycle
712-
vulnerability_id_strings = [row.vulnerability_id for row in self.vulnerability_id_set.all()]
710+
# entity references (copy.cve was already carried over by copy_model_util). Read the SOURCE
711+
# ids through the flag seam too (not the legacy relation directly), so copy() keeps working
712+
# after the legacy Vulnerability_Id store is retired.
713+
from dojo.vulnerability.manager import persist_for_finding # noqa: PLC0415 -- avoid import cycle
714+
from dojo.vulnerability.queries import finding_vulnerability_id_strings # noqa: PLC0415
715+
716+
vulnerability_id_strings = finding_vulnerability_id_strings(self)
713717
if vulnerability_id_strings:
714718
persist_for_finding(copy, vulnerability_id_strings, delete_existing=False)
715719
for finding_cwe in self.finding_cwe_set.all():
@@ -842,7 +846,7 @@ def _get_unsaved_vulnerability_ids(finding) -> str:
842846

843847
def _get_saved_vulnerability_ids(finding) -> str:
844848
if finding.id is not None:
845-
from dojo.vulnerability_id.queries import use_entity_reads # noqa: PLC0415
849+
from dojo.vulnerability.queries import use_entity_reads # noqa: PLC0415
846850
if use_entity_reads():
847851
# Entity store: the prefetch-honoring reverse relation (vulnerability_references
848852
# + select_related("vulnerability")) so Prefetch is honored — no N+1 in dedupe.
@@ -1393,7 +1397,7 @@ def get_references_with_links(self):
13931397

13941398
@cached_property
13951399
def vulnerability_ids(self):
1396-
from dojo.vulnerability_id.queries import use_entity_reads # noqa: PLC0415
1400+
from dojo.vulnerability.queries import use_entity_reads # noqa: PLC0415
13971401
# Get vulnerability ids from database and convert to list of strings
13981402
if use_entity_reads():
13991403
# Entity store: reverse relation is ordered by FindingVulnerabilityReference.Meta.ordering.

dojo/finding/queries.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def get_auth_filter(key): return None
2020
Vulnerability_Id,
2121
)
2222
from dojo.request_cache import cache_for_request_or_task
23-
from dojo.vulnerability_id.queries import vulnerability_id_prefetch
23+
from dojo.vulnerability.queries import vulnerability_id_prefetch
2424

2525
logger = logging.getLogger(__name__)
2626

0 commit comments

Comments
 (0)