Skip to content

Commit 49f9c07

Browse files
authored
Add children_packages m2m and rename resolved_to_package #1066 (#1252)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 6759958 commit 49f9c07

14 files changed

+197
-48
lines changed

CHANGELOG.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ v34.6.0 (unreleased)
1717
Project error message.
1818
https://github.com/nexB/scancode.io/issues/1249
1919

20+
- Rename DiscoveredDependency ``resolved_to`` to ``resolved_to_package``, and
21+
``resolved_dependencies`` to ``resolved_from_dependencies`` for clarity and
22+
consistency.
23+
Add ``children_packages`` and ``parent_packages`` ManyToMany field on the
24+
DiscoveredPackage model.
25+
Add full dependency tree in the CycloneDX output.
26+
https://github.com/nexB/scancode.io/issues/1066
27+
2028
v34.5.0 (2024-05-22)
2129
--------------------
2230

scanpipe/api/serializers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ class Meta:
392392
class DiscoveredDependencySerializer(serializers.ModelSerializer):
393393
purl = serializers.ReadOnlyField()
394394
for_package_uid = serializers.ReadOnlyField()
395-
resolved_to_uid = serializers.ReadOnlyField()
395+
resolved_to_package_uid = serializers.ReadOnlyField()
396396
datafile_path = serializers.ReadOnlyField()
397397
package_type = serializers.ReadOnlyField(source="type")
398398

@@ -407,7 +407,7 @@ class Meta:
407407
"is_resolved",
408408
"dependency_uid",
409409
"for_package_uid",
410-
"resolved_to_uid",
410+
"resolved_to_package_uid",
411411
"datafile_path",
412412
"datasource_id",
413413
"package_type",

scanpipe/filters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,7 @@ class DependencyFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
748748
"is_optional",
749749
"is_resolved",
750750
"for_package",
751-
"resolved_to",
751+
"resolved_to_package",
752752
"datafile_resource",
753753
"datasource_id",
754754
],
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Generated by Django 5.0.6 on 2024-06-03 11:32
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+
("scanpipe", "0059_alter_codebaseresource_status"),
11+
]
12+
13+
operations = [
14+
migrations.RenameField(
15+
model_name="discovereddependency",
16+
old_name="resolved_to",
17+
new_name="resolved_to_package",
18+
),
19+
migrations.AlterField(
20+
model_name="discovereddependency",
21+
name="resolved_to_package",
22+
field=models.ForeignKey(
23+
blank=True,
24+
editable=False,
25+
help_text="The resolved package for this dependency. If empty, it indicates the dependency is unresolved.",
26+
null=True,
27+
on_delete=django.db.models.deletion.SET_NULL,
28+
related_name="resolved_from_dependencies",
29+
to="scanpipe.discoveredpackage",
30+
),
31+
),
32+
migrations.AddField(
33+
model_name="discoveredpackage",
34+
name="children_packages",
35+
field=models.ManyToManyField(
36+
related_name="parent_packages",
37+
through="scanpipe.DiscoveredDependency",
38+
to="scanpipe.discoveredpackage",
39+
),
40+
),
41+
]

scanpipe/models.py

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3034,6 +3034,13 @@ class DiscoveredPackage(
30343034
codebase_resources = models.ManyToManyField(
30353035
"CodebaseResource", related_name="discovered_packages"
30363036
)
3037+
children_packages = models.ManyToManyField(
3038+
"self",
3039+
through="DiscoveredDependency",
3040+
symmetrical=False,
3041+
related_name="parent_packages",
3042+
through_fields=("for_package", "resolved_to_package"),
3043+
)
30373044
missing_resources = models.JSONField(default=list, blank=True)
30383045
modified_resources = models.JSONField(default=list, blank=True)
30393046
package_uid = models.CharField(
@@ -3232,6 +3239,15 @@ def as_spdx(self):
32323239
external_refs=external_refs,
32333240
)
32343241

3242+
@property
3243+
def cyclonedx_bom_ref(self):
3244+
"""
3245+
Use the package_uid when available to ensure having unique bom_ref
3246+
in the SBOM when several instances of the same DiscoveredPackage
3247+
(i.e. same purl) are present in the project.
3248+
"""
3249+
return self.package_uid or str(self.get_package_url())
3250+
32353251
def as_cyclonedx(self):
32363252
"""Return this DiscoveredPackage as an CycloneDX Component entry."""
32373253
licenses = []
@@ -3298,17 +3314,12 @@ def as_cyclonedx(self):
32983314
],
32993315
)
33003316

3301-
package_url = self.get_package_url()
3302-
# Use the package_uid when available to ensure having unique bom_ref
3303-
# in the SBOM when several instances of the same DiscoveredPackage
3304-
# (i.e. same purl) are present in the project.
3305-
bom_ref = self.package_uid or str(package_url)
3306-
33073317
return cyclonedx_component.Component(
33083318
name=self.name,
33093319
version=self.version,
3310-
bom_ref=bom_ref,
3311-
purl=package_url, # Warning: Use the real purl and not package_uid here.
3320+
bom_ref=self.cyclonedx_bom_ref,
3321+
# Warning: Use the real purl and not package_uid here.
3322+
purl=self.get_package_url(),
33123323
licenses=licenses,
33133324
copyright=self.copyright,
33143325
description=self.description,
@@ -3332,6 +3343,10 @@ def prefetch_for_serializer(self):
33323343
Prefetch(
33333344
"for_package", queryset=DiscoveredPackage.objects.only("package_uid")
33343345
),
3346+
Prefetch(
3347+
"resolved_to_package",
3348+
queryset=DiscoveredPackage.objects.only("package_uid"),
3349+
),
33353350
Prefetch(
33363351
"datafile_resource", queryset=CodebaseResource.objects.only("path")
33373352
),
@@ -3373,9 +3388,9 @@ class DiscoveredDependency(
33733388
blank=True,
33743389
null=True,
33753390
)
3376-
resolved_to = models.ForeignKey(
3391+
resolved_to_package = models.ForeignKey(
33773392
DiscoveredPackage,
3378-
related_name="resolved_dependencies",
3393+
related_name="resolved_from_dependencies",
33793394
help_text=_(
33803395
"The resolved package for this dependency. "
33813396
"If empty, it indicates the dependency is unresolved."
@@ -3468,9 +3483,9 @@ def for_package_uid(self):
34683483
return self.for_package.package_uid
34693484

34703485
@cached_property
3471-
def resolved_to_uid(self):
3472-
if self.resolved_to:
3473-
return self.resolved_to.package_uid
3486+
def resolved_to_package_uid(self):
3487+
if self.resolved_to_package:
3488+
return self.resolved_to_package.package_uid
34743489

34753490
@cached_property
34763491
def datafile_path(self):

scanpipe/pipes/output.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -675,23 +675,36 @@ def get_cyclonedx_bom(project):
675675
],
676676
)
677677

678-
components = []
679678
vulnerabilities = []
680-
for package in get_queryset(project, "discoveredpackage"):
679+
dependencies = {}
680+
681+
package_qs = get_queryset(project, "discoveredpackage")
682+
package_qs = package_qs.prefetch_related("children_packages")
683+
684+
for package in package_qs:
681685
component = package.as_cyclonedx()
682-
components.append(component)
686+
bom.components.add(component)
687+
bom.register_dependency(project_as_root_component, [component])
688+
689+
# Store the component dependencies to be added later since all components need
690+
# to be added on the BOM first.
691+
dependencies[component] = [
692+
package.cyclonedx_bom_ref for package in package.children_packages.all()
693+
]
683694

684695
for vulnerability_data in package.affected_by_vulnerabilities:
685696
vulnerabilities.append(
686-
vulnerability_as_cyclonedx(
687-
vulnerability_data=vulnerability_data,
688-
component_bom_ref=component.bom_ref,
689-
)
697+
vulnerability_as_cyclonedx(vulnerability_data, component.bom_ref)
690698
)
691699

692-
for component in components:
693-
bom.components.add(component)
694-
bom.register_dependency(project_as_root_component, [component])
700+
for component, depends_on_bom_refs in dependencies.items():
701+
if not depends_on_bom_refs:
702+
continue
703+
# Craft disposable Component instances for registering dependencies
704+
dependencies = [
705+
cdx_component.Component(name="", bom_ref=ref) for ref in depends_on_bom_refs
706+
]
707+
bom.register_dependency(component, dependencies)
695708

696709
bom.vulnerabilities = vulnerabilities
697710

scanpipe/templates/scanpipe/dependency_list.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@
5555
{% endif %}
5656
</td>
5757
<td>
58-
{% if dependency.resolved_to %}
58+
{% if dependency.resolved_to_package %}
5959
{# CAUTION: Avoid relying on get_absolute_url to prevent unnecessary query triggers #}
60-
<a href="{% url 'package_detail' project.slug dependency.for_package.uuid %}" title="{{ dependency.resolved_to.purl }}">{{ dependency.resolved_to.purl }}</a>
60+
<a href="{% url 'package_detail' project.slug dependency.resolved_to_package.uuid %}" title="{{ dependency.resolved_to_package.purl }}">{{ dependency.resolved_to_package.purl }}</a>
6161
{% endif %}
6262
</td>
6363
<td>

scanpipe/tests/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
from django.apps import apps
2828

2929
from scanpipe.models import CodebaseResource
30+
from scanpipe.models import DiscoveredDependency
31+
from scanpipe.models import DiscoveredPackage
3032
from scanpipe.tests.pipelines.do_nothing import DoNothing
3133
from scanpipe.tests.pipelines.profile_step import ProfileStep
3234
from scanpipe.tests.pipelines.raise_exception import RaiseException
@@ -65,6 +67,17 @@ def make_resource_directory(project, path, **extra):
6567
)
6668

6769

70+
def make_package(project, package_url, **extra):
71+
package = DiscoveredPackage(project=project, **extra)
72+
package.set_package_url(package_url)
73+
package.save()
74+
return package
75+
76+
77+
def make_dependency(project, **extra):
78+
return DiscoveredDependency.objects.create(project=project, **extra)
79+
80+
6881
resource_data1 = {
6982
"path": "notice.NOTICE",
7083
"type": "file",

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@
277277
"is_resolved": false,
278278
"dependency_uid": "pkg:pypi/pytest?uuid=fixed-uid-done-for-testing-5642512d1758",
279279
"for_package_uid": "pkg:pypi/asgiref@3.3.0?uuid=fixed-uid-done-for-testing-5642512d1758",
280-
"resolved_to_uid": null,
280+
"resolved_to_package_uid": null,
281281
"datafile_path": "asgiref-3.3.0-py3-none-any.whl",
282282
"datasource_id": "pypi_wheel",
283283
"package_type": "pypi",
@@ -292,7 +292,7 @@
292292
"is_resolved": false,
293293
"dependency_uid": "pkg:pypi/pytest?uuid=fixed-uid-done-for-testing-5642512d1758",
294294
"for_package_uid": "pkg:pypi/asgiref@3.3.0?uuid=fixed-uid-done-for-testing-5642512d1758",
295-
"resolved_to_uid": null,
295+
"resolved_to_package_uid": null,
296296
"datafile_path": "asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info/METADATA",
297297
"datasource_id": "pypi_wheel_metadata",
298298
"package_type": "pypi",
@@ -307,7 +307,7 @@
307307
"is_resolved": false,
308308
"dependency_uid": "pkg:pypi/pytest-asyncio?uuid=fixed-uid-done-for-testing-5642512d1758",
309309
"for_package_uid": "pkg:pypi/asgiref@3.3.0?uuid=fixed-uid-done-for-testing-5642512d1758",
310-
"resolved_to_uid": null,
310+
"resolved_to_package_uid": null,
311311
"datafile_path": "asgiref-3.3.0-py3-none-any.whl",
312312
"datasource_id": "pypi_wheel",
313313
"package_type": "pypi",
@@ -322,7 +322,7 @@
322322
"is_resolved": false,
323323
"dependency_uid": "pkg:pypi/pytest-asyncio?uuid=fixed-uid-done-for-testing-5642512d1758",
324324
"for_package_uid": "pkg:pypi/asgiref@3.3.0?uuid=fixed-uid-done-for-testing-5642512d1758",
325-
"resolved_to_uid": null,
325+
"resolved_to_package_uid": null,
326326
"datafile_path": "asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info/METADATA",
327327
"datasource_id": "pypi_wheel_metadata",
328328
"package_type": "pypi",

scanpipe/tests/data/daglib-0.6.0-py3-none-any.whl_scan_codebase.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@
257257
"is_resolved": false,
258258
"dependency_uid": "pkg:pypi/dask?uuid=fixed-uid-done-for-testing-5642512d1758",
259259
"for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758",
260-
"resolved_to_uid": null,
260+
"resolved_to_package_uid": null,
261261
"datafile_path": "daglib-0.6.0-py3-none-any.whl",
262262
"datasource_id": "pypi_wheel",
263263
"package_type": "pypi",
@@ -272,7 +272,7 @@
272272
"is_resolved": false,
273273
"dependency_uid": "pkg:pypi/dask?uuid=fixed-uid-done-for-testing-5642512d1758",
274274
"for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758",
275-
"resolved_to_uid": null,
275+
"resolved_to_package_uid": null,
276276
"datafile_path": "daglib-0.6.0-py3-none-any.whl-extract/daglib-0.6.0.dist-info/METADATA",
277277
"datasource_id": "pypi_wheel_metadata",
278278
"package_type": "pypi",
@@ -287,7 +287,7 @@
287287
"is_resolved": false,
288288
"dependency_uid": "pkg:pypi/graphviz?uuid=fixed-uid-done-for-testing-5642512d1758",
289289
"for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758",
290-
"resolved_to_uid": null,
290+
"resolved_to_package_uid": null,
291291
"datafile_path": "daglib-0.6.0-py3-none-any.whl",
292292
"datasource_id": "pypi_wheel",
293293
"package_type": "pypi",
@@ -302,7 +302,7 @@
302302
"is_resolved": false,
303303
"dependency_uid": "pkg:pypi/graphviz?uuid=fixed-uid-done-for-testing-5642512d1758",
304304
"for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758",
305-
"resolved_to_uid": null,
305+
"resolved_to_package_uid": null,
306306
"datafile_path": "daglib-0.6.0-py3-none-any.whl-extract/daglib-0.6.0.dist-info/METADATA",
307307
"datasource_id": "pypi_wheel_metadata",
308308
"package_type": "pypi",
@@ -317,7 +317,7 @@
317317
"is_resolved": false,
318318
"dependency_uid": "pkg:pypi/ipycytoscape?uuid=fixed-uid-done-for-testing-5642512d1758",
319319
"for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758",
320-
"resolved_to_uid": null,
320+
"resolved_to_package_uid": null,
321321
"datafile_path": "daglib-0.6.0-py3-none-any.whl",
322322
"datasource_id": "pypi_wheel",
323323
"package_type": "pypi",
@@ -332,7 +332,7 @@
332332
"is_resolved": false,
333333
"dependency_uid": "pkg:pypi/ipycytoscape?uuid=fixed-uid-done-for-testing-5642512d1758",
334334
"for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758",
335-
"resolved_to_uid": null,
335+
"resolved_to_package_uid": null,
336336
"datafile_path": "daglib-0.6.0-py3-none-any.whl-extract/daglib-0.6.0.dist-info/METADATA",
337337
"datasource_id": "pypi_wheel_metadata",
338338
"package_type": "pypi",
@@ -347,7 +347,7 @@
347347
"is_resolved": false,
348348
"dependency_uid": "pkg:pypi/networkx?uuid=fixed-uid-done-for-testing-5642512d1758",
349349
"for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758",
350-
"resolved_to_uid": null,
350+
"resolved_to_package_uid": null,
351351
"datafile_path": "daglib-0.6.0-py3-none-any.whl",
352352
"datasource_id": "pypi_wheel",
353353
"package_type": "pypi",
@@ -362,7 +362,7 @@
362362
"is_resolved": false,
363363
"dependency_uid": "pkg:pypi/networkx?uuid=fixed-uid-done-for-testing-5642512d1758",
364364
"for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758",
365-
"resolved_to_uid": null,
365+
"resolved_to_package_uid": null,
366366
"datafile_path": "daglib-0.6.0-py3-none-any.whl-extract/daglib-0.6.0.dist-info/METADATA",
367367
"datasource_id": "pypi_wheel_metadata",
368368
"package_type": "pypi",

0 commit comments

Comments
 (0)