Skip to content

Commit

Permalink
Add children_packages m2m and rename resolved_to_package #1066 (#1252)
Browse files Browse the repository at this point in the history
Signed-off-by: tdruez <tdruez@nexb.com>
  • Loading branch information
tdruez authored Jun 4, 2024
1 parent 6759958 commit 49f9c07
Show file tree
Hide file tree
Showing 14 changed files with 197 additions and 48 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ v34.6.0 (unreleased)
Project error message.
https://github.com/nexB/scancode.io/issues/1249

- Rename DiscoveredDependency ``resolved_to`` to ``resolved_to_package``, and
``resolved_dependencies`` to ``resolved_from_dependencies`` for clarity and
consistency.
Add ``children_packages`` and ``parent_packages`` ManyToMany field on the
DiscoveredPackage model.
Add full dependency tree in the CycloneDX output.
https://github.com/nexB/scancode.io/issues/1066

v34.5.0 (2024-05-22)
--------------------

Expand Down
4 changes: 2 additions & 2 deletions scanpipe/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ class Meta:
class DiscoveredDependencySerializer(serializers.ModelSerializer):
purl = serializers.ReadOnlyField()
for_package_uid = serializers.ReadOnlyField()
resolved_to_uid = serializers.ReadOnlyField()
resolved_to_package_uid = serializers.ReadOnlyField()
datafile_path = serializers.ReadOnlyField()
package_type = serializers.ReadOnlyField(source="type")

Expand All @@ -407,7 +407,7 @@ class Meta:
"is_resolved",
"dependency_uid",
"for_package_uid",
"resolved_to_uid",
"resolved_to_package_uid",
"datafile_path",
"datasource_id",
"package_type",
Expand Down
2 changes: 1 addition & 1 deletion scanpipe/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,7 @@ class DependencyFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
"is_optional",
"is_resolved",
"for_package",
"resolved_to",
"resolved_to_package",
"datafile_resource",
"datasource_id",
],
Expand Down
41 changes: 41 additions & 0 deletions scanpipe/migrations/0060_discovereddependency_renames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Generated by Django 5.0.6 on 2024-06-03 11:32

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("scanpipe", "0059_alter_codebaseresource_status"),
]

operations = [
migrations.RenameField(
model_name="discovereddependency",
old_name="resolved_to",
new_name="resolved_to_package",
),
migrations.AlterField(
model_name="discovereddependency",
name="resolved_to_package",
field=models.ForeignKey(
blank=True,
editable=False,
help_text="The resolved package for this dependency. If empty, it indicates the dependency is unresolved.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="resolved_from_dependencies",
to="scanpipe.discoveredpackage",
),
),
migrations.AddField(
model_name="discoveredpackage",
name="children_packages",
field=models.ManyToManyField(
related_name="parent_packages",
through="scanpipe.DiscoveredDependency",
to="scanpipe.discoveredpackage",
),
),
]
41 changes: 28 additions & 13 deletions scanpipe/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3034,6 +3034,13 @@ class DiscoveredPackage(
codebase_resources = models.ManyToManyField(
"CodebaseResource", related_name="discovered_packages"
)
children_packages = models.ManyToManyField(
"self",
through="DiscoveredDependency",
symmetrical=False,
related_name="parent_packages",
through_fields=("for_package", "resolved_to_package"),
)
missing_resources = models.JSONField(default=list, blank=True)
modified_resources = models.JSONField(default=list, blank=True)
package_uid = models.CharField(
Expand Down Expand Up @@ -3232,6 +3239,15 @@ def as_spdx(self):
external_refs=external_refs,
)

@property
def cyclonedx_bom_ref(self):
"""
Use the package_uid when available to ensure having unique bom_ref
in the SBOM when several instances of the same DiscoveredPackage
(i.e. same purl) are present in the project.
"""
return self.package_uid or str(self.get_package_url())

def as_cyclonedx(self):
"""Return this DiscoveredPackage as an CycloneDX Component entry."""
licenses = []
Expand Down Expand Up @@ -3298,17 +3314,12 @@ def as_cyclonedx(self):
],
)

package_url = self.get_package_url()
# Use the package_uid when available to ensure having unique bom_ref
# in the SBOM when several instances of the same DiscoveredPackage
# (i.e. same purl) are present in the project.
bom_ref = self.package_uid or str(package_url)

return cyclonedx_component.Component(
name=self.name,
version=self.version,
bom_ref=bom_ref,
purl=package_url, # Warning: Use the real purl and not package_uid here.
bom_ref=self.cyclonedx_bom_ref,
# Warning: Use the real purl and not package_uid here.
purl=self.get_package_url(),
licenses=licenses,
copyright=self.copyright,
description=self.description,
Expand All @@ -3332,6 +3343,10 @@ def prefetch_for_serializer(self):
Prefetch(
"for_package", queryset=DiscoveredPackage.objects.only("package_uid")
),
Prefetch(
"resolved_to_package",
queryset=DiscoveredPackage.objects.only("package_uid"),
),
Prefetch(
"datafile_resource", queryset=CodebaseResource.objects.only("path")
),
Expand Down Expand Up @@ -3373,9 +3388,9 @@ class DiscoveredDependency(
blank=True,
null=True,
)
resolved_to = models.ForeignKey(
resolved_to_package = models.ForeignKey(
DiscoveredPackage,
related_name="resolved_dependencies",
related_name="resolved_from_dependencies",
help_text=_(
"The resolved package for this dependency. "
"If empty, it indicates the dependency is unresolved."
Expand Down Expand Up @@ -3468,9 +3483,9 @@ def for_package_uid(self):
return self.for_package.package_uid

@cached_property
def resolved_to_uid(self):
if self.resolved_to:
return self.resolved_to.package_uid
def resolved_to_package_uid(self):
if self.resolved_to_package:
return self.resolved_to_package.package_uid

@cached_property
def datafile_path(self):
Expand Down
33 changes: 23 additions & 10 deletions scanpipe/pipes/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -675,23 +675,36 @@ def get_cyclonedx_bom(project):
],
)

components = []
vulnerabilities = []
for package in get_queryset(project, "discoveredpackage"):
dependencies = {}

package_qs = get_queryset(project, "discoveredpackage")
package_qs = package_qs.prefetch_related("children_packages")

for package in package_qs:
component = package.as_cyclonedx()
components.append(component)
bom.components.add(component)
bom.register_dependency(project_as_root_component, [component])

# Store the component dependencies to be added later since all components need
# to be added on the BOM first.
dependencies[component] = [
package.cyclonedx_bom_ref for package in package.children_packages.all()
]

for vulnerability_data in package.affected_by_vulnerabilities:
vulnerabilities.append(
vulnerability_as_cyclonedx(
vulnerability_data=vulnerability_data,
component_bom_ref=component.bom_ref,
)
vulnerability_as_cyclonedx(vulnerability_data, component.bom_ref)
)

for component in components:
bom.components.add(component)
bom.register_dependency(project_as_root_component, [component])
for component, depends_on_bom_refs in dependencies.items():
if not depends_on_bom_refs:
continue
# Craft disposable Component instances for registering dependencies
dependencies = [
cdx_component.Component(name="", bom_ref=ref) for ref in depends_on_bom_refs
]
bom.register_dependency(component, dependencies)

bom.vulnerabilities = vulnerabilities

Expand Down
4 changes: 2 additions & 2 deletions scanpipe/templates/scanpipe/dependency_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@
{% endif %}
</td>
<td>
{% if dependency.resolved_to %}
{% if dependency.resolved_to_package %}
{# CAUTION: Avoid relying on get_absolute_url to prevent unnecessary query triggers #}
<a href="{% url 'package_detail' project.slug dependency.for_package.uuid %}" title="{{ dependency.resolved_to.purl }}">{{ dependency.resolved_to.purl }}</a>
<a href="{% url 'package_detail' project.slug dependency.resolved_to_package.uuid %}" title="{{ dependency.resolved_to_package.purl }}">{{ dependency.resolved_to_package.purl }}</a>
{% endif %}
</td>
<td>
Expand Down
13 changes: 13 additions & 0 deletions scanpipe/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
from django.apps import apps

from scanpipe.models import CodebaseResource
from scanpipe.models import DiscoveredDependency
from scanpipe.models import DiscoveredPackage
from scanpipe.tests.pipelines.do_nothing import DoNothing
from scanpipe.tests.pipelines.profile_step import ProfileStep
from scanpipe.tests.pipelines.raise_exception import RaiseException
Expand Down Expand Up @@ -65,6 +67,17 @@ def make_resource_directory(project, path, **extra):
)


def make_package(project, package_url, **extra):
package = DiscoveredPackage(project=project, **extra)
package.set_package_url(package_url)
package.save()
return package


def make_dependency(project, **extra):
return DiscoveredDependency.objects.create(project=project, **extra)


resource_data1 = {
"path": "notice.NOTICE",
"type": "file",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@
"is_resolved": false,
"dependency_uid": "pkg:pypi/pytest?uuid=fixed-uid-done-for-testing-5642512d1758",
"for_package_uid": "pkg:pypi/asgiref@3.3.0?uuid=fixed-uid-done-for-testing-5642512d1758",
"resolved_to_uid": null,
"resolved_to_package_uid": null,
"datafile_path": "asgiref-3.3.0-py3-none-any.whl",
"datasource_id": "pypi_wheel",
"package_type": "pypi",
Expand All @@ -292,7 +292,7 @@
"is_resolved": false,
"dependency_uid": "pkg:pypi/pytest?uuid=fixed-uid-done-for-testing-5642512d1758",
"for_package_uid": "pkg:pypi/asgiref@3.3.0?uuid=fixed-uid-done-for-testing-5642512d1758",
"resolved_to_uid": null,
"resolved_to_package_uid": null,
"datafile_path": "asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info/METADATA",
"datasource_id": "pypi_wheel_metadata",
"package_type": "pypi",
Expand All @@ -307,7 +307,7 @@
"is_resolved": false,
"dependency_uid": "pkg:pypi/pytest-asyncio?uuid=fixed-uid-done-for-testing-5642512d1758",
"for_package_uid": "pkg:pypi/asgiref@3.3.0?uuid=fixed-uid-done-for-testing-5642512d1758",
"resolved_to_uid": null,
"resolved_to_package_uid": null,
"datafile_path": "asgiref-3.3.0-py3-none-any.whl",
"datasource_id": "pypi_wheel",
"package_type": "pypi",
Expand All @@ -322,7 +322,7 @@
"is_resolved": false,
"dependency_uid": "pkg:pypi/pytest-asyncio?uuid=fixed-uid-done-for-testing-5642512d1758",
"for_package_uid": "pkg:pypi/asgiref@3.3.0?uuid=fixed-uid-done-for-testing-5642512d1758",
"resolved_to_uid": null,
"resolved_to_package_uid": null,
"datafile_path": "asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info/METADATA",
"datasource_id": "pypi_wheel_metadata",
"package_type": "pypi",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@
"is_resolved": false,
"dependency_uid": "pkg:pypi/dask?uuid=fixed-uid-done-for-testing-5642512d1758",
"for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758",
"resolved_to_uid": null,
"resolved_to_package_uid": null,
"datafile_path": "daglib-0.6.0-py3-none-any.whl",
"datasource_id": "pypi_wheel",
"package_type": "pypi",
Expand All @@ -272,7 +272,7 @@
"is_resolved": false,
"dependency_uid": "pkg:pypi/dask?uuid=fixed-uid-done-for-testing-5642512d1758",
"for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758",
"resolved_to_uid": null,
"resolved_to_package_uid": null,
"datafile_path": "daglib-0.6.0-py3-none-any.whl-extract/daglib-0.6.0.dist-info/METADATA",
"datasource_id": "pypi_wheel_metadata",
"package_type": "pypi",
Expand All @@ -287,7 +287,7 @@
"is_resolved": false,
"dependency_uid": "pkg:pypi/graphviz?uuid=fixed-uid-done-for-testing-5642512d1758",
"for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758",
"resolved_to_uid": null,
"resolved_to_package_uid": null,
"datafile_path": "daglib-0.6.0-py3-none-any.whl",
"datasource_id": "pypi_wheel",
"package_type": "pypi",
Expand All @@ -302,7 +302,7 @@
"is_resolved": false,
"dependency_uid": "pkg:pypi/graphviz?uuid=fixed-uid-done-for-testing-5642512d1758",
"for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758",
"resolved_to_uid": null,
"resolved_to_package_uid": null,
"datafile_path": "daglib-0.6.0-py3-none-any.whl-extract/daglib-0.6.0.dist-info/METADATA",
"datasource_id": "pypi_wheel_metadata",
"package_type": "pypi",
Expand All @@ -317,7 +317,7 @@
"is_resolved": false,
"dependency_uid": "pkg:pypi/ipycytoscape?uuid=fixed-uid-done-for-testing-5642512d1758",
"for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758",
"resolved_to_uid": null,
"resolved_to_package_uid": null,
"datafile_path": "daglib-0.6.0-py3-none-any.whl",
"datasource_id": "pypi_wheel",
"package_type": "pypi",
Expand All @@ -332,7 +332,7 @@
"is_resolved": false,
"dependency_uid": "pkg:pypi/ipycytoscape?uuid=fixed-uid-done-for-testing-5642512d1758",
"for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758",
"resolved_to_uid": null,
"resolved_to_package_uid": null,
"datafile_path": "daglib-0.6.0-py3-none-any.whl-extract/daglib-0.6.0.dist-info/METADATA",
"datasource_id": "pypi_wheel_metadata",
"package_type": "pypi",
Expand All @@ -347,7 +347,7 @@
"is_resolved": false,
"dependency_uid": "pkg:pypi/networkx?uuid=fixed-uid-done-for-testing-5642512d1758",
"for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758",
"resolved_to_uid": null,
"resolved_to_package_uid": null,
"datafile_path": "daglib-0.6.0-py3-none-any.whl",
"datasource_id": "pypi_wheel",
"package_type": "pypi",
Expand All @@ -362,7 +362,7 @@
"is_resolved": false,
"dependency_uid": "pkg:pypi/networkx?uuid=fixed-uid-done-for-testing-5642512d1758",
"for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758",
"resolved_to_uid": null,
"resolved_to_package_uid": null,
"datafile_path": "daglib-0.6.0-py3-none-any.whl-extract/daglib-0.6.0.dist-info/METADATA",
"datasource_id": "pypi_wheel_metadata",
"package_type": "pypi",
Expand Down
2 changes: 1 addition & 1 deletion scanpipe/tests/data/is-npm-1.0.0_scan_codebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@
"is_resolved": false,
"dependency_uid": "pkg:npm/ava?uuid=fixed-uid-done-for-testing-5642512d1758",
"for_package_uid": "pkg:npm/is-npm@1.0.0?uuid=fixed-uid-done-for-testing-5642512d1758",
"resolved_to_uid": null,
"resolved_to_package_uid": null,
"datafile_path": "is-npm-1.0.0.tgz-extract/package/package.json",
"datasource_id": "npm_package_json",
"package_type": "npm",
Expand Down
Loading

0 comments on commit 49f9c07

Please sign in to comment.