Skip to content

Commit 8cc7c50

Browse files
committed
Adjust API and UI for new grouping
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 772848f commit 8cc7c50

File tree

17 files changed

+620
-461
lines changed

17 files changed

+620
-461
lines changed

vulnerabilities/api_v3.py

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@
2020
from rest_framework.throttling import AnonRateThrottle
2121

2222
from vulnerabilities.models import AdvisoryReference
23+
from vulnerabilities.models import AdvisorySet
2324
from vulnerabilities.models import AdvisorySeverity
2425
from vulnerabilities.models import AdvisoryV2
2526
from vulnerabilities.models import AdvisoryWeakness
2627
from vulnerabilities.models import ImpactedPackageAffecting
2728
from vulnerabilities.models import PackageV2
2829
from vulnerabilities.throttling import PermissionBasedUserRateThrottle
29-
from vulnerabilities.utils import group_advisories_by_content
30+
from vulnerabilities.utils import TYPES_WITH_MULTIPLE_IMPORTERS
31+
from vulnerabilities.utils import get_advisories_from_groups
32+
from vulnerabilities.utils import merge_and_save_grouped_advisories
3033

3134

3235
class PackageQuerySerializer(serializers.Serializer):
@@ -210,6 +213,32 @@ def get_affected_by_vulnerabilities(self, package):
210213
"""Return a dictionary with advisory as keys and their details, including fixed_by_packages."""
211214
advisories_qs = AdvisoryV2.objects.latest_affecting_advisories_for_purl(package.package_url)
212215

216+
advisories = []
217+
218+
is_grouped = AdvisorySet.objects.filter(package=package, relation_type="affecting").exists()
219+
220+
if is_grouped:
221+
affected_by_advisories_qs = AdvisorySet.objects.filter(
222+
package=package, relation_type="affecting"
223+
).select_related("primary_advisory")
224+
225+
affected_groups = [
226+
(list(adv.aliases.all()), adv.primary_advisory, "")
227+
for adv in affected_by_advisories_qs
228+
]
229+
230+
advisories = get_advisories_from_groups(affected_groups)
231+
return self.return_advisories_data(package, advisories_qs, advisories)
232+
233+
if package.type in TYPES_WITH_MULTIPLE_IMPORTERS:
234+
advisories_qs = advisories_qs.prefetch_related(
235+
"aliases",
236+
"impacted_packages__affecting_packages",
237+
"impacted_packages__fixed_by_packages",
238+
)
239+
advisories = merge_and_save_grouped_advisories(package, advisories_qs, "affecting")
240+
return self.return_advisories_data(package, advisories_qs, advisories)
241+
213242
advisories_ids = advisories_qs.only("id")
214243

215244
advisories_ids = list(advisories_ids[:101])
@@ -227,20 +256,19 @@ def get_affected_by_vulnerabilities(self, package):
227256

228257
impact_by_avid = {impact.advisory.avid: impact for impact in impacts}
229258

230-
grouped = group_advisories_by_content(advisories_qs)
231-
232259
result = []
233-
for entry in grouped.values():
234-
primary = entry["primary"]
235-
impact = impact_by_avid.get(primary.avid)
260+
261+
for advisory in advisories_qs:
262+
impact = impact_by_avid.get(advisory.avid)
236263
if not impact:
237264
continue
238265

239266
result.append(
240267
{
241-
"advisory_id": primary.avid,
268+
"advisory_id": advisory.advisory_id.split("/")[-1],
269+
"aliases": [alias.alias for alias in advisory.aliases.all()],
270+
"summary": advisory.summary,
242271
"fixed_by_packages": [pkg.purl for pkg in impact.fixed_by_packages.all()],
243-
"duplicate_advisory_ids": [a.avid for a in entry["secondary"]],
244272
}
245273
)
246274

@@ -249,21 +277,82 @@ def get_affected_by_vulnerabilities(self, package):
249277
def get_fixing_vulnerabilities(self, package):
250278
advisories_qs = AdvisoryV2.objects.latest_fixed_by_advisories_for_purl(package.package_url)
251279

280+
advisories = []
281+
282+
is_grouped = AdvisorySet.objects.filter(package=package, relation_type="fixing").exists()
283+
284+
if is_grouped:
285+
fixing_advisories_qs = AdvisorySet.objects.filter(
286+
package=package, relation_type="fixing"
287+
).select_related("primary_advisory")
288+
289+
fixing_groups = [
290+
(list(adv.aliases.all()), adv.primary_advisory, "") for adv in fixing_advisories_qs
291+
]
292+
293+
advisories = get_advisories_from_groups(fixing_groups)
294+
return self.return_fixing_advisories_data(advisories)
295+
296+
if package.type in TYPES_WITH_MULTIPLE_IMPORTERS:
297+
advisories_qs = advisories_qs.prefetch_related(
298+
"aliases",
299+
"impacted_packages__affecting_packages",
300+
"impacted_packages__fixed_by_packages",
301+
)
302+
advisories = merge_and_save_grouped_advisories(package, advisories_qs, "fixing")
303+
return self.return_fixing_advisories_data(advisories)
304+
252305
advisories_ids = advisories_qs.only("id")
253306

254307
advisories_ids = list(advisories_ids[:101])
255308
if len(advisories_ids) > 100:
256309
return None
257310

258-
grouped = group_advisories_by_content(advisories_qs)
311+
results = []
259312

313+
for advisory in advisories_qs:
314+
results.append(
315+
{
316+
"advisory_id": advisory.advisory_id.split("/")[-1],
317+
}
318+
)
319+
return results
320+
321+
def return_fixing_advisories_data(self, advisories):
260322
result = []
261-
for entry in grouped.values():
262-
primary = entry["primary"]
323+
for advisory in advisories:
263324
result.append(
264325
{
265-
"advisory_id": primary.avid,
266-
"duplicate_advisory_ids": [a.avid for a in entry["secondary"]],
326+
"advisory_id": advisory["identifier"],
327+
}
328+
)
329+
330+
return result
331+
332+
def return_advisories_data(self, package, advisories_qs, advisories):
333+
advisory_by_avid = {adv.avid: adv for adv in advisories_qs}
334+
avids = advisory_by_avid.keys()
335+
336+
impacts = (
337+
package.affected_in_impacts.filter(advisory__avid__in=avids)
338+
.select_related("advisory")
339+
.prefetch_related("fixed_by_packages")
340+
)
341+
342+
impact_by_avid = {impact.advisory.avid: impact for impact in impacts}
343+
344+
result = []
345+
for advisory in advisories:
346+
impact = impact_by_avid.get(advisory["advisory"].avid)
347+
if not impact:
348+
continue
349+
350+
result.append(
351+
{
352+
"advisory_id": advisory["identifier"],
353+
"aliases": [alias.alias for alias in advisory["aliases"]],
354+
"summary": advisory["advisory"].summary,
355+
"fixed_by_packages": [pkg.purl for pkg in impact.fixed_by_packages.all()],
267356
}
268357
)
269358

vulnerabilities/improvers/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
from vulnerabilities.pipelines import populate_vulnerability_summary_pipeline
2121
from vulnerabilities.pipelines import remove_duplicate_advisories
2222
from vulnerabilities.pipelines.v2_improvers import collect_ssvc_trees
23-
from vulnerabilities.pipelines.v2_improvers import compute_advisory_content_hash
2423
from vulnerabilities.pipelines.v2_improvers import compute_advisory_todo as compute_advisory_todo_v2
2524
from vulnerabilities.pipelines.v2_improvers import compute_package_risk as compute_package_risk_v2
2625
from vulnerabilities.pipelines.v2_improvers import (
@@ -76,7 +75,6 @@
7675
compute_advisory_todo.ComputeToDo,
7776
collect_ssvc_trees.CollectSSVCPipeline,
7877
relate_severities.RelateSeveritiesPipeline,
79-
compute_advisory_content_hash.ComputeAdvisoryContentHash,
8078
group_advisories_for_packages.GroupAdvisoriesForPackages,
8179
]
8280
)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Generated by Django 5.2.11 on 2026-03-30 08:35
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("vulnerabilities", "0118_advisoryset_advisorysetmember"),
10+
]
11+
12+
operations = [
13+
migrations.RemoveField(
14+
model_name="advisoryset",
15+
name="identifiers",
16+
),
17+
migrations.RemoveField(
18+
model_name="advisoryv2",
19+
name="advisory_content_hash",
20+
),
21+
migrations.AddField(
22+
model_name="advisoryset",
23+
name="aliases",
24+
field=models.ManyToManyField(
25+
help_text="A list of serializable Alias objects",
26+
related_name="advisory_sets",
27+
to="vulnerabilities.advisoryalias",
28+
),
29+
),
30+
]

vulnerabilities/models.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2949,7 +2949,11 @@ class AdvisorySet(models.Model):
29492949
package = models.ForeignKey("PackageV2", on_delete=models.CASCADE)
29502950
relation_type = models.CharField(max_length=20, choices=RELATION_TYPE_CHOICES)
29512951

2952-
identifiers = models.JSONField()
2952+
aliases = models.ManyToManyField(
2953+
AdvisoryAlias,
2954+
related_name="advisory_sets",
2955+
help_text="A list of serializable Alias objects",
2956+
)
29532957

29542958
primary_advisory = models.ForeignKey("AdvisoryV2", on_delete=models.PROTECT)
29552959

@@ -3101,13 +3105,6 @@ class AdvisoryV2(models.Model):
31013105
help_text="Related advisories that are used to calculate the severity of this advisory.",
31023106
)
31033107

3104-
advisory_content_hash = models.CharField(
3105-
max_length=64,
3106-
blank=True,
3107-
null=True,
3108-
help_text="A unique hash computed from the content of the advisory used to identify advisories with the same content.",
3109-
)
3110-
31113108
risk_score = models.DecimalField(
31123109
null=True,
31133110
blank=True,
@@ -3311,7 +3308,7 @@ def search(self, query: str = None):
33113308
except ValueError:
33123309
# otherwise use query as a plain string
33133310
qs = qs.filter(package_url__icontains=query)
3314-
return qs.order_by("package_url")
3311+
return qs.order_by("package_url").order_by("-version_rank")
33153312

33163313
def with_vulnerability_counts(self):
33173314
return self.annotate(

vulnerabilities/pipelines/v2_importers/github_osv_importer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class GithubOSVImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
3131
license_url = "https://github.com/github/advisory-database/blob/main/LICENSE.md"
3232
repo_url = "git+https://github.com/github/advisory-database/"
3333

34-
precedence = 100
34+
precedence = 200
3535

3636
@classmethod
3737
def steps(cls):

vulnerabilities/pipelines/v2_importers/pypa_importer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class PyPaImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
2929
spdx_license_expression = "CC-BY-4.0"
3030
license_url = "https://github.com/pypa/advisory-database/blob/main/LICENSE"
3131
repo_url = "git+https://github.com/pypa/advisory-database"
32-
precedence = 200
32+
precedence = 500
3333

3434
@classmethod
3535
def steps(cls):

vulnerabilities/pipelines/v2_importers/pysec_importer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class PyPIImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
2929
license_url = "https://github.com/pypa/advisory-database/blob/main/LICENSE"
3030
url = "https://osv-vulnerabilities.storage.googleapis.com/PyPI/all.zip"
3131
spdx_license_expression = "CC-BY-4.0"
32-
precedence = 100
32+
precedence = 300
3333

3434
@classmethod
3535
def steps(cls):

vulnerabilities/pipelines/v2_improvers/compute_advisory_content_hash.py

Lines changed: 0 additions & 65 deletions
This file was deleted.

0 commit comments

Comments
 (0)