Skip to content

Commit 0eac542

Browse files
committed
Add SSVC trees, resource URL and max_advisories
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 30b3ef6 commit 0eac542

3 files changed

Lines changed: 152 additions & 19 deletions

File tree

vulnerabilities/api_v3.py

Lines changed: 116 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,19 @@
2323
from rest_framework.reverse import reverse
2424
from rest_framework.throttling import AnonRateThrottle
2525

26+
from vulnerabilities.models import SSVC
2627
from vulnerabilities.models import AdvisoryAlias
2728
from vulnerabilities.models import AdvisoryReference
2829
from vulnerabilities.models import AdvisorySet
2930
from vulnerabilities.models import AdvisorySetMember
3031
from vulnerabilities.models import AdvisorySeverity
3132
from vulnerabilities.models import AdvisoryV2
3233
from vulnerabilities.models import AdvisoryWeakness
33-
from vulnerabilities.models import Group
3434
from vulnerabilities.models import GroupedAdvisory
3535
from vulnerabilities.models import ImpactedPackageAffecting
3636
from vulnerabilities.models import PackageV2
3737
from vulnerabilities.throttling import PermissionBasedUserRateThrottle
3838
from vulnerabilities.utils import TYPES_WITH_MULTIPLE_IMPORTERS
39-
from vulnerabilities.utils import get_advisories_from_groups
4039
from vulnerabilities.utils import merge_and_save_grouped_advisories
4140

4241

@@ -48,6 +47,7 @@ class PackageQuerySerializer(serializers.Serializer):
4847
)
4948
details = serializers.BooleanField(default=False)
5049
ignore_qualifiers_subpath = serializers.BooleanField(default=False)
50+
max_advisories = serializers.IntegerField(default=100, min_value=1, max_value=10000)
5151

5252
def validate(self, data):
5353
if not data["purls"]:
@@ -228,11 +228,17 @@ def get_affected_by_vulnerabilities(self, package):
228228
for adv in advisories:
229229
fixed = impact_map.get(adv["avid"])
230230
adv.pop("avid", None)
231+
resource_url = None
232+
233+
if request := self.context.get("request", None):
234+
resource_url = adv.pop("resource_url", None)
235+
resource_url = request.build_absolute_uri(location=resource_url)
231236

232237
result.append(
233238
{
234239
**adv,
235240
"fixed_by_packages": fixed,
241+
"resource_url": resource_url,
236242
}
237243
)
238244

@@ -246,9 +252,20 @@ def get_affected_by_vulnerabilities(self, package):
246252
advisories_ids = advisories_qs.only("id")
247253

248254
advisories_ids = list(advisories_ids[:101])
249-
if len(advisories_ids) > 100:
255+
if len(advisories_ids) > self.context.get("max_advisories", 100):
250256
return None
251257

258+
advisories_qs = advisories_qs.prefetch_related(
259+
"aliases",
260+
Prefetch(
261+
"related_ssvcs",
262+
queryset=SSVC.objects.select_related("source_advisory").only(
263+
"id", "decision", "options", "vector", "source_advisory__url"
264+
),
265+
to_attr="prefetched_ssvc_trees",
266+
),
267+
)
268+
252269
advisory_by_avid = {adv.avid: adv for adv in advisories_qs}
253270
avids = advisory_by_avid.keys()
254271

@@ -264,8 +281,14 @@ def get_affected_by_vulnerabilities(self, package):
264281

265282
for advisory in advisories_qs:
266283
impact = impact_by_avid.get(advisory.avid)
267-
if not impact:
268-
continue
284+
fixed_by_packages = []
285+
if impact:
286+
fixed_by_packages = [pkg.purl for pkg in impact.fixed_by_packages.all()]
287+
288+
resource_url = None
289+
290+
if request := self.context.get("request", None):
291+
resource_url = request.build_absolute_uri(location=advisory.get_absolute_url())
269292

270293
result.append(
271294
{
@@ -275,7 +298,17 @@ def get_affected_by_vulnerabilities(self, package):
275298
"severity": advisory.weighted_severity,
276299
"exploitability": advisory.exploitability,
277300
"risk_score": advisory.risk_score,
278-
"fixed_by_packages": [pkg.purl for pkg in impact.fixed_by_packages.all()],
301+
"fixed_by_packages": fixed_by_packages,
302+
"resource_url": resource_url,
303+
"ssvc_trees": [
304+
{
305+
"vector": ssvc.vector,
306+
"decision": ssvc.decision,
307+
"options": ssvc.options,
308+
"source_url": ssvc.source_advisory.url,
309+
}
310+
for ssvc in advisory.prefetched_ssvc_trees
311+
],
279312
}
280313
)
281314

@@ -295,24 +328,37 @@ def get_affected_by_vulnerabilities(self, package):
295328

296329
def get_fixing_vulnerabilities(self, package):
297330
advisories = self.context["fixing_advisory_map"].get(package.id, [])
298-
if advisories:
299-
return advisories
331+
results = []
332+
for advisory in advisories:
333+
if request := self.context.get("request", None):
334+
resource_url = request.build_absolute_uri(location=advisory["resource_url"])
335+
results.append(
336+
{
337+
"advisory_id": advisory["advisory_id"],
338+
"resource_url": resource_url,
339+
}
340+
)
341+
if results:
342+
return results
300343

301344
advisories_qs = AdvisoryV2.objects.latest_fixed_by_advisories_for_purl(package.package_url)
302345

303346
if not package.type in TYPES_WITH_MULTIPLE_IMPORTERS:
304347
advisories_ids = advisories_qs.only("id")
305348

306349
advisories_ids = list(advisories_ids[:101])
307-
if len(advisories_ids) > 100:
350+
if len(advisories_ids) > self.context.get("max_advisories", 100):
308351
return None
309352

310353
results = []
311354

312355
for advisory in advisories_qs:
356+
if request := self.context.get("request", None):
357+
resource_url = request.build_absolute_uri(location=advisory.get_absolute_url())
313358
results.append(
314359
{
315360
"advisory_id": advisory.advisory_id.split("/")[-1],
361+
"resource_url": resource_url,
316362
}
317363
)
318364
return results
@@ -334,9 +380,15 @@ def return_fixing_advisories_data(self, advisories):
334380
result = []
335381
for advisory in advisories:
336382
assert isinstance(advisory, GroupedAdvisory)
383+
resource_url = None
384+
if request := self.context.get("request", None):
385+
resource_url = request.build_absolute_uri(
386+
location=advisory.advisory.get_absolute_url()
387+
)
337388
result.append(
338389
{
339390
"advisory_id": advisory.identifier,
391+
"resource_url": resource_url,
340392
}
341393
)
342394

@@ -357,9 +409,15 @@ def return_advisories_data(self, package, advisories_qs, advisories):
357409
result = []
358410
for advisory in advisories:
359411
assert isinstance(advisory, GroupedAdvisory)
412+
resource_url = None
413+
fixed_by_packages = []
414+
if request := self.context.get("request", None):
415+
resource_url = request.build_absolute_uri(
416+
location=advisory.advisory.get_absolute_url()
417+
)
360418
impact = impact_by_avid.get(advisory.advisory.avid)
361419
if not impact:
362-
continue
420+
fixed_by_packages = list(set([pkg.purl for pkg in impact.fixed_by_packages.all()]))
363421

364422
result.append(
365423
{
@@ -369,9 +427,9 @@ def return_advisories_data(self, package, advisories_qs, advisories):
369427
"exploitability": advisory.exploitability,
370428
"risk_score": advisory.risk_score,
371429
"summary": advisory.advisory.summary,
372-
"fixed_by_packages": list(
373-
set([pkg.purl for pkg in impact.fixed_by_packages.all()])
374-
),
430+
"fixed_by_packages": fixed_by_packages,
431+
"resource_url": resource_url,
432+
"ssvc_trees": advisory.ssvc_trees,
375433
}
376434
)
377435

@@ -400,6 +458,7 @@ def create(self, request, *args, **kwargs):
400458
purls = serializer.validated_data["purls"]
401459
details = serializer.validated_data["details"]
402460
ignore_qualifiers_subpath = serializer.validated_data["ignore_qualifiers_subpath"]
461+
max_advisories = serializer.validated_data["max_advisories"]
403462

404463
if not purls:
405464
impacted = ImpactedPackageAffecting.objects.filter(package_id=OuterRef("id"))
@@ -464,6 +523,7 @@ def create(self, request, *args, **kwargs):
464523
"advisory_map": affected_advisory_map,
465524
"impact_map": impact_map,
466525
"fixing_advisory_map": fixing_advisory_map,
526+
"max_advisories": max_advisories,
467527
},
468528
)
469529
return self.get_paginated_response(serializer.data)
@@ -576,7 +636,25 @@ def get_affected_advisories_bulk(packages):
576636
relation_type="affecting",
577637
)
578638
.select_related("primary_advisory")
579-
.prefetch_related(Prefetch("aliases", queryset=AdvisoryAlias.objects.only("alias")))
639+
.prefetch_related(
640+
Prefetch("aliases", queryset=AdvisoryAlias.objects.only("alias")),
641+
Prefetch(
642+
"members",
643+
queryset=AdvisorySetMember.objects.select_related("advisory").prefetch_related(
644+
Prefetch(
645+
"advisory__related_ssvcs",
646+
queryset=SSVC.objects.select_related("source_advisory").only(
647+
"id",
648+
"options",
649+
"decision",
650+
"vector",
651+
"source_advisory__url",
652+
),
653+
to_attr="prefetched_ssvc_trees",
654+
)
655+
),
656+
),
657+
)
580658
.annotate(
581659
max_severity=Max(
582660
"members__advisory__weighted_severity",
@@ -620,6 +698,20 @@ def get_affected_advisories_bulk(packages):
620698
identifier = primary.advisory_id.split("/")[-1]
621699

622700
aliases = [a for a in adv._aliases_cache if a != identifier]
701+
all_ssvc = []
702+
703+
for member in adv.members.all():
704+
all_ssvc.extend(member.advisory.prefetched_ssvc_trees)
705+
706+
for ssvc in all_ssvc:
707+
all_ssvc.append(
708+
{
709+
"vector": ssvc.vector,
710+
"decision": ssvc.decision,
711+
"options": ssvc.options,
712+
"source_url": ssvc.source_advisory.url,
713+
}
714+
)
623715

624716
grouped.append(
625717
{
@@ -630,6 +722,8 @@ def get_affected_advisories_bulk(packages):
630722
"exploitability": exploitability,
631723
"risk_score": risk_score,
632724
"summary": primary.summary,
725+
"resource_url": primary.get_absolute_url(),
726+
"ssvc_trees": all_ssvc,
633727
}
634728
)
635729

@@ -690,16 +784,21 @@ def get_fixing_advisories_bulk(packages):
690784
package_map = defaultdict(list)
691785

692786
for adv in advisory_sets:
693-
package_map[adv.package_id].append(adv.primary_advisory.advisory_id)
787+
package_map[adv.package_id].append(adv.primary_advisory)
694788

695789
result = {}
696790

697791
for package in packages:
698792
groups = package_map.get(package.id, [])
699793
grouped = []
700794

701-
for adv_id in groups:
702-
grouped.append({"advisory_id": adv_id.split("/")[-1]})
795+
for advisory in groups:
796+
grouped.append(
797+
{
798+
"advisory_id": advisory.advisory_id.split("/")[-1],
799+
"resource_url": advisory.get_absolute_url(),
800+
}
801+
)
703802

704803
result[package.id] = grouped
705804

vulnerabilities/models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from itertools import groupby
2020
from operator import attrgetter
2121
from traceback import format_exc as traceback_format_exc
22-
from typing import List
22+
from typing import Dict, List
2323
from typing import NamedTuple
2424
from typing import Optional
2525
from typing import Set
@@ -3817,3 +3817,4 @@ class GroupedAdvisory(NamedTuple):
38173817
weighted_severity: Optional[float]
38183818
exploitability: Optional[float]
38193819
risk_score: Optional[float]
3820+
ssvc_trees: List[Dict]

vulnerabilities/utils.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import urllib3
3636
from cwe2.database import Database
3737
from cwe2.database import InvalidCWEError
38+
from django.db.models import Prefetch
3839
from packageurl import PackageURL
3940
from packageurl.contrib.django.utils import without_empty_values
4041
from univers.version_range import RANGE_CLASS_BY_SCHEMES
@@ -959,10 +960,12 @@ def get_merged_identifier_groups(advisories):
959960
return final_groups
960961

961962

962-
def get_advisories_from_groups(groups):
963+
def get_advisories_from_groups(groups, include_ssvc_trees=False):
963964
"""
964965
Return a list of advisories from the merged groups of advisories.
965966
"""
967+
from vulnerabilities.models import SSVC
968+
from vulnerabilities.models import AdvisoryV2
966969
from vulnerabilities.models import Group
967970
from vulnerabilities.models import GroupedAdvisory
968971

@@ -996,6 +999,35 @@ def get_advisories_from_groups(groups):
996999
identifier = group.primary.advisory_id.split("/")[-1]
9971000
filtered_aliases = [alias for alias in group.aliases if alias.alias != identifier]
9981001

1002+
ssvc_trees = []
1003+
1004+
if include_ssvc_trees:
1005+
1006+
all_advs = [group.primary] + list(group.secondaries)
1007+
1008+
advisories_qs = AdvisoryV2.objects.filter(
1009+
id__in=[adv.id for adv in all_advs]
1010+
).prefetch_related(
1011+
Prefetch(
1012+
"related_ssvcs",
1013+
queryset=SSVC.objects.select_related("source_advisory")
1014+
.only("id", "vector", "decision", "options", "source_advisory__url")
1015+
.distinct(),
1016+
to_attr="ssvc_trees",
1017+
)
1018+
)
1019+
1020+
ssvc_trees = [
1021+
{
1022+
"vector": ssvc.vector,
1023+
"decision": ssvc.decision,
1024+
"options": ssvc.options,
1025+
"url": ssvc.source_advisory.url if ssvc.source_advisory else None,
1026+
}
1027+
for adv in advisories_qs
1028+
for ssvc in adv.ssvc_trees
1029+
]
1030+
9991031
advisories.append(
10001032
GroupedAdvisory(
10011033
aliases=filtered_aliases,
@@ -1004,6 +1036,7 @@ def get_advisories_from_groups(groups):
10041036
weighted_severity=weighted_severity,
10051037
exploitability=exploitability,
10061038
risk_score=risk_score,
1039+
ssvc_trees=ssvc_trees or [],
10071040
)
10081041
)
10091042

0 commit comments

Comments
 (0)