Skip to content

Commit f59c867

Browse files
committed
Fix grouping
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 50ff81c commit f59c867

File tree

7 files changed

+127
-62
lines changed

7 files changed

+127
-62
lines changed

vulnerabilities/api_v3.py

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10+
from typing import List
1011
from urllib.parse import urlencode
1112

1213
from django.db.models import Exists
@@ -25,6 +26,8 @@
2526
from vulnerabilities.models import AdvisorySeverity
2627
from vulnerabilities.models import AdvisoryV2
2728
from vulnerabilities.models import AdvisoryWeakness
29+
from vulnerabilities.models import Group
30+
from vulnerabilities.models import GroupedAdvisory
2831
from vulnerabilities.models import ImpactedPackageAffecting
2932
from vulnerabilities.models import PackageV2
3033
from vulnerabilities.throttling import PermissionBasedUserRateThrottle
@@ -273,15 +276,15 @@ def get_affected_by_vulnerabilities(self, package):
273276
)
274277

275278
affected_groups = [
276-
(
277-
list(adv.aliases.all()),
278-
adv.primary_advisory,
279-
[member.advisory for member in adv.secondary_members],
279+
Group(
280+
aliases=list(adv.aliases.all()),
281+
primary_advisory=adv.primary_advisory,
282+
secondaries=[member.advisory for member in adv.secondary_members],
280283
)
281284
for adv in affected_by_advisories_qs
282285
]
283286

284-
advisories = get_advisories_from_groups(affected_groups)
287+
advisories: List[GroupedAdvisory] = get_advisories_from_groups(affected_groups)
285288
return self.return_advisories_data(package, advisories_qs, advisories)
286289

287290
if package.type in TYPES_WITH_MULTIPLE_IMPORTERS:
@@ -290,7 +293,9 @@ def get_affected_by_vulnerabilities(self, package):
290293
"impacted_packages__affecting_packages",
291294
"impacted_packages__fixed_by_packages",
292295
)
293-
advisories = merge_and_save_grouped_advisories(package, advisories_qs, "affecting")
296+
advisories: List[GroupedAdvisory] = merge_and_save_grouped_advisories(
297+
package, advisories_qs, "affecting"
298+
)
294299
return self.return_advisories_data(package, advisories_qs, advisories)
295300

296301
def get_fixing_vulnerabilities(self, package):
@@ -333,15 +338,15 @@ def get_fixing_vulnerabilities(self, package):
333338
)
334339

335340
fixing_groups = [
336-
(
337-
list(adv.aliases.all()),
338-
adv.primary_advisory,
339-
[member.advisory for member in adv.secondary_members],
341+
Group(
342+
aliases=list(adv.aliases.all()),
343+
primary_advisory=adv.primary_advisory,
344+
secondaries=[member.advisory for member in adv.secondary_members],
340345
)
341346
for adv in fixing_advisories_qs
342347
]
343348

344-
advisories = get_advisories_from_groups(fixing_groups)
349+
advisories: List[GroupedAdvisory] = get_advisories_from_groups(fixing_groups)
345350
return self.return_fixing_advisories_data(advisories)
346351

347352
if package.type in TYPES_WITH_MULTIPLE_IMPORTERS:
@@ -350,15 +355,18 @@ def get_fixing_vulnerabilities(self, package):
350355
"impacted_packages__affecting_packages",
351356
"impacted_packages__fixed_by_packages",
352357
)
353-
advisories = merge_and_save_grouped_advisories(package, advisories_qs, "fixing")
358+
advisories: List[GroupedAdvisory] = merge_and_save_grouped_advisories(
359+
package, advisories_qs, "fixing"
360+
)
354361
return self.return_fixing_advisories_data(advisories)
355362

356363
def return_fixing_advisories_data(self, advisories):
357364
result = []
358365
for advisory in advisories:
366+
assert isinstance(advisory, GroupedAdvisory)
359367
result.append(
360368
{
361-
"advisory_id": advisory["identifier"],
369+
"advisory_id": advisory.identifier,
362370
}
363371
)
364372

@@ -378,18 +386,19 @@ def return_advisories_data(self, package, advisories_qs, advisories):
378386

379387
result = []
380388
for advisory in advisories:
381-
impact = impact_by_avid.get(advisory["advisory"].avid)
389+
assert isinstance(advisory, GroupedAdvisory)
390+
impact = impact_by_avid.get(advisory.advisory.avid)
382391
if not impact:
383392
continue
384393

385394
result.append(
386395
{
387-
"advisory_id": advisory["identifier"],
388-
"aliases": [alias.alias for alias in advisory["aliases"]],
389-
"weighted_severity": advisory["weighted_severity"],
390-
"exploitability": advisory["exploitability"],
391-
"risk_score": advisory["risk_score"],
392-
"summary": advisory["advisory"].summary,
396+
"advisory_id": advisory.identifier,
397+
"aliases": [alias.alias for alias in advisory.aliases],
398+
"weighted_severity": advisory.weighted_severity,
399+
"exploitability": advisory.exploitability,
400+
"risk_score": advisory.risk_score,
401+
"summary": advisory.advisory.summary,
393402
"fixed_by_packages": list(
394403
set([pkg.purl for pkg in impact.fixed_by_packages.all()])
395404
),

vulnerabilities/models.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
from operator import attrgetter
2121
from traceback import format_exc as traceback_format_exc
2222
from typing import List
23+
from typing import NamedTuple
24+
from typing import Optional
25+
from typing import Set
2326
from typing import Union
2427
from urllib.parse import urljoin
2528

@@ -3714,3 +3717,26 @@ def __str__(self):
37143717

37153718
class Meta:
37163719
unique_together = ("vector", "source_advisory")
3720+
3721+
3722+
class Group(NamedTuple):
3723+
"""
3724+
A Group of advisories that have been merged together based on their content and identifiers.
3725+
"""
3726+
3727+
aliases: Set[AdvisoryAlias]
3728+
primary: AdvisoryV2
3729+
secondaries: List[AdvisoryV2]
3730+
3731+
3732+
class GroupedAdvisory(NamedTuple):
3733+
"""
3734+
A GroupedAdvisory represents a single advisory that has been grouped with its aliases and related advisories.
3735+
"""
3736+
3737+
aliases: Set[AdvisoryAlias]
3738+
advisory: AdvisoryV2
3739+
identifier: str
3740+
weighted_severity: Optional[float]
3741+
exploitability: Optional[float]
3742+
risk_score: Optional[float]

vulnerabilities/pipelines/v2_improvers/group_advisories_for_packages.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10+
from typing import List
11+
1012
from vulnerabilities.models import AdvisoryV2
13+
from vulnerabilities.models import Group
1114
from vulnerabilities.models import PackageV2
1215
from vulnerabilities.pipelines import VulnerableCodePipeline
1316
from vulnerabilities.pipes.group_advisories import delete_and_save_advisory_set
@@ -48,8 +51,8 @@ def group_advisoris_for_packages(logger=None):
4851
)
4952

5053
try:
51-
affected_groups = merge_advisories(affecting_advisories, package)
52-
fixed_by_groups = merge_advisories(fixed_by_advisories, package)
54+
affected_groups: List[Group] = merge_advisories(affecting_advisories, package)
55+
fixed_by_groups: List[Group] = merge_advisories(fixed_by_advisories, package)
5356
delete_and_save_advisory_set(affected_groups, package, relation="affecting")
5457
delete_and_save_advisory_set(fixed_by_groups, package, relation="fixing")
5558
except Exception as e:

vulnerabilities/pipes/group_advisories.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,33 @@
1414
def delete_and_save_advisory_set(groups, package, relation=None):
1515
from vulnerabilities.models import AdvisorySet
1616
from vulnerabilities.models import AdvisorySetMember
17+
from vulnerabilities.models import Group
1718

1819
AdvisorySet.objects.filter(package=package, relation_type=relation).delete()
1920

2021
membership_to_create = []
2122

22-
for identifiers, primary, secondary in groups:
23+
for group in groups:
2324

25+
assert isinstance(group, Group)
2426
advisory_set = AdvisorySet.objects.create(
2527
package=package,
2628
relation_type=relation,
27-
primary_advisory=primary,
29+
primary_advisory=group.primary,
2830
)
2931

30-
advisory_set.aliases.add(*identifiers)
32+
advisory_set.aliases.add(*group.aliases)
3133
advisory_set.save()
3234

3335
membership_to_create.append(
3436
AdvisorySetMember(
3537
advisory_set=advisory_set,
36-
advisory=primary,
38+
advisory=group.primary,
3739
is_primary=True,
3840
)
3941
)
4042

41-
for adv in secondary:
43+
for adv in group.secondaries:
4244
membership_to_create.append(
4345
AdvisorySetMember(
4446
advisory_set=advisory_set,

vulnerabilities/tests/test_advisory_merge.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from vulnerabilities.models import AdvisorySet
1616
from vulnerabilities.models import AdvisorySetMember
1717
from vulnerabilities.models import AdvisoryV2
18+
from vulnerabilities.models import Group
1819
from vulnerabilities.models import ImpactedPackage
1920
from vulnerabilities.models import PackageV2
2021
from vulnerabilities.utils import compute_advisory_content_hash
@@ -136,8 +137,8 @@ def test_get_advisories_from_groups(self):
136137
groups = get_merged_identifier_groups([adv])
137138
result = get_advisories_from_groups(groups)
138139

139-
assert result[0]["identifier"] == "GHSA-ABC-123"
140-
assert len(result[0]["aliases"]) == 1
140+
assert result[0].identifier == "GHSA-ABC-123"
141+
assert len(result[0].aliases) == 1
141142

142143
def test_delete_and_save_advisory_set(self):
143144
package = PackageV2.objects.from_purl("pkg:pypi/sample@1.0.0")
@@ -147,7 +148,7 @@ def test_delete_and_save_advisory_set(self):
147148

148149
adv1.aliases.create(alias="CVE-1")
149150

150-
groups = [(set(adv1.aliases.all()), adv1, [adv2])]
151+
groups = [Group(aliases=set(adv1.aliases.all()), primary=adv1, secondaries=[adv2])]
151152

152153
delete_and_save_advisory_set(groups, package, relation="affecting")
153154

vulnerabilities/utils.py

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
from functools import total_ordering
2121
from http import HTTPStatus
2222
from typing import List
23+
from typing import NamedTuple
2324
from typing import Optional
25+
from typing import Set
2426
from typing import Tuple
2527
from typing import Union
2628
from unittest.mock import MagicMock
@@ -850,6 +852,7 @@ def merge_advisories(advisories, package):
850852
"""
851853
Merge advisories based on their content hash and identifiers.
852854
"""
855+
from vulnerabilities.models import Group
853856

854857
advisories = list(advisories)
855858

@@ -859,7 +862,7 @@ def merge_advisories(advisories, package):
859862
content_hash = compute_advisory_content_hash(adv, package)
860863
content_hash_map[content_hash].append(adv)
861864

862-
final_groups = []
865+
final_groups: List[Group] = []
863866

864867
for group in content_hash_map.values():
865868
groups = get_merged_identifier_groups(group)
@@ -901,6 +904,7 @@ def get_merged_identifier_groups(advisories):
901904
Merge advisories based on their identifiers (advisory_id and aliases).
902905
Example: If two advisories share ``advisory_id`` or share an alias, they will be merged together.
903906
"""
907+
from vulnerabilities.models import Group
904908

905909
identifier_groups = defaultdict(set)
906910

@@ -938,7 +942,7 @@ def get_merged_identifier_groups(advisories):
938942
if adv not in all_grouped:
939943
merged.append({adv})
940944

941-
final_groups = []
945+
final_groups: List[Group] = []
942946

943947
for group in merged:
944948
identifiers = set()
@@ -950,7 +954,7 @@ def get_merged_identifier_groups(advisories):
950954

951955
secondary = [a for a in group if a != primary]
952956

953-
final_groups.append((identifiers, primary, secondary))
957+
final_groups.append(Group(aliases=identifiers, primary=primary, secondaries=secondary))
954958

955959
return final_groups
956960

@@ -959,35 +963,48 @@ def get_advisories_from_groups(groups):
959963
"""
960964
Return a list of advisories from the merged groups of advisories.
961965
"""
966+
from vulnerabilities.models import Group
967+
from vulnerabilities.models import GroupedAdvisory
968+
962969
advisories = []
963-
weighted_severity = None
964-
exploitability = None
965-
risk_score = None
966-
for aliases, primary, secondaries in groups:
970+
971+
for group in groups:
972+
973+
assert isinstance(group, Group)
974+
weighted_severity = None
975+
exploitability = None
976+
risk_score = None
977+
967978
severity_scores = []
968-
exploitability_scores = []
969-
identifier = primary.advisory_id.split("/")[-1]
970-
filtered_aliases = [alias for alias in aliases if alias.alias != identifier]
971-
severity_scores.extend([adv.weighted_severity for adv in secondaries])
972-
exploitability_scores.extend([adv.exploitability for adv in secondaries])
973-
severity_scores.append(primary.weighted_severity)
974-
exploitability_scores.append(primary.exploitability)
979+
severity_scores.append(group.primary.weighted_severity or 0.0)
980+
severity_scores.extend([adv.weighted_severity or 0.0 for adv in group.secondaries])
981+
975982
if severity_scores:
976983
weighted_severity = round(max(severity_scores), 1)
984+
985+
exploitability_scores = []
986+
exploitability_scores.append(group.primary.exploitability or 0.0)
987+
exploitability_scores.extend([adv.exploitability or 0.0 for adv in group.secondaries])
988+
977989
if exploitability_scores:
978990
exploitability = max(exploitability_scores)
991+
979992
if exploitability and weighted_severity:
980993
risk_score = min(float(exploitability * weighted_severity), 10.0)
981994
risk_score = round(risk_score, 1)
995+
996+
identifier = group.primary.advisory_id.split("/")[-1]
997+
filtered_aliases = [alias for alias in group.aliases if alias.alias != identifier]
998+
982999
advisories.append(
983-
{
984-
"aliases": filtered_aliases,
985-
"advisory": primary,
986-
"identifier": identifier,
987-
"weighted_severity": weighted_severity,
988-
"exploitability": exploitability,
989-
"risk_score": risk_score,
990-
}
1000+
GroupedAdvisory(
1001+
aliases=filtered_aliases,
1002+
advisory=group.primary,
1003+
identifier=identifier,
1004+
weighted_severity=weighted_severity,
1005+
exploitability=exploitability,
1006+
risk_score=risk_score,
1007+
)
9911008
)
9921009

9931010
return advisories

0 commit comments

Comments
 (0)