Skip to content

Commit db75b3f

Browse files
authored
Merge pull request #2406 from aboutcode-org/fedcode-next-curation-api
Expose advisory todo count and curating advisories in API response
2 parents 0661b98 + e42ace0 commit db75b3f

7 files changed

Lines changed: 503 additions & 68 deletions

File tree

docs/source/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
r"https://nixos\.wiki/", # NixOS wiki blocks CI bots with 403
4444
"https://usn.ubuntu.com/usn-db/database-all.json.bz2",
4545
"https://public.vulnerablecode.io/vulnerabilities/search/",
46+
"https://sourceware.org/git/glibc.git",
4647
]
4748

4849
# Add any Sphinx extension module names here, as strings. They can be

vulnerabilities/api_v3.py

Lines changed: 167 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from django.contrib.postgres.aggregates import ArrayAgg
1414
from django.core.cache import cache
15+
from django.db.models import Count
1516
from django.db.models import Exists
1617
from django.db.models import F
1718
from django.db.models import Max
@@ -34,6 +35,7 @@
3435
from vulnerabilities.models import AdvisorySet
3536
from vulnerabilities.models import AdvisorySetMember
3637
from vulnerabilities.models import AdvisorySeverity
38+
from vulnerabilities.models import AdvisoryToDoV2
3739
from vulnerabilities.models import AdvisoryV2
3840
from vulnerabilities.models import AdvisoryWeakness
3941
from vulnerabilities.models import ImpactedPackageAffecting
@@ -126,6 +128,8 @@ class AdvisoryV3Serializer(serializers.ModelSerializer):
126128
severities = AdvisorySeveritySerializer(many=True)
127129
advisory_uid = serializers.CharField(source="avid", read_only=True)
128130
related_ssvc_trees = serializers.SerializerMethodField()
131+
todo_count = serializers.IntegerField(read_only=True)
132+
curating_advisories = serializers.SerializerMethodField()
129133

130134
def get_related_ssvc_trees(self, obj):
131135
seen = set()
@@ -150,6 +154,18 @@ def get_related_ssvc_trees(self, obj):
150154

151155
return result
152156

157+
def get_curating_advisories(self, obj):
158+
request = self.context.get("request")
159+
return [
160+
reverse(
161+
"advisory_details",
162+
kwargs={"avid": related_advisory.avid},
163+
request=request,
164+
)
165+
for todo in obj.resolves_todos.all()
166+
for related_advisory in todo.advisories.all()
167+
]
168+
153169
class Meta:
154170
model = AdvisoryV2
155171
fields = [
@@ -165,6 +181,9 @@ class Meta:
165181
"weighted_severity",
166182
"risk_score",
167183
"related_ssvc_trees",
184+
"todo_count",
185+
"is_curation",
186+
"curating_advisories",
168187
]
169188

170189

@@ -380,6 +399,9 @@ class Meta:
380399
"risk_score",
381400
"related_ssvc_trees",
382401
"fixed_by_packages",
402+
"todo_count",
403+
"is_curation",
404+
"curating_advisories",
383405
]
384406

385407

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

397419
purls = serializer.validated_data["purls"]
398420

399-
latest_advisories = AdvisoryV2.objects.latest_advisories_for_purls(
400-
purls=purls
401-
).prefetch_related(
402-
Prefetch(
403-
"references",
404-
queryset=AdvisoryReference.objects.only(
405-
"id",
406-
"url",
407-
"reference_type",
408-
"reference_id",
421+
latest_advisories = (
422+
AdvisoryV2.objects.latest_advisories_for_purls(purls=purls)
423+
.annotate(
424+
todo_count=Count(
425+
"advisory_todos",
426+
filter=Q(advisory_todos__is_todo_stale=False),
427+
)
428+
)
429+
.prefetch_related(
430+
Prefetch(
431+
"references",
432+
queryset=AdvisoryReference.objects.only(
433+
"id",
434+
"url",
435+
"reference_type",
436+
"reference_id",
437+
),
409438
),
410-
),
411-
Prefetch(
412-
"severities",
413-
queryset=AdvisorySeverity.objects.only(
414-
"id",
415-
"url",
416-
"value",
417-
"scoring_system",
418-
"scoring_elements",
419-
"published_at",
439+
Prefetch(
440+
"severities",
441+
queryset=AdvisorySeverity.objects.only(
442+
"id",
443+
"url",
444+
"value",
445+
"scoring_system",
446+
"scoring_elements",
447+
"published_at",
448+
),
420449
),
421-
),
422-
"weaknesses",
423-
"aliases",
424-
Prefetch(
425-
"related_ssvcs",
426-
queryset=SSVC.objects.only(
427-
"id",
428-
"vector",
429-
"decision",
430-
"options",
431-
"source_advisory__url",
450+
"weaknesses",
451+
"aliases",
452+
Prefetch(
453+
"related_ssvcs",
454+
queryset=SSVC.objects.only(
455+
"id",
456+
"vector",
457+
"decision",
458+
"options",
459+
"source_advisory__url",
460+
),
432461
),
433-
),
434-
Prefetch(
435-
"source_ssvcs",
436-
queryset=SSVC.objects.only(
437-
"id",
438-
"vector",
439-
"decision",
440-
"options",
441-
"source_advisory__url",
462+
Prefetch(
463+
"source_ssvcs",
464+
queryset=SSVC.objects.only(
465+
"id",
466+
"vector",
467+
"decision",
468+
"options",
469+
"source_advisory__url",
470+
),
442471
),
443-
),
472+
Prefetch(
473+
"resolves_todos",
474+
queryset=AdvisoryToDoV2.objects.prefetch_related(
475+
Prefetch(
476+
"advisories",
477+
queryset=AdvisoryV2.objects.only("avid"),
478+
)
479+
),
480+
),
481+
)
444482
)
445483

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

462-
return AdvisoryV2.objects.filter(**{self.relation: purl}).latest_per_avid()
500+
return (
501+
AdvisoryV2.objects.filter(**{self.relation: purl})
502+
.latest_per_avid()
503+
.annotate(
504+
todo_count=Count(
505+
"advisory_todos",
506+
filter=Q(advisory_todos__is_todo_stale=False),
507+
)
508+
)
509+
.prefetch_related(
510+
Prefetch(
511+
"resolves_todos",
512+
queryset=AdvisoryToDoV2.objects.prefetch_related(
513+
Prefetch(
514+
"advisories",
515+
queryset=AdvisoryV2.objects.only("avid"),
516+
)
517+
),
518+
),
519+
)
520+
)
463521

464522

465523
class FixingAdvisoriesViewSet(PackageAdvisoriesViewSet):
@@ -619,6 +677,15 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit
619677
"aliases",
620678
queryset=AdvisoryAlias.objects.only("alias"),
621679
),
680+
Prefetch(
681+
"primary_advisory__resolves_todos",
682+
queryset=AdvisoryToDoV2.objects.prefetch_related(
683+
Prefetch(
684+
"advisories",
685+
queryset=AdvisoryV2.objects.only("avid"),
686+
)
687+
),
688+
),
622689
)
623690
.annotate(
624691
max_severity=Max(
@@ -627,13 +694,18 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit
627694
max_exploitability=Max(
628695
"members__advisory__exploitability",
629696
),
697+
primary_adv_todo_count=Count(
698+
"primary_advisory__advisory_todos",
699+
filter=Q(primary_advisory__advisory_todos__is_todo_stale=False),
700+
),
630701
)
631702
.only(
632703
"id",
633704
"package_id",
634705
"primary_advisory__avid",
635706
"primary_advisory__summary",
636707
"primary_advisory__advisory_id",
708+
"primary_advisory__is_curation",
637709
)
638710
)
639711

@@ -746,6 +818,12 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit
746818

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

821+
curating_advisories = [
822+
f"{base_url}{related_advisory.get_absolute_url()}"
823+
for todo in primary.resolves_todos.all()
824+
for related_advisory in todo.advisories.all()
825+
]
826+
749827
resource_url = None
750828
advisory_url = primary.get_absolute_url()
751829

@@ -772,6 +850,9 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit
772850
),
773851
"ssvc_trees": adv.ssvc_trees,
774852
"resource_url": resource_url,
853+
"todo_count": adv.primary_adv_todo_count,
854+
"is_curation": primary.is_curation,
855+
"curating_advisories": curating_advisories,
775856
}
776857
)
777858

@@ -809,25 +890,43 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit
809890
if not allowed_package_ids:
810891
return result
811892

812-
advisories = AdvisoryV2.objects.filter(
813-
id__in=allowed_advisory_ids,
814-
).prefetch_related(
815-
"aliases",
816-
Prefetch(
817-
"related_ssvcs",
818-
queryset=(
819-
SSVC.objects.select_related("source_advisory")
820-
.only(
821-
"id",
822-
"decision",
823-
"options",
824-
"vector",
825-
"source_advisory__url",
826-
)
827-
.distinct("source_advisory__url")
893+
advisories = (
894+
AdvisoryV2.objects.filter(
895+
id__in=allowed_advisory_ids,
896+
)
897+
.annotate(
898+
todo_count=Count(
899+
"advisory_todos",
900+
filter=Q(advisory_todos__is_todo_stale=False),
901+
)
902+
)
903+
.prefetch_related(
904+
"aliases",
905+
Prefetch(
906+
"related_ssvcs",
907+
queryset=(
908+
SSVC.objects.select_related("source_advisory")
909+
.only(
910+
"id",
911+
"decision",
912+
"options",
913+
"vector",
914+
"source_advisory__url",
915+
)
916+
.distinct("source_advisory__url")
917+
),
918+
to_attr="prefetched_ssvc_trees",
828919
),
829-
to_attr="prefetched_ssvc_trees",
830-
),
920+
Prefetch(
921+
"resolves_todos",
922+
queryset=AdvisoryToDoV2.objects.prefetch_related(
923+
Prefetch(
924+
"advisories",
925+
queryset=AdvisoryV2.objects.only("avid"),
926+
)
927+
),
928+
),
929+
)
831930
)
832931

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

853952
aliases = [alias.alias for alias in advisory.aliases.all() if alias.alias != identifier]
953+
curating_advisories = [
954+
f"{base_url}{related_advisory.get_absolute_url()}"
955+
for todo in advisory.resolves_todos.all()
956+
for related_advisory in todo.advisories.all()
957+
]
854958

855959
resource_url = None
856960
advisory_url = advisory.get_absolute_url()
@@ -880,6 +984,9 @@ def get_affected_advisories_bulk(packages, max_advisories, base_url, reachabilit
880984
for ssvc in advisory.prefetched_ssvc_trees
881985
],
882986
"resource_url": resource_url,
987+
"todo_count": advisory.todo_count,
988+
"is_curation": advisory.is_curation,
989+
"curating_advisories": curating_advisories,
883990
}
884991
)
885992

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Generated by Django 5.2.11 on 2026-08-06 17:21
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("vulnerabilities", "0141_advisorymitigations"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="advisoryv2",
15+
name="is_curation",
16+
field=models.BooleanField(
17+
db_index=True,
18+
default=False,
19+
help_text="Indicates whether this is a curation advisory.",
20+
),
21+
),
22+
migrations.AddField(
23+
model_name="advisoryv2",
24+
name="resolves_todos",
25+
field=models.ManyToManyField(
26+
help_text="A list of Advisory ToDos resolved by this advisory.",
27+
related_name="resolved_in_advisories",
28+
to="vulnerabilities.advisorytodov2",
29+
),
30+
),
31+
]

0 commit comments

Comments
 (0)