Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions dojo/api_v2/prefetch/registrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
from dojo.engagement.queries import get_authorized_engagements
from dojo.finding.queries import (
get_authorized_findings,
get_authorized_vulnerability_ids,
)
from dojo.finding_group.queries import get_authorized_finding_groups
from dojo.github.models import GITHUB_Issue, GITHUB_PKey
Expand Down Expand Up @@ -94,7 +93,6 @@
Tool_Product_Settings,
Tool_Type,
UserContactInfo,
Vulnerability_Id,
)
from dojo.notifications.models import Notification_Webhooks, Notifications
from dojo.product.queries import (
Expand All @@ -110,6 +108,14 @@
from dojo.test.queries import get_authorized_test_imports, get_authorized_tests
from dojo.tool_product.queries import get_authorized_tool_product_settings
from dojo.url.models import URL
from dojo.vulnerability.models import (
FindingVulnerabilityReference,
Vulnerability,
)
from dojo.vulnerability.queries import (
get_authorized_finding_vulnerability_references,
get_authorized_vulnerability_id_entities,
)

########
# Models backed by ViewSets (api_v2.views) from which we can derive the required permission check.
Expand Down Expand Up @@ -215,7 +221,8 @@
# Models where we can simply fall back to a `get_authorized_*` method to check auth
for model, helper in (
(Finding_Group, get_authorized_finding_groups),
(Vulnerability_Id, get_authorized_vulnerability_ids),
(Vulnerability, get_authorized_vulnerability_id_entities),
(FindingVulnerabilityReference, get_authorized_finding_vulnerability_references),
):
register(model, discard_user(helper), "view")

Expand Down
3 changes: 2 additions & 1 deletion dojo/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ def register_watson_models(app_config):
watson.register(app_config.get_model("Location"))
watson.register(app_config.get_model("Engagement"), fields=get_model_fields_with_extra(app_config.get_model("Engagement"), ("id", "product__name")), store=("product__name", ))
watson.register(app_config.get_model("App_Analysis"))
watson.register(app_config.get_model("Vulnerability_Id"), store=("finding__test__engagement__product__name", ))
# The legacy Vulnerability_Id table was retired (entity-only cutover); the classic watson-backed
# vuln-id search is gone. The Vue global search covers vulnerability ids via the entity store.

# YourModel = app_config.get_model("YourModel")
# watson.register(YourModel)
Expand Down
23 changes: 18 additions & 5 deletions dojo/authorization/query_registrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@
Test,
Test_Import,
Tool_Product_Settings,
Vulnerability_Id,
)
from dojo.request_cache import cache_for_request_or_task
from dojo.vulnerability.models import FindingVulnerabilityReference, Vulnerability


def _resolve_user(user):
Expand Down Expand Up @@ -404,18 +404,31 @@ def _get_authorized_findings(permission, queryset=None, user=None):
register_auth_filter("finding.get_authorized_findings_for_queryset", _get_authorized_findings)


def _get_authorized_vulnerability_ids(permission, queryset=None, user=None):
def _get_authorized_vulnerability_id_entities(permission, queryset=None, user=None):
user = _resolve_user(user)
qs = queryset if queryset is not None else Vulnerability_Id.objects.all()
qs = queryset if queryset is not None else Vulnerability.objects.all()
if user is None or getattr(user, "is_anonymous", False):
return qs.none()
if _is_unrestricted(user, permission_to_action(permission)):
return qs
# An entity links to findings across many products; distinct() collapses the join fan-out.
return qs.filter(
finding_references__finding__test__engagement__product__id__in=_authorized_product_ids(user),
).distinct()


def _get_authorized_finding_vulnerability_references(permission, queryset=None, user=None):
user = _resolve_user(user)
qs = queryset if queryset is not None else FindingVulnerabilityReference.objects.all()
if user is None or getattr(user, "is_anonymous", False):
return qs.none()
if _is_unrestricted(user, permission_to_action(permission)):
return qs
return qs.filter(finding__test__engagement__product__id__in=_authorized_product_ids(user))


register_auth_filter("finding.get_authorized_vulnerability_ids", _get_authorized_vulnerability_ids)
register_auth_filter("finding.get_authorized_vulnerability_ids_for_queryset", _get_authorized_vulnerability_ids)
register_auth_filter("vulnerability_id.get_authorized_entities", _get_authorized_vulnerability_id_entities)
register_auth_filter("vulnerability_id.get_authorized_references", _get_authorized_finding_vulnerability_references)


# ---------------------------------------------------------------------------
Expand Down
60 changes: 60 additions & 0 deletions dojo/db_migrations/0286_vulnerability_id_entity_tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Vulnerability ID entity tables: the normalized Vulnerability registry and the ordered
# Finding -> Vulnerability reference table. Both tables are CREATED EMPTY here, so every index
# (including the two GIN indexes) is built on zero rows — the DDL is effectively instant and needs
# no AddIndexConcurrently. The data backfill runs separately in 0287 (atomic=False, resumable).
#
# >50M-row fallback (documented, NOT the default path): on installs where the subsequent 0287
# backfill would insert tens of millions of reference rows, GIN maintenance during the backfill
# dominates. Such installs can strip the two GinIndex entries from the Vulnerability Meta here,
# let 0287 populate the tables, then add both GIN indexes with CREATE INDEX CONCURRENTLY in a
# follow-up migration. Not needed for typical installs.

import django.contrib.postgres.indexes
import django.contrib.postgres.search
import django.core.validators
import django.db.models.deletion
import django.db.models.functions.text
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("dojo", "0285_cicd_infrastructure"),
]

operations = [
migrations.CreateModel(
name="Vulnerability",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("vulnerability_id", models.TextField(unique=True, verbose_name="Vulnerability Id")),
("vulnerability_id_type", models.CharField(blank=True, db_index=True, editable=False, max_length=20, null=True)),
("epss_score", models.FloatField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])),
("epss_percentile", models.FloatField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(1.0)])),
("known_exploited", models.BooleanField(blank=True, default=None, null=True)),
("ransomware_used", models.BooleanField(blank=True, default=None, null=True)),
("kev_date", models.DateField(blank=True, default=None, null=True)),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
],
options={
"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")],
},
),
migrations.CreateModel(
name="FindingVulnerabilityReference",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("order", models.PositiveSmallIntegerField(default=0, help_text="Position in the finding's id list; 0 is the primary identifier.")),
("created", models.DateTimeField(auto_now_add=True)),
("finding", models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name="vulnerability_references", to="dojo.finding")),
("vulnerability", models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name="finding_references", to="dojo.vulnerability")),
],
options={
"ordering": ["order"],
"indexes": [models.Index(fields=["finding", "order"], name="dojo_findin_finding_d1a7a8_idx"), models.Index(fields=["vulnerability"], name="dojo_findin_vulnera_13169c_idx")],
"constraints": [models.UniqueConstraint(fields=("finding", "vulnerability"), name="unique_finding_vulnerability"), models.UniqueConstraint(fields=("finding", "order"), name="unique_finding_order")],
},
),
]
43 changes: 43 additions & 0 deletions dojo/db_migrations/0287_backfill_vulnerability_id_entities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Data-only backfill of the Vulnerability entity + FindingVulnerabilityReference tables created
# in 0286. atomic=False so each set-based statement (and each finding_id window in the reference
# build) commits on its own — a crash mid-backfill resumes where it left off, and ON CONFLICT DO
# NOTHING everywhere makes any re-run a no-op on completed work. Insert-only into the two NEW
# tables: dojo_vulnerability_id and dojo_finding are never rewritten, locked, or deleted, so
# concurrent readers/writers are not blocked.
#
# The actual work lives in dojo.vulnerability.backfill.run_backfill, shared with the
# `migrate_vulnerability_ids` management command (big installs pre-run the command on the previous
# release; this migration is then an idempotent catch-up during the deploy).
import logging

from django.db import connection, migrations

logger = logging.getLogger(__name__)


def backfill_vulnerability_id_entities(apps, schema_editor):
if connection.vendor != "postgresql":
logger.warning(
"Vulnerability ID entity backfill is PostgreSQL-only and was skipped on the %s "
"backend. Run `manage.py migrate_vulnerability_ids` for a backend-appropriate backfill.",
connection.vendor,
)
return
from dojo.vulnerability.backfill import run_backfill # noqa: PLC0415 -- lazy import

run_backfill(logger=logger)


class Migration(migrations.Migration):

"""Idempotent, resumable, insert-only backfill of the vulnerability-id entity tables."""

atomic = False

dependencies = [
("dojo", "0286_vulnerability_id_entity_tables"),
]

operations = [
migrations.RunPython(backfill_vulnerability_id_entities, migrations.RunPython.noop),
]
20 changes: 20 additions & 0 deletions dojo/db_migrations/0288_delete_vulnerability_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Entity-only cutover: drop the legacy Vulnerability_Id store. Vulnerability ids now live entirely in
# the Vulnerability entity + FindingVulnerabilityReference through-table (created in 0286, backfilled
# from dojo_vulnerability_id in 0287 — which runs BEFORE this drop). ⚠️ DESTRUCTIVE: drops
# dojo_vulnerability_id and its data. On upgrading installs, ensure 0287's backfill (or
# `manage.py migrate_vulnerability_ids` pre-run) has completed before this migration removes the source.

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("dojo", "0287_backfill_vulnerability_id_entities"),
]

operations = [
migrations.DeleteModel(
name="Vulnerability_Id",
),
]
12 changes: 3 additions & 9 deletions dojo/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@
Product,
Product_Type,
Test,
Vulnerability_Id,
)
from dojo.product_type.queries import get_authorized_product_types
from dojo.utils import get_system_setting, is_finding_groups_enabled, truncate_timezone_aware
from dojo.vulnerability.queries import finding_ids_with_vulnerability_ids

logger = logging.getLogger(__name__)

Expand All @@ -73,17 +73,11 @@ def custom_filter(queryset, name, value):

def custom_vulnerability_id_filter(queryset, name, value):
values = value.split(",")
ids = Vulnerability_Id.objects \
.filter(vulnerability_id__in=values) \
.values_list("finding_id", flat=True)
return queryset.filter(id__in=ids)
return queryset.filter(id__in=finding_ids_with_vulnerability_ids(values, lookup="in"))


def vulnerability_id_filter(queryset, name, value):
ids = Vulnerability_Id.objects \
.filter(vulnerability_id=value) \
.values_list("finding_id", flat=True)
return queryset.filter(id__in=ids)
return queryset.filter(id__in=finding_ids_with_vulnerability_ids(value, lookup="exact"))


class NumberInFilter(filters.BaseInFilter, filters.NumberFilter):
Expand Down
7 changes: 0 additions & 7 deletions dojo/finding/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
Finding,
Finding_Group,
Finding_Template,
Vulnerability_Id,
)


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


@admin.register(Vulnerability_Id)
class VulnerabilityIdAdmin(admin.ModelAdmin):

"""Admin support for the Vulnerability_Id model."""


@admin.register(Finding_Group)
class FindingGroupAdmin(admin.ModelAdmin):

Expand Down
Loading
Loading