Skip to content

Commit bd13ee9

Browse files
authored
Merge pull request #2268 from aboutcode-org/store-latest-advisory
feat: add new field to track latest version of v2 advisory
2 parents 1707d1c + afd9d53 commit bd13ee9

File tree

10 files changed

+283
-111
lines changed

10 files changed

+283
-111
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Generated by Django 5.2.11 on 2026-04-13 19:05
2+
3+
from django.db import migrations
4+
from django.db import models
5+
from django.db.models import F
6+
7+
8+
class Migration(migrations.Migration):
9+
def add_is_latest_on_existing_advisory(apps, schema_editor):
10+
Advisory = apps.get_model("vulnerabilities", "AdvisoryV2")
11+
12+
print(f"\nUpdating is_latest on existing V2 Advisory.")
13+
latest_qs = Advisory.objects.order_by(
14+
"avid",
15+
F("date_collected").desc(nulls_last=True),
16+
"-id",
17+
).distinct("avid")
18+
19+
Advisory.objects.filter(id__in=latest_qs.values("id")).update(is_latest=True)
20+
21+
dependencies = [
22+
("vulnerabilities", "0120_impactedpackage_last_range_unfurl_at_and_more"),
23+
]
24+
25+
operations = [
26+
migrations.AddField(
27+
model_name="advisoryv2",
28+
name="is_latest",
29+
field=models.BooleanField(
30+
db_index=True,
31+
default=False,
32+
help_text="Indicates whether this is the latest version of the advisory identified by its AVID.",
33+
),
34+
),
35+
migrations.AlterField(
36+
model_name="advisoryv2",
37+
name="advisory_id",
38+
field=models.CharField(
39+
db_index=True,
40+
help_text="An advisory is a unique vulnerability identifier in some database, such as PYSEC-2020-2233",
41+
max_length=500,
42+
),
43+
),
44+
migrations.AlterField(
45+
model_name="advisoryv2",
46+
name="datasource_id",
47+
field=models.CharField(
48+
db_index=True,
49+
help_text="Unique ID for the datasource used for this advisory .e.g.: nginx_importer_v2",
50+
max_length=200,
51+
),
52+
),
53+
migrations.AddConstraint(
54+
model_name="advisoryv2",
55+
constraint=models.UniqueConstraint(
56+
condition=models.Q(("is_latest", True)),
57+
fields=("avid",),
58+
name="unique_latest_per_avid",
59+
),
60+
),
61+
migrations.RunPython(
62+
code=add_is_latest_on_existing_advisory,
63+
reverse_code=migrations.RunPython.noop,
64+
),
65+
]

vulnerabilities/models.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2886,11 +2886,7 @@ def latest_for_avid(self, avid: str):
28862886
)
28872887

28882888
def latest_per_avid(self):
2889-
return self.order_by(
2890-
"avid",
2891-
F("date_collected").desc(nulls_last=True),
2892-
"-id",
2893-
).distinct("avid")
2889+
return self.filter(is_latest=True)
28942890

28952891
def latest_for_avids(self, avids):
28962892
return self.filter(avid__in=avids).latest_per_avid()
@@ -3007,6 +3003,7 @@ class AdvisoryV2(models.Model):
30073003
max_length=200,
30083004
blank=False,
30093005
null=False,
3006+
db_index=True,
30103007
help_text="Unique ID for the datasource used for this advisory ." "e.g.: nginx_importer_v2",
30113008
)
30123009

@@ -3016,6 +3013,7 @@ class AdvisoryV2(models.Model):
30163013
blank=False,
30173014
null=False,
30183015
unique=False,
3016+
db_index=True,
30193017
help_text="An advisory is a unique vulnerability identifier in some database, "
30203018
"such as PYSEC-2020-2233",
30213019
)
@@ -3090,6 +3088,14 @@ class AdvisoryV2(models.Model):
30903088
help_text="UTC Date on which the advisory was collected",
30913089
)
30923090

3091+
is_latest = models.BooleanField(
3092+
default=False,
3093+
blank=False,
3094+
null=False,
3095+
db_index=True,
3096+
help_text="Indicates whether this is the latest version of the advisory identified by its AVID.",
3097+
)
3098+
30933099
original_advisory_text = models.TextField(
30943100
blank=True,
30953101
null=True,
@@ -3142,6 +3148,11 @@ class AdvisoryV2(models.Model):
31423148
class Meta:
31433149
unique_together = ["datasource_id", "advisory_id", "unique_content_id"]
31443150
ordering = ["datasource_id", "advisory_id", "date_published", "unique_content_id"]
3151+
constraints = [
3152+
models.UniqueConstraint(
3153+
fields=["avid"], condition=Q(is_latest=True), name="unique_latest_per_avid"
3154+
)
3155+
]
31453156
indexes = [
31463157
models.Index(
31473158
fields=["avid", "-date_collected", "-id"],

vulnerabilities/pipes/advisory.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,13 @@ def insert_advisory_v2(
334334
if not created:
335335
return advisory_obj
336336

337+
AdvisoryV2.objects.filter(
338+
avid=f"{pipeline_id}/{advisory.advisory_id}",
339+
is_latest=True,
340+
).update(is_latest=False)
341+
advisory_obj.is_latest = True
342+
advisory_obj.save()
343+
337344
aliases = get_or_create_advisory_aliases(aliases=advisory.aliases)
338345
references = get_or_create_advisory_references(references=advisory.references)
339346
severities = get_or_create_advisory_severities(severities=advisory.severities)

vulnerabilities/tests/pipelines/v2_improvers/test_collect_ssvc_trees.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def vulnrichment_advisory(db):
3636
url="https://example.com/advisory/TEST-2024-0001",
3737
unique_content_id="unique-1234",
3838
date_collected=datetime.now(),
39+
is_latest=True,
3940
)
4041

4142

@@ -59,6 +60,7 @@ def related_advisory(db):
5960
url="https://example.com/related/TEST-2024-0001",
6061
unique_content_id="unique-5678",
6162
date_collected=datetime.now(),
63+
is_latest=True,
6264
)
6365

6466

vulnerabilities/tests/pipelines/v2_improvers/test_compute_package_risk_v2.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def test_simple_risk_pipeline():
3434
unique_content_id="ajkef",
3535
url="https://test.com",
3636
date_collected=datetime.now(),
37+
is_latest=True,
3738
)
3839
adv.save()
3940

vulnerabilities/tests/pipelines/v2_improvers/test_relate_severities.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def test_relate_severities_by_advisory_id():
2525
unique_content_id="ab1",
2626
url="https://example.com/advisory/CVE-2024-0001",
2727
date_collected="2024-01-01",
28+
is_latest=True,
2829
)
2930

3031
severity_advisory = AdvisoryV2.objects.create(
@@ -34,6 +35,7 @@ def test_relate_severities_by_advisory_id():
3435
unique_content_id="ab2",
3536
url="https://example.com/epss/CVE-2024-0001",
3637
date_collected="2024-01-02",
38+
is_latest=True,
3739
)
3840
severity_advisory.severities.create(
3941
scoring_system=EPSS.identifier,
@@ -59,6 +61,7 @@ def test_relate_severities_via_alias():
5961
unique_content_id="ab3",
6062
url="https://example.com/advisory/CVE-2024-0002",
6163
date_collected="2024-01-01",
64+
is_latest=True,
6265
)
6366

6467
base.aliases.create(alias="CVE-2024-ALIAS")
@@ -70,6 +73,7 @@ def test_relate_severities_via_alias():
7073
unique_content_id="ab4",
7174
url="https://example.com/epss/CVE-2024-ALIAS",
7275
date_collected="2024-01-02",
76+
is_latest=True,
7377
)
7478
severity_advisory.severities.create(
7579
scoring_system=EPSS.identifier,
@@ -91,6 +95,7 @@ def test_no_self_relation_created():
9195
url="https://example.com/advisory/CVE-2024-0003",
9296
date_collected="2024-01-03",
9397
avid="epss/CVE-2024-0003",
98+
is_latest=True,
9499
)
95100
advisory.severities.create(
96101
scoring_system=EPSS.identifier,
@@ -112,6 +117,7 @@ def test_unsupported_severity_system_is_ignored():
112117
url="https://example.com/advisory/CVE-2024-0004",
113118
date_collected="2024-01-01",
114119
avid="nvd/CVE-2024-0004",
120+
is_latest=True,
115121
)
116122

117123
severity_advisory = AdvisoryV2.objects.create(
@@ -121,6 +127,7 @@ def test_unsupported_severity_system_is_ignored():
121127
url="https://example.com/epss/CVE-2024-0004",
122128
date_collected="2024-01-02",
123129
avid="epss/CVE-2024-0004",
130+
is_latest=True,
124131
)
125132
severity_advisory.severities.create(
126133
scoring_system="UNKNOWN_SYSTEM",
@@ -142,6 +149,7 @@ def test_pipeline_is_idempotent():
142149
url="https://example.com/advisory/CVE-2024-0005",
143150
date_collected="2024-01-01",
144151
avid="nvd/CVE-2024-0005",
152+
is_latest=True,
145153
)
146154

147155
severity = AdvisoryV2.objects.create(
@@ -150,6 +158,7 @@ def test_pipeline_is_idempotent():
150158
unique_content_id="ab9",
151159
url="https://example.com/epss/CVE-2024-0005",
152160
date_collected="2024-01-02",
161+
is_latest=True,
153162
avid="epss/CVE-2024-0005",
154163
)
155164
severity.severities.create(

vulnerabilities/tests/pipes/test_advisory.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818

1919
from vulnerabilities import models
2020
from vulnerabilities.importer import AdvisoryData
21+
from vulnerabilities.importer import AdvisoryDataV2
2122
from vulnerabilities.importer import AffectedPackage
23+
from vulnerabilities.importer import AffectedPackageV2
2224
from vulnerabilities.importer import PackageCommitPatchData
2325
from vulnerabilities.importer import Reference
2426
from vulnerabilities.models import AdvisoryAlias
2527
from vulnerabilities.models import AdvisoryReference
2628
from vulnerabilities.models import AdvisorySeverity
29+
from vulnerabilities.models import AdvisoryV2
2730
from vulnerabilities.models import AdvisoryWeakness
2831
from vulnerabilities.models import PackageCommitPatch
2932
from vulnerabilities.pipes.advisory import get_or_create_advisory_aliases
@@ -33,6 +36,8 @@
3336
from vulnerabilities.pipes.advisory import get_or_create_advisory_weaknesses
3437
from vulnerabilities.pipes.advisory import get_or_create_aliases
3538
from vulnerabilities.pipes.advisory import import_advisory
39+
from vulnerabilities.pipes.advisory import insert_advisory_v2
40+
from vulnerabilities.tests.pipelines import TestLogger
3641
from vulnerabilities.utils import compute_content_id
3742

3843

@@ -257,3 +262,76 @@ def test_get_or_create_advisory_commit(advisory_commit):
257262
assert isinstance(commit, PackageCommitPatch)
258263
assert commit.commit_hash in [c.commit_hash for c in advisory_commit]
259264
assert commit.vcs_url in [c.vcs_url for c in advisory_commit]
265+
266+
267+
class TestLatestAdvisoryV2(TestCase):
268+
def setUp(self):
269+
self.logger = TestLogger()
270+
self.advisory1 = AdvisoryDataV2(
271+
summary="Test advisory old",
272+
aliases=["CVE-2025-0001"],
273+
references=[],
274+
severities=[],
275+
weaknesses=[],
276+
affected_packages=[
277+
AffectedPackageV2(
278+
package=PackageURL.from_string("pkg:npm/foobar"),
279+
affected_version_range=VersionRange.from_string("vers:npm/>3.2.1|<4.0.0"),
280+
fixed_version_range=VersionRange.from_string("vers:npm/4.0.0"),
281+
introduced_by_commit_patches=[],
282+
fixed_by_commit_patches=[],
283+
),
284+
],
285+
patches=[],
286+
advisory_id="GHSA-1234",
287+
url="https://example.com/advisory",
288+
)
289+
290+
self.advisory2 = AdvisoryDataV2(
291+
summary="Test advisory new",
292+
aliases=["CVE-2025-0001"],
293+
references=[],
294+
severities=[],
295+
weaknesses=[],
296+
affected_packages=[
297+
AffectedPackageV2(
298+
package=PackageURL.from_string("pkg:npm/foobar"),
299+
affected_version_range=VersionRange.from_string("vers:npm/>3.2.1|<4.0.0"),
300+
fixed_version_range=VersionRange.from_string("vers:npm/4.0.0"),
301+
introduced_by_commit_patches=[],
302+
fixed_by_commit_patches=[],
303+
),
304+
AffectedPackageV2(
305+
package=PackageURL.from_string("pkg:npm/foobar"),
306+
affected_version_range=None,
307+
fixed_version_range=None,
308+
introduced_by_commit_patches=[],
309+
fixed_by_commit_patches=[
310+
PackageCommitPatchData(
311+
vcs_url="https://foobar.vcs/",
312+
commit_hash="982f801f",
313+
),
314+
],
315+
),
316+
],
317+
patches=[],
318+
advisory_id="GHSA-1234",
319+
url="https://example.com/advisory",
320+
)
321+
322+
insert_advisory_v2(
323+
advisory=self.advisory1,
324+
pipeline_id="test_pipeline_v2",
325+
logger=self.logger.write,
326+
)
327+
328+
def test_latest_advisory_update_on_advisory_insert(self):
329+
adv_old = AdvisoryV2.objects.get(avid="test_pipeline_v2/GHSA-1234", is_latest=True)
330+
insert_advisory_v2(
331+
advisory=self.advisory2,
332+
pipeline_id="test_pipeline_v2",
333+
logger=self.logger.write,
334+
)
335+
adv_new = AdvisoryV2.objects.get(avid="test_pipeline_v2/GHSA-1234", is_latest=True)
336+
self.assertEqual("Test advisory old", adv_old.summary)
337+
self.assertEqual("Test advisory new", adv_new.summary)

vulnerabilities/tests/test_api_v3.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,26 @@
1414
from rest_framework.test import APITestCase
1515
from univers.version_range import PypiVersionRange
1616

17+
from vulnerabilities.importer import AdvisoryDataV2
1718
from vulnerabilities.models import AdvisoryV2
1819
from vulnerabilities.models import PackageV2
1920
from vulnerabilities.pipes.advisory import insert_advisory_v2
21+
from vulnerabilities.tests.pipelines import TestLogger
2022

2123

2224
class APIV3TestCase(APITestCase):
2325
def setUp(self):
2426
from vulnerabilities.models import ImpactedPackage
2527

26-
self.advisory = AdvisoryV2.objects.create(
27-
datasource_id="ghsa",
28-
advisory_id="GHSA-1234",
29-
avid="ghsa/GHSA-1234",
30-
unique_content_id="f" * 64,
31-
url="https://example.com/advisory",
32-
date_collected="2025-07-01T00:00:00Z",
28+
self.logger = TestLogger()
29+
self.advisory = insert_advisory_v2(
30+
advisory=AdvisoryDataV2(
31+
summary="summary",
32+
advisory_id="GHSA-1234",
33+
url="https://example.com/advisory",
34+
),
35+
pipeline_id="ghsa",
36+
logger=self.logger.write,
3337
)
3438

3539
self.package = PackageV2.objects.from_purl(purl="pkg:pypi/sample@1.0.0")

0 commit comments

Comments
 (0)