Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
227 changes: 167 additions & 60 deletions vulnerabilities/api_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from django.contrib.postgres.aggregates import ArrayAgg
from django.core.cache import cache
from django.db.models import Count
from django.db.models import Exists
from django.db.models import F
from django.db.models import Max
Expand All @@ -34,6 +35,7 @@
from vulnerabilities.models import AdvisorySet
from vulnerabilities.models import AdvisorySetMember
from vulnerabilities.models import AdvisorySeverity
from vulnerabilities.models import AdvisoryToDoV2
from vulnerabilities.models import AdvisoryV2
from vulnerabilities.models import AdvisoryWeakness
from vulnerabilities.models import ImpactedPackageAffecting
Expand Down Expand Up @@ -126,6 +128,8 @@ class AdvisoryV3Serializer(serializers.ModelSerializer):
severities = AdvisorySeveritySerializer(many=True)
advisory_uid = serializers.CharField(source="avid", read_only=True)
related_ssvc_trees = serializers.SerializerMethodField()
todo_count = serializers.IntegerField(read_only=True)
curating_advisories = serializers.SerializerMethodField()

def get_related_ssvc_trees(self, obj):
seen = set()
Expand All @@ -150,6 +154,18 @@ def get_related_ssvc_trees(self, obj):

return result

def get_curating_advisories(self, obj):
request = self.context.get("request")
return [
reverse(
"advisory_details",
kwargs={"avid": related_advisory.avid},
request=request,
)
for todo in obj.resolves_todos.all()
for related_advisory in todo.advisories.all()
]

class Meta:
model = AdvisoryV2
fields = [
Expand All @@ -165,6 +181,9 @@ class Meta:
"weighted_severity",
"risk_score",
"related_ssvc_trees",
"todo_count",
"is_curation",
"curating_advisories",
]


Expand Down Expand Up @@ -380,6 +399,9 @@ class Meta:
"risk_score",
"related_ssvc_trees",
"fixed_by_packages",
"todo_count",
"is_curation",
"curating_advisories",
]


Expand All @@ -396,51 +418,67 @@ def create(self, request, *args, **kwargs):

purls = serializer.validated_data["purls"]

latest_advisories = AdvisoryV2.objects.latest_advisories_for_purls(
purls=purls
).prefetch_related(
Prefetch(
"references",
queryset=AdvisoryReference.objects.only(
"id",
"url",
"reference_type",
"reference_id",
latest_advisories = (
AdvisoryV2.objects.latest_advisories_for_purls(purls=purls)
.annotate(
todo_count=Count(
"advisory_todos",
filter=Q(advisory_todos__is_todo_stale=False),
)
)
.prefetch_related(
Prefetch(
"references",
queryset=AdvisoryReference.objects.only(
"id",
"url",
"reference_type",
"reference_id",
),
),
),
Prefetch(
"severities",
queryset=AdvisorySeverity.objects.only(
"id",
"url",
"value",
"scoring_system",
"scoring_elements",
"published_at",
Prefetch(
"severities",
queryset=AdvisorySeverity.objects.only(
"id",
"url",
"value",
"scoring_system",
"scoring_elements",
"published_at",
),
),
),
"weaknesses",
"aliases",
Prefetch(
"related_ssvcs",
queryset=SSVC.objects.only(
"id",
"vector",
"decision",
"options",
"source_advisory__url",
"weaknesses",
"aliases",
Prefetch(
"related_ssvcs",
queryset=SSVC.objects.only(
"id",
"vector",
"decision",
"options",
"source_advisory__url",
),
),
),
Prefetch(
"source_ssvcs",
queryset=SSVC.objects.only(
"id",
"vector",
"decision",
"options",
"source_advisory__url",
Prefetch(
"source_ssvcs",
queryset=SSVC.objects.only(
"id",
"vector",
"decision",
"options",
"source_advisory__url",
),
),
),
Prefetch(
"resolves_todos",
queryset=AdvisoryToDoV2.objects.prefetch_related(
Prefetch(
"advisories",
queryset=AdvisoryV2.objects.only("avid"),
)
),
),
)
)

page = self.paginate_queryset(latest_advisories)
Expand All @@ -459,7 +497,27 @@ def get_queryset(self):
if not purl:
return AdvisoryV2.objects.none()

return AdvisoryV2.objects.filter(**{self.relation: purl}).latest_per_avid()
return (
AdvisoryV2.objects.filter(**{self.relation: purl})
.latest_per_avid()
.annotate(
todo_count=Count(
"advisory_todos",
filter=Q(advisory_todos__is_todo_stale=False),
)
)
.prefetch_related(
Prefetch(
"resolves_todos",
queryset=AdvisoryToDoV2.objects.prefetch_related(
Prefetch(
"advisories",
queryset=AdvisoryV2.objects.only("avid"),
)
),
),
)
)


class FixingAdvisoriesViewSet(PackageAdvisoriesViewSet):
Expand Down Expand Up @@ -619,6 +677,15 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit
"aliases",
queryset=AdvisoryAlias.objects.only("alias"),
),
Prefetch(
"primary_advisory__resolves_todos",
queryset=AdvisoryToDoV2.objects.prefetch_related(
Prefetch(
"advisories",
queryset=AdvisoryV2.objects.only("avid"),
)
),
),
)
.annotate(
max_severity=Max(
Expand All @@ -627,13 +694,18 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit
max_exploitability=Max(
"members__advisory__exploitability",
),
primary_adv_todo_count=Count(
"primary_advisory__advisory_todos",
filter=Q(primary_advisory__advisory_todos__is_todo_stale=False),
),
)
.only(
"id",
"package_id",
"primary_advisory__avid",
"primary_advisory__summary",
"primary_advisory__advisory_id",
"primary_advisory__is_curation",
)
)

Expand Down Expand Up @@ -746,6 +818,12 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit

aliases = [a for a in adv._aliases_cache if a != identifier]

curating_advisories = [
f"{base_url}{related_advisory.get_absolute_url()}"
for todo in primary.resolves_todos.all()
for related_advisory in todo.advisories.all()
]

resource_url = None
advisory_url = primary.get_absolute_url()

Expand All @@ -772,6 +850,9 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit
),
"ssvc_trees": adv.ssvc_trees,
"resource_url": resource_url,
"todo_count": adv.primary_adv_todo_count,
"is_curation": primary.is_curation,
"curating_advisories": curating_advisories,
}
)

Expand Down Expand Up @@ -809,25 +890,43 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit
if not allowed_package_ids:
return result

advisories = AdvisoryV2.objects.filter(
id__in=allowed_advisory_ids,
).prefetch_related(
"aliases",
Prefetch(
"related_ssvcs",
queryset=(
SSVC.objects.select_related("source_advisory")
.only(
"id",
"decision",
"options",
"vector",
"source_advisory__url",
)
.distinct("source_advisory__url")
advisories = (
AdvisoryV2.objects.filter(
id__in=allowed_advisory_ids,
)
.annotate(
todo_count=Count(
"advisory_todos",
filter=Q(advisory_todos__is_todo_stale=False),
)
)
.prefetch_related(
"aliases",
Prefetch(
"related_ssvcs",
queryset=(
SSVC.objects.select_related("source_advisory")
.only(
"id",
"decision",
"options",
"vector",
"source_advisory__url",
)
.distinct("source_advisory__url")
),
to_attr="prefetched_ssvc_trees",
),
to_attr="prefetched_ssvc_trees",
),
Prefetch(
"resolves_todos",
queryset=AdvisoryToDoV2.objects.prefetch_related(
Prefetch(
"advisories",
queryset=AdvisoryV2.objects.only("avid"),
)
),
),
)
)

advisory_by_id = {advisory.id: advisory for advisory in advisories}
Expand All @@ -851,6 +950,11 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit
identifier = advisory.advisory_id.split("/")[-1]

aliases = [alias.alias for alias in advisory.aliases.all() if alias.alias != identifier]
curating_advisories = [
f"{base_url}{related_advisory.get_absolute_url()}"
for todo in advisory.resolves_todos.all()
for related_advisory in todo.advisories.all()
]

resource_url = None
advisory_url = advisory.get_absolute_url()
Expand Down Expand Up @@ -880,6 +984,9 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit
for ssvc in advisory.prefetched_ssvc_trees
],
"resource_url": resource_url,
"todo_count": advisory.todo_count,
"is_curation": advisory.is_curation,
"curating_advisories": curating_advisories,
}
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 5.2.11 on 2026-08-06 17:21

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("vulnerabilities", "0141_advisorymitigations"),
]

operations = [
migrations.AddField(
model_name="advisoryv2",
name="is_curation",
field=models.BooleanField(
db_index=True,
default=False,
help_text="Indicates whether this is a curation advisory.",
),
),
migrations.AddField(
model_name="advisoryv2",
name="resolves_todos",
field=models.ManyToManyField(
help_text="A list of Advisory ToDos resolved by this advisory.",
related_name="resolved_in_advisories",
to="vulnerabilities.advisorytodov2",
),
),
]
14 changes: 14 additions & 0 deletions vulnerabilities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3213,6 +3213,20 @@ class AdvisoryV2(models.Model):
help_text="A list of patches associated with this advisory.",
)

resolves_todos = models.ManyToManyField(
AdvisoryToDoV2,
related_name="resolved_in_advisories",
help_text="A list of Advisory ToDos resolved by this advisory.",
)

is_curation = models.BooleanField(
default=False,
blank=False,
null=False,
db_index=True,
help_text="Indicates whether this is a curation advisory.",
)

date_published = models.DateTimeField(
blank=True,
null=True,
Expand Down
Loading