Skip to content

Commit 3ce578d

Browse files
authored
Add an UUID field on the DiscoveredDependency model #1651 (#1654)
* Add an UUID field on the DiscoveredDependency model #1651 Signed-off-by: tdruez <tdruez@nexb.com> * Use the UUID for the DiscoveredDependency spdx_id #1651 Signed-off-by: tdruez <tdruez@nexb.com> * Add changelog entry #1651 Signed-off-by: tdruez <tdruez@nexb.com> --------- Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 759e306 commit 3ce578d

8 files changed

+119
-17
lines changed

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Changelog
22
=========
33

4+
v34.10.2 (unreleased)
5+
---------------------
6+
7+
- Add a ``UUID`` field on the DiscoveredDependency model.
8+
Use the UUID for the DiscoveredDependency spdx_id for better SPDX compatibility.
9+
https://github.com/aboutcode-org/scancode.io/issues/1651
10+
411
v34.10.1 (2025-03-26)
512
---------------------
613

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 5.1.8 on 2025-04-16 06:49
2+
3+
import uuid
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('scanpipe', '0069_project_purl'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='discovereddependency',
16+
name='uuid',
17+
field=models.UUIDField(null=True, editable=False, verbose_name='UUID'),
18+
),
19+
]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated by Django 5.1.8 on 2025-04-16 06:57
2+
3+
import uuid
4+
from django.db import migrations
5+
6+
7+
def gen_uuid_bulk(apps, schema_editor):
8+
DiscoveredDependency = apps.get_model("scanpipe", "DiscoveredDependency")
9+
batch_size = 10000
10+
objs = []
11+
for obj in DiscoveredDependency.objects.filter(uuid__isnull=True).iterator():
12+
obj.uuid = uuid.uuid4()
13+
objs.append(obj)
14+
if len(objs) >= batch_size:
15+
DiscoveredDependency.objects.bulk_update(objs, ['uuid'])
16+
objs = []
17+
if objs:
18+
DiscoveredDependency.objects.bulk_update(objs, ['uuid'])
19+
20+
21+
class Migration(migrations.Migration):
22+
23+
dependencies = [
24+
('scanpipe', '0070_discovereddependency_uuid'),
25+
]
26+
27+
operations = [
28+
migrations.RunPython(gen_uuid_bulk, reverse_code=migrations.RunPython.noop),
29+
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 5.1.8 on 2025-04-16 07:00
2+
3+
import uuid
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('scanpipe', '0071_discovereddependency_uuid_populate'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='discovereddependency',
16+
name='uuid',
17+
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='UUID'),
18+
),
19+
]

scanpipe/models.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,18 @@ def short_uuid(self):
129129
return str(self.uuid)[0:8]
130130

131131

132+
class UUIDFieldMixin(models.Model):
133+
uuid = models.UUIDField(
134+
verbose_name=_("UUID"),
135+
default=uuid.uuid4,
136+
editable=False,
137+
unique=True,
138+
)
139+
140+
class Meta:
141+
abstract = True
142+
143+
132144
class HashFieldsMixin(models.Model):
133145
"""
134146
The hash fields are not indexed by default, use the `indexes` in Meta as needed:
@@ -3400,6 +3412,7 @@ class Meta:
34003412

34013413
class DiscoveredPackage(
34023414
ProjectRelatedModel,
3415+
UUIDFieldMixin,
34033416
ExtraDataFieldMixin,
34043417
SaveProjectMessageMixin,
34053418
UpdateFromDataMixin,
@@ -3421,9 +3434,6 @@ class DiscoveredPackage(
34213434

34223435
license_expression_field = "declared_license_expression"
34233436

3424-
uuid = models.UUIDField(
3425-
verbose_name=_("UUID"), default=uuid.uuid4, unique=True, editable=False
3426-
)
34273437
codebase_resources = models.ManyToManyField(
34283438
"CodebaseResource", related_name="discovered_packages"
34293439
)
@@ -3769,6 +3779,7 @@ def only_package_url_fields(self, extra=None):
37693779

37703780
class DiscoveredDependency(
37713781
ProjectRelatedModel,
3782+
UUIDFieldMixin,
37723783
SaveProjectMessageMixin,
37733784
UpdateFromDataMixin,
37743785
VulnerabilityMixin,
@@ -4031,7 +4042,10 @@ def populate_dependency_uuid(cls, dependency_data):
40314042

40324043
@property
40334044
def spdx_id(self):
4034-
return f"SPDXRef-scancodeio-{self._meta.model_name}-{self.dependency_uid}"
4045+
# We cannot rely on `dependency_uid` for the SPDX ID because it may contain
4046+
# PURL components that are not SPDX-compliant. According to the spec,
4047+
# "SPDXID is a unique string containing letters, numbers, ., and/or -"
4048+
return f"SPDXRef-scancodeio-{self._meta.model_name}-{self.uuid}"
40354049

40364050
def as_spdx(self):
40374051
"""Return this Dependency as an SPDX Package entry."""

scanpipe/tests/data/asgiref/asgiref-3.3.0.spdx.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
},
5353
{
5454
"name": "pytest",
55-
"SPDXID": "SPDXRef-scancodeio-discovereddependency-pkg:pypi/pytest?uuid=cfa26c80-95fc-4da3-a290-5e7403d0d9bc",
55+
"SPDXID": "SPDXRef-scancodeio-discovereddependency-13818fb7-6094-4868-97ca-384a8fc8d16d",
5656
"downloadLocation": "NOASSERTION",
5757
"licenseConcluded": "NOASSERTION",
5858
"copyrightText": "NOASSERTION",
@@ -68,7 +68,7 @@
6868
},
6969
{
7070
"name": "pytest",
71-
"SPDXID": "SPDXRef-scancodeio-discovereddependency-pkg:pypi/pytest?uuid=bfafc414-739f-4747-bfb0-1b3ad03d62c7",
71+
"SPDXID": "SPDXRef-scancodeio-discovereddependency-2f1d3742-0553-4c4f-8731-1ffbbc13827d",
7272
"downloadLocation": "NOASSERTION",
7373
"licenseConcluded": "NOASSERTION",
7474
"copyrightText": "NOASSERTION",
@@ -84,7 +84,7 @@
8484
},
8585
{
8686
"name": "pytest-asyncio",
87-
"SPDXID": "SPDXRef-scancodeio-discovereddependency-pkg:pypi/pytest-asyncio?uuid=68b8d3cb-eddb-4727-b6cb-707dde279301",
87+
"SPDXID": "SPDXRef-scancodeio-discovereddependency-fd5a81e5-0739-406e-9189-7b8a3644ef0d",
8888
"downloadLocation": "NOASSERTION",
8989
"licenseConcluded": "NOASSERTION",
9090
"copyrightText": "NOASSERTION",
@@ -100,7 +100,7 @@
100100
},
101101
{
102102
"name": "pytest-asyncio",
103-
"SPDXID": "SPDXRef-scancodeio-discovereddependency-pkg:pypi/pytest-asyncio?uuid=570878e1-aa7c-46bc-9216-122b73b34f9b",
103+
"SPDXID": "SPDXRef-scancodeio-discovereddependency-e175db55-d0f3-4224-b6d4-2b0ad553b865",
104104
"downloadLocation": "NOASSERTION",
105105
"licenseConcluded": "NOASSERTION",
106106
"copyrightText": "NOASSERTION",
@@ -118,30 +118,30 @@
118118
"documentDescribes": [
119119
"SPDXRef-scancodeio-discoveredpackage-101147dd-f8a7-4ea3-87a1-01b9b0af5d4f",
120120
"SPDXRef-scancodeio-discoveredpackage-b5035991-5b4b-40be-b68b-1c9c528078cd",
121-
"SPDXRef-scancodeio-discovereddependency-pkg:pypi/pytest?uuid=cfa26c80-95fc-4da3-a290-5e7403d0d9bc",
122-
"SPDXRef-scancodeio-discovereddependency-pkg:pypi/pytest?uuid=bfafc414-739f-4747-bfb0-1b3ad03d62c7",
123-
"SPDXRef-scancodeio-discovereddependency-pkg:pypi/pytest-asyncio?uuid=68b8d3cb-eddb-4727-b6cb-707dde279301",
124-
"SPDXRef-scancodeio-discovereddependency-pkg:pypi/pytest-asyncio?uuid=570878e1-aa7c-46bc-9216-122b73b34f9b"
121+
"SPDXRef-scancodeio-discovereddependency-13818fb7-6094-4868-97ca-384a8fc8d16d",
122+
"SPDXRef-scancodeio-discovereddependency-2f1d3742-0553-4c4f-8731-1ffbbc13827d",
123+
"SPDXRef-scancodeio-discovereddependency-fd5a81e5-0739-406e-9189-7b8a3644ef0d",
124+
"SPDXRef-scancodeio-discovereddependency-e175db55-d0f3-4224-b6d4-2b0ad553b865"
125125
],
126126
"files": [],
127127
"relationships": [
128128
{
129-
"spdxElementId": "SPDXRef-scancodeio-discovereddependency-pkg:pypi/pytest?uuid=cfa26c80-95fc-4da3-a290-5e7403d0d9bc",
129+
"spdxElementId": "SPDXRef-scancodeio-discovereddependency-13818fb7-6094-4868-97ca-384a8fc8d16d",
130130
"relatedSpdxElement": "SPDXRef-scancodeio-discoveredpackage-101147dd-f8a7-4ea3-87a1-01b9b0af5d4f",
131131
"relationshipType": "DEPENDENCY_OF"
132132
},
133133
{
134-
"spdxElementId": "SPDXRef-scancodeio-discovereddependency-pkg:pypi/pytest?uuid=bfafc414-739f-4747-bfb0-1b3ad03d62c7",
134+
"spdxElementId": "SPDXRef-scancodeio-discovereddependency-2f1d3742-0553-4c4f-8731-1ffbbc13827d",
135135
"relatedSpdxElement": "SPDXRef-scancodeio-discoveredpackage-b5035991-5b4b-40be-b68b-1c9c528078cd",
136136
"relationshipType": "DEPENDENCY_OF"
137137
},
138138
{
139-
"spdxElementId": "SPDXRef-scancodeio-discovereddependency-pkg:pypi/pytest-asyncio?uuid=68b8d3cb-eddb-4727-b6cb-707dde279301",
139+
"spdxElementId": "SPDXRef-scancodeio-discovereddependency-fd5a81e5-0739-406e-9189-7b8a3644ef0d",
140140
"relatedSpdxElement": "SPDXRef-scancodeio-discoveredpackage-101147dd-f8a7-4ea3-87a1-01b9b0af5d4f",
141141
"relationshipType": "DEPENDENCY_OF"
142142
},
143143
{
144-
"spdxElementId": "SPDXRef-scancodeio-discovereddependency-pkg:pypi/pytest-asyncio?uuid=570878e1-aa7c-46bc-9216-122b73b34f9b",
144+
"spdxElementId": "SPDXRef-scancodeio-discovereddependency-e175db55-d0f3-4224-b6d4-2b0ad553b865",
145145
"relatedSpdxElement": "SPDXRef-scancodeio-discoveredpackage-b5035991-5b4b-40be-b68b-1c9c528078cd",
146146
"relationshipType": "DEPENDENCY_OF"
147147
}

scanpipe/tests/data/asgiref/asgiref-3.3.0_fixtures.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1714,6 +1714,7 @@
17141714
"model": "scanpipe.discovereddependency",
17151715
"pk": 1,
17161716
"fields": {
1717+
"uuid": "13818fb7-6094-4868-97ca-384a8fc8d16d",
17171718
"type": "pypi",
17181719
"namespace": "",
17191720
"name": "pytest",
@@ -1739,6 +1740,7 @@
17391740
"model": "scanpipe.discovereddependency",
17401741
"pk": 2,
17411742
"fields": {
1743+
"uuid": "fd5a81e5-0739-406e-9189-7b8a3644ef0d",
17421744
"type": "pypi",
17431745
"namespace": "",
17441746
"name": "pytest-asyncio",
@@ -1764,6 +1766,7 @@
17641766
"model": "scanpipe.discovereddependency",
17651767
"pk": 3,
17661768
"fields": {
1769+
"uuid": "2f1d3742-0553-4c4f-8731-1ffbbc13827d",
17671770
"type": "pypi",
17681771
"namespace": "",
17691772
"name": "pytest",
@@ -1789,6 +1792,7 @@
17891792
"model": "scanpipe.discovereddependency",
17901793
"pk": 4,
17911794
"fields": {
1795+
"uuid": "e175db55-d0f3-4224-b6d4-2b0ad553b865",
17921796
"type": "pypi",
17931797
"namespace": "",
17941798
"name": "pytest-asyncio",

scanpipe/tests/test_models.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2429,6 +2429,11 @@ def test_scanpipe_discovered_package_model_compliance_alert(self):
24292429
# Reset the index value
24302430
scanpipe_app.license_policies_index = None
24312431

2432+
def test_scanpipe_discovered_package_model_spdx_id(self):
2433+
package1 = make_package(self.project1, "pkg:type/a")
2434+
expected = f"SPDXRef-scancodeio-discoveredpackage-{package1.uuid}"
2435+
self.assertEqual(expected, package1.spdx_id)
2436+
24322437
def test_scanpipe_model_create_user_creates_auth_token(self):
24332438
basic_user = User.objects.create_user(username="basic_user")
24342439
self.assertTrue(basic_user.auth_token.key)
@@ -2492,14 +2497,19 @@ def test_scanpipe_discovered_dependency_model_many_to_many(self):
24922497
self.assertEqual([], list(c.declared_dependencies.all()))
24932498
self.assertEqual([b_c], list(c.resolved_from_dependencies.all()))
24942499

2495-
def test_scanpipe_discovered_dependency_model_is_vulnerable_property(self):
2500+
def test_scanpipe_discovered_package_model_is_vulnerable_property(self):
24962501
package = DiscoveredPackage.create_from_data(self.project1, package_data1)
24972502
self.assertFalse(package.is_vulnerable)
24982503
package.update(
24992504
affected_by_vulnerabilities=[{"vulnerability_id": "VCID-cah8-awtr-aaad"}]
25002505
)
25012506
self.assertTrue(package.is_vulnerable)
25022507

2508+
def test_scanpipe_discovered_dependency_model_spdx_id(self):
2509+
dependency1 = make_dependency(self.project1)
2510+
expected = f"SPDXRef-scancodeio-discovereddependency-{dependency1.uuid}"
2511+
self.assertEqual(expected, dependency1.spdx_id)
2512+
25032513
def test_scanpipe_package_model_integrity_with_toolkit_package_model(self):
25042514
scanpipe_only_fields = [
25052515
"id",

0 commit comments

Comments
 (0)