Skip to content

Commit 15ff34f

Browse files
committed
Group advisories with alias and affected packages
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent f133fed commit 15ff34f

File tree

7 files changed

+769
-2
lines changed

7 files changed

+769
-2
lines changed

vulnerabilities/improvers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
enhance_with_metasploit as enhance_with_metasploit_v2,
3333
)
3434
from vulnerabilities.pipelines.v2_improvers import flag_ghost_packages as flag_ghost_packages_v2
35+
from vulnerabilities.pipelines.v2_improvers import group_advisories_for_packages
3536
from vulnerabilities.pipelines.v2_improvers import relate_severities
3637
from vulnerabilities.pipelines.v2_improvers import unfurl_version_range as unfurl_version_range_v2
3738
from vulnerabilities.utils import create_registry
@@ -76,5 +77,6 @@
7677
collect_ssvc_trees.CollectSSVCPipeline,
7778
relate_severities.RelateSeveritiesPipeline,
7879
compute_advisory_content_hash.ComputeAdvisoryContentHash,
80+
group_advisories_for_packages.GroupAdvisoriesForPackages,
7981
]
8082
)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Generated by Django 5.2.11 on 2026-03-25 10:34
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("vulnerabilities", "0117_advisoryv2_risk_score"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="AdvisorySet",
16+
fields=[
17+
(
18+
"id",
19+
models.AutoField(
20+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
21+
),
22+
),
23+
(
24+
"relation_type",
25+
models.CharField(
26+
choices=[("affecting", "Affecting"), ("fixing", "Fixing")], max_length=20
27+
),
28+
),
29+
("identifiers", models.JSONField()),
30+
("created_at", models.DateTimeField(auto_now_add=True)),
31+
(
32+
"package",
33+
models.ForeignKey(
34+
on_delete=django.db.models.deletion.CASCADE, to="vulnerabilities.packagev2"
35+
),
36+
),
37+
(
38+
"primary_advisory",
39+
models.ForeignKey(
40+
on_delete=django.db.models.deletion.PROTECT, to="vulnerabilities.advisoryv2"
41+
),
42+
),
43+
],
44+
),
45+
migrations.CreateModel(
46+
name="AdvisorySetMember",
47+
fields=[
48+
(
49+
"id",
50+
models.AutoField(
51+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
52+
),
53+
),
54+
("is_primary", models.BooleanField(default=False)),
55+
(
56+
"advisory",
57+
models.ForeignKey(
58+
on_delete=django.db.models.deletion.CASCADE, to="vulnerabilities.advisoryv2"
59+
),
60+
),
61+
(
62+
"advisory_set",
63+
models.ForeignKey(
64+
on_delete=django.db.models.deletion.CASCADE,
65+
related_name="members",
66+
to="vulnerabilities.advisoryset",
67+
),
68+
),
69+
],
70+
),
71+
]

vulnerabilities/models.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2920,6 +2920,53 @@ def latest_advisories_for_purls(self, purls):
29202920
qs = self.filter(id__in=Subquery(adv_ids))
29212921
return qs.latest_per_avid()
29222922

2923+
def latest_advisories_for_purl(self, purl):
2924+
adv_ids = (
2925+
ImpactedPackageAffecting.objects.filter(package__package_url=purl)
2926+
.values_list(
2927+
"impacted_package__advisory_id",
2928+
flat=True,
2929+
)
2930+
.union(
2931+
ImpactedPackageFixedBy.objects.filter(package__package_url=purl).values_list(
2932+
"impacted_package__advisory_id",
2933+
flat=True,
2934+
)
2935+
)
2936+
)
2937+
2938+
qs = self.filter(id__in=Subquery(adv_ids))
2939+
return qs.latest_per_avid()
2940+
2941+
2942+
class AdvisorySet(models.Model):
2943+
2944+
RELATION_TYPE_CHOICES = [
2945+
("affecting", "Affecting"),
2946+
("fixing", "Fixing"),
2947+
]
2948+
2949+
package = models.ForeignKey("PackageV2", on_delete=models.CASCADE)
2950+
relation_type = models.CharField(max_length=20, choices=RELATION_TYPE_CHOICES)
2951+
2952+
identifiers = models.JSONField()
2953+
2954+
primary_advisory = models.ForeignKey("AdvisoryV2", on_delete=models.PROTECT)
2955+
2956+
created_at = models.DateTimeField(auto_now_add=True)
2957+
2958+
2959+
class AdvisorySetMember(models.Model):
2960+
2961+
advisory_set = models.ForeignKey(
2962+
AdvisorySet,
2963+
on_delete=models.CASCADE,
2964+
related_name="members",
2965+
)
2966+
2967+
advisory = models.ForeignKey("AdvisoryV2", on_delete=models.CASCADE)
2968+
is_primary = models.BooleanField(default=False)
2969+
29232970

29242971
class AdvisoryV2(models.Model):
29252972
"""
@@ -3085,6 +3132,9 @@ def save(self, *args, **kwargs):
30853132
self.full_clean()
30863133
return super().save(*args, **kwargs)
30873134

3135+
def __str__(self):
3136+
return self.avid
3137+
30883138
@property
30893139
def get_status_label(self):
30903140
label_by_status = {choice[0]: choice[1] for choice in VulnerabilityStatusType.choices}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
from collections import defaultdict
11+
12+
from django.db import transaction
13+
14+
from vulnerabilities.models import AdvisorySet
15+
from vulnerabilities.models import AdvisorySetMember
16+
from vulnerabilities.models import AdvisoryV2
17+
from vulnerabilities.models import PackageV2
18+
from vulnerabilities.pipelines import VulnerableCodePipeline
19+
from vulnerabilities.utils import compute_advisory_content
20+
21+
22+
class GroupAdvisoriesForPackages(VulnerableCodePipeline):
23+
"""Detect and flag packages that do not exist upstream."""
24+
25+
pipeline_id = "group_advisories_for_packages"
26+
27+
@classmethod
28+
def steps(cls):
29+
return (cls.group_advisories_for_packages,)
30+
31+
def group_advisories_for_packages(self):
32+
group_advisoris_for_packages(logger=self.log)
33+
34+
35+
def merge_advisories(advisories):
36+
37+
advisories = list(advisories)
38+
39+
content_hash_map = defaultdict(list)
40+
result_groups = []
41+
42+
for adv in advisories:
43+
44+
if adv.advisory_content_hash:
45+
content_hash_map[adv.advisory_content_hash].append(adv)
46+
else:
47+
content_hash = compute_advisory_content(advisory_data=adv)
48+
if content_hash:
49+
content_hash_map[content_hash].append(adv)
50+
else:
51+
result_groups.append([adv])
52+
53+
final_groups = []
54+
55+
for group in content_hash_map.values():
56+
groups = get_merged_identifier_groups(group)
57+
final_groups.extend(groups)
58+
59+
return final_groups
60+
61+
62+
def get_merged_identifier_groups(advisories):
63+
64+
identifier_groups = defaultdict(set)
65+
advisory_to_identifiers = defaultdict(set)
66+
67+
advisories = list(advisories)
68+
69+
for adv in advisories:
70+
71+
identifier_groups[adv.advisory_id].add(adv)
72+
advisory_to_identifiers[adv].add(adv.advisory_id)
73+
74+
for alias in adv.aliases.all():
75+
identifier_groups[alias.alias].add(adv)
76+
advisory_to_identifiers[adv].add(alias.alias)
77+
78+
groups = [set(advs) for advs in identifier_groups.values() if len(advs) > 1]
79+
80+
merged = []
81+
82+
for group in groups:
83+
group = set(group)
84+
85+
i = 0
86+
while i < len(merged):
87+
if group & merged[i]:
88+
group |= merged[i]
89+
merged.pop(i)
90+
else:
91+
i += 1
92+
93+
merged.append(group)
94+
95+
all_grouped = set()
96+
for g in merged:
97+
all_grouped |= g
98+
99+
for adv in advisories:
100+
if adv not in all_grouped:
101+
merged.append({adv})
102+
103+
final_groups = []
104+
105+
for group in merged:
106+
identifiers = set()
107+
for adv in group:
108+
for alias in adv.aliases.values_list("alias", flat=True):
109+
identifiers.add(alias)
110+
111+
primary = max(group, key=lambda a: a.precedence if a.precedence is not None else -1)
112+
113+
secondary = [a for a in group if a != primary]
114+
115+
final_groups.append((identifiers, primary, secondary))
116+
117+
return final_groups
118+
119+
120+
def group_advisoris_for_packages(logger=None):
121+
for package in PackageV2.objects.iterator():
122+
affecting_advisories = AdvisoryV2.objects.latest_affecting_advisories_for_purl(
123+
purl=package.purl
124+
).prefetch_related("aliases")
125+
126+
fixed_by_advisories = AdvisoryV2.objects.latest_fixed_by_advisories_for_purl(
127+
purl=package.purl
128+
).prefetch_related("aliases")
129+
130+
try:
131+
delete_and_save_advisory_set(package, affecting_advisories, relation="affecting")
132+
delete_and_save_advisory_set(package, fixed_by_advisories, relation="fixing")
133+
except Exception as e:
134+
print(f"Failed rebuilding advisory sets for package {package.purl}: {e!r}")
135+
continue
136+
137+
138+
@transaction.atomic
139+
def delete_and_save_advisory_set(package, advisories, relation=None):
140+
AdvisorySet.objects.filter(package=package, relation_type=relation).delete()
141+
142+
groups = merge_advisories(advisories)
143+
144+
membership_to_create = []
145+
146+
for identifiers, primary, secondary in groups:
147+
148+
advisory_set = AdvisorySet.objects.create(
149+
package=package,
150+
relation_type=relation,
151+
identifiers=list(identifiers),
152+
primary_advisory=primary,
153+
)
154+
155+
membership_to_create.append(
156+
AdvisorySetMember(
157+
advisory_set=advisory_set,
158+
advisory=primary,
159+
is_primary=True,
160+
)
161+
)
162+
163+
for adv in secondary:
164+
membership_to_create.append(
165+
AdvisorySetMember(
166+
advisory_set=advisory_set,
167+
advisory=adv,
168+
is_primary=False,
169+
)
170+
)
171+
172+
AdvisorySetMember.objects.bulk_create(membership_to_create)

0 commit comments

Comments
 (0)