Skip to content

Commit 6c26874

Browse files
JonoYangtdruez
andauthored
Implement DiscoveredDependency model (#486)
* Implement package assembly in scancode.io #447 Signed-off-by: Jono Yang <jyang@nexb.com> * Minor formatting changes for consistency #447 Signed-off-by: Thomas Druez <tdruez@nexb.com> * Create DiscoveredPackages before other models #447 Signed-off-by: Jono Yang <jyang@nexb.com> * Revert "Create DiscoveredPackages before other models #447" This reverts commit c9b8bed. Sorting Packages, Dependencies, and Resources from DatafileHandler.assemble() will never work. The code needs to be changed in scancode-toolkit. Signed-off-by: Jono Yang <jyang@nexb.com> * Update migration #444 Signed-off-by: Jono Yang <jyang@nexb.com> * Return package_uids in for_packages #444 * This is so we are consistent with scancode-toolkit JSON output * Update expected test results Signed-off-by: Jono Yang <jyang@nexb.com> * Create directory Resources in docker pipeline #485 * Update test expectations Signed-off-by: Jono Yang <jyang@nexb.com> * Implement package assembly in scancode.io #447 Signed-off-by: Jono Yang <jyang@nexb.com> * Implement package assembly in scancode.io #447 Signed-off-by: Jono Yang <jyang@nexb.com> * Create DiscoveredDependency model #447 * Create new dependency list and detail views * Update assemble_packages() to create DiscoveredDependencies * Update test expectations Signed-off-by: Jono Yang <jyang@nexb.com> * Update fields on DiscoveredDependency #447 * Remove for_package_uid and replace with ForeignKey for_package * Remove datafile_path and replace with ForeignKey datafile_resource * Create properties for the two removed fields * Update dependency views to link to datafile_resource * Update expected test results Signed-off-by: Jono Yang <jyang@nexb.com> * Properly pluralize verbose name #447 Signed-off-by: Jono Yang <jyang@nexb.com> * Create new argument for create_from_data #447 * Add strip_datafile_path_root to DiscoveredDependency.create_from_data * This argument strips the root path segment from `datafile_path` before using the path to look up the corresponding CodebaseResource * This is used in the case where we are importing a scan from scancode-toolkit, where the root path segments are not stripped by default * Update expected test results Signed-off-by: Jono Yang <jyang@nexb.com> * Update prefetch_related #447 * Used cached_property for DiscoveredDependency properties Signed-off-by: Jono Yang <jyang@nexb.com> * Prefetch related models in output code #447 Signed-off-by: Jono Yang <jyang@nexb.com> * Import scancode.io 30.2.0 scans in load_codebase * Order DiscoveredDependencies by is_runtime, is_optional, is_resolved, and dependency_uid * Do not show dependency_uid value in DiscoveredDependency list view Signed-off-by: Jono Yang <jyang@nexb.com> * Revert changes for importing old scancode.io scans Signed-off-by: Jono Yang <jyang@nexb.com> * Regen migrations for DiscoveredDependency #447 Signed-off-by: Jono Yang <jyang@nexb.com> * Migrate DiscoveredPackage.dependencies #447 * Create migrations to generate new DiscoveredDependency objects from DiscoveredPackage.dependencies before removing the dependencies field Signed-off-by: Jono Yang <jyang@nexb.com> * Update test expectations #447 Signed-off-by: Jono Yang <jyang@nexb.com> * Remove accidentally committed files #447 Signed-off-by: Jono Yang <jyang@nexb.com> * Update migration logic #447 * Remove unnecessary else from DiscoveredDependency properties Signed-off-by: Jono Yang <jyang@nexb.com> * Add PackageURLMixin to DiscoveredDependency #447 Signed-off-by: Jono Yang <jyang@nexb.com> * Set DiscoveredDependencies purl fields #447 * Create migration that populates purl fields for existing DiscoveredDependencies Signed-off-by: Jono Yang <jyang@nexb.com> * Store purl values in purl fields #447 * Do not store dependency_uid in purl fields Signed-off-by: Jono Yang <jyang@nexb.com> * Remove purl field from DiscoveredDependency #447 * We are already storing this info in the purl fields * Create purl property on DiscoveredDependency for compatibility Signed-off-by: Jono Yang <jyang@nexb.com> * Update DependencyFilterSet #447 * Add search and purl fields Signed-off-by: Jono Yang <jyang@nexb.com> * Don't show DiscoveredDependencies purl fields #447 Signed-off-by: Jono Yang <jyang@nexb.com> * Update package detail view dependencies tab #447 Signed-off-by: Jono Yang <jyang@nexb.com> * Add package_type to dependency serializer #511 * Update test expectations Signed-off-by: Jono Yang <jyang@nexb.com> * Update expected test results Signed-off-by: Jono Yang <jyang@nexb.com> * Add dependency table column #447 Signed-off-by: Jono Yang <jyang@nexb.com> * Use tabset in dependency detail view #447 * Add package_type property to DiscoveredDependency Signed-off-by: Jono Yang <jyang@nexb.com> * Update dependency list view #447 * Use updated table header include * Update dependency presentation in package detail view * Show package uid on hover on for package tab Signed-off-by: Jono Yang <jyang@nexb.com> * Set DiscoveredDependency serializer fields #511 * Update DiscoveredDependency ordering Signed-off-by: Jono Yang <jyang@nexb.com> * Create donut chart for package type #447 Signed-off-by: Jono Yang <jyang@nexb.com> * Consolidate migrations #447 * Update DiscoveredDependency ordering * Update daglib test expectations Signed-off-by: Jono Yang <jyang@nexb.com> * Update dependency JSON ordering #447 * Update test expectations Signed-off-by: Jono Yang <jyang@nexb.com> * Set proper discovereddependencies related_name #447 Signed-off-by: Thomas Druez <tdruez@nexb.com> * Fix template indentation #447 Signed-off-by: Thomas Druez <tdruez@nexb.com> * Refactor update_from_data method into a UpdateFromDataMixin #447 Signed-off-by: Thomas Druez <tdruez@nexb.com> * Fix the ProjectSerializer fields #447 Signed-off-by: Thomas Druez <tdruez@nexb.com> * Fix test_scanpipe_api_project_detail unit test #447 Signed-off-by: Thomas Druez <tdruez@nexb.com> * Add HTML title for list views #506 Signed-off-by: Thomas Druez <tdruez@nexb.com> * Update dependency tabs #447 * Only show links in dependency for_package tab or dependency datafile_resource tab if there is a value Signed-off-by: Jono Yang <jyang@nexb.com> * Use UpdateFromDataMixin #447 * Use UpdateFromDataMixin in DiscoveredDependency * Create test for DiscoveredDependency.update_from_data() Signed-off-by: Jono Yang <jyang@nexb.com> * Fix formatting #447 Signed-off-by: Thomas Druez <tdruez@nexb.com> Signed-off-by: Jono Yang <jyang@nexb.com> Signed-off-by: Thomas Druez <tdruez@nexb.com> Co-authored-by: Thomas Druez <tdruez@nexb.com>
1 parent cf389ae commit 6c26874

49 files changed

Lines changed: 3132 additions & 556 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.rst

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,42 @@ v31.0.0 (2022-08-25)
9999
- Package data detected from a file are now stored on the
100100
CodebaseResource.package_data field.
101101
- A second processing step is now done after scanning for Package data, where
102-
Package Resources are determined and DiscoveredPackages are created.
102+
Package Resources are determined and DiscoveredPackages and
103+
DiscoveredDependencies are created.
103104

104105
https://github.com/nexB/scancode.io/issues/444
105106

106107
- ``CodebaseResource.for_packages`` now returns a list of
107108
``DiscoveredPackage.package_uid`` or ``DiscoveredPackage.package_url`` if
108109
``DiscoveredPackage.package_uid`` is not present. This is done to reflect the
109-
how scancode-toolkit's JSON output returns ``package_uid`` in the
110+
how scancode-toolkit's JSON output returns ``package_uid``s in the
110111
``for_packages`` field for Resources.
111112

113+
- Add the model DiscoveredDependency. This represents Package dependencies
114+
discovered in a Project. The ``scan_codebase`` and ``scan_packages`` pipelines
115+
have been updated to create DiscoveredDepdendency objects. The Project API has
116+
been updated with new fields:
117+
118+
- ``dependency_count``
119+
- The number of DiscoveredDependencies associated with the project.
120+
121+
- ``discovered_dependencies_summary``
122+
- A mapping that contains following fields:
123+
124+
- ``total``
125+
- The number of DiscoveredDependencies associated with the project.
126+
- ``is_runtime``
127+
- The number of runtime dependencies.
128+
- ``is_optional``
129+
- The number of optional dependencies.
130+
- ``is_resolved``
131+
- The number of resolved dependencies.
132+
133+
These values are also available on the Project view.
134+
https://github.com/nexB/scancode.io/issues/447
135+
136+
- The ``dependencies`` field has been removed from the DiscoveredPackage model.
137+
112138
v30.2.0 (2021-12-17)
113139
--------------------
114140

docs/rest-api.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ The project details view returns all information available about a project.
188188
"codebase_resources_summary": {
189189
"application-package": 1
190190
},
191-
"discovered_package_summary": {
191+
"discovered_packages_summary": {
192192
"total": 1,
193193
"with_missing_resources": 0,
194194
"with_modified_resources": 0

scanpipe/api/serializers.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from scanpipe.api import ExcludeFromListViewMixin
2828
from scanpipe.models import CodebaseResource
29+
from scanpipe.models import DiscoveredDependency
2930
from scanpipe.models import DiscoveredPackage
3031
from scanpipe.models import Project
3132
from scanpipe.models import ProjectError
@@ -112,7 +113,8 @@ class ProjectSerializer(
112113
runs = RunSerializer(many=True, read_only=True)
113114
input_sources = serializers.JSONField(source="input_sources_list", read_only=True)
114115
codebase_resources_summary = serializers.SerializerMethodField()
115-
discovered_package_summary = serializers.SerializerMethodField()
116+
discovered_packages_summary = serializers.SerializerMethodField()
117+
discovered_dependencies_summary = serializers.SerializerMethodField()
116118

117119
class Meta:
118120
model = Project
@@ -136,8 +138,10 @@ class Meta:
136138
"error_count",
137139
"resource_count",
138140
"package_count",
141+
"dependency_count",
139142
"codebase_resources_summary",
140-
"discovered_package_summary",
143+
"discovered_packages_summary",
144+
"discovered_dependencies_summary",
141145
)
142146

143147
exclude_from_list_view = [
@@ -147,22 +151,33 @@ class Meta:
147151
"error_count",
148152
"resource_count",
149153
"package_count",
154+
"dependency_count",
150155
"codebase_resources_summary",
151-
"discovered_package_summary",
156+
"discovered_packages_summary",
157+
"discovered_dependencies_summary",
152158
]
153159

154160
def get_codebase_resources_summary(self, project):
155161
queryset = project.codebaseresources.all()
156162
return count_group_by(queryset, "status")
157163

158-
def get_discovered_package_summary(self, project):
164+
def get_discovered_packages_summary(self, project):
159165
base_qs = project.discoveredpackages
160166
return {
161167
"total": base_qs.count(),
162168
"with_missing_resources": base_qs.exclude(missing_resources=[]).count(),
163169
"with_modified_resources": base_qs.exclude(modified_resources=[]).count(),
164170
}
165171

172+
def get_discovered_dependencies_summary(self, project):
173+
base_qs = project.discovereddependencies
174+
return {
175+
"total": base_qs.count(),
176+
"is_runtime": base_qs.filter(is_runtime=True).count(),
177+
"is_optional": base_qs.filter(is_optional=True).count(),
178+
"is_resolved": base_qs.filter(is_resolved=True).count(),
179+
}
180+
166181
def create(self, validated_data):
167182
"""
168183
Creates a new `project` with `upload_file` and `pipeline` as optional.
@@ -219,6 +234,29 @@ class Meta:
219234
]
220235

221236

237+
class DiscoveredDependencySerializer(serializers.ModelSerializer):
238+
purl = serializers.ReadOnlyField()
239+
for_package_uid = serializers.ReadOnlyField()
240+
datafile_path = serializers.ReadOnlyField()
241+
package_type = serializers.ReadOnlyField(source="type")
242+
243+
class Meta:
244+
model = DiscoveredDependency
245+
fields = [
246+
"purl",
247+
"package_type",
248+
"extracted_requirement",
249+
"scope",
250+
"is_runtime",
251+
"is_optional",
252+
"is_resolved",
253+
"dependency_uid",
254+
"for_package_uid",
255+
"datafile_path",
256+
"datasource_id",
257+
]
258+
259+
222260
class ProjectErrorSerializer(serializers.ModelSerializer):
223261
traceback = serializers.SerializerMethodField()
224262

@@ -257,6 +295,7 @@ def get_model_serializer(model_class):
257295
serializer = {
258296
CodebaseResource: CodebaseResourceSerializer,
259297
DiscoveredPackage: DiscoveredPackageSerializer,
298+
DiscoveredDependency: DiscoveredDependencySerializer,
260299
ProjectError: ProjectErrorSerializer,
261300
}.get(model_class, None)
262301

scanpipe/api/views.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from rest_framework.response import Response
3737

3838
from scanpipe.api.serializers import CodebaseResourceSerializer
39+
from scanpipe.api.serializers import DiscoveredDependencySerializer
3940
from scanpipe.api.serializers import DiscoveredPackageSerializer
4041
from scanpipe.api.serializers import PipelineSerializer
4142
from scanpipe.api.serializers import ProjectErrorSerializer
@@ -180,6 +181,16 @@ def packages(self, request, *args, **kwargs):
180181

181182
return Response(serializer.data)
182183

184+
@action(detail=True)
185+
def dependencies(self, request, *args, **kwargs):
186+
project = self.get_object()
187+
queryset = project.discovereddependencies.all()
188+
189+
paginated_qs = self.paginate_queryset(queryset)
190+
serializer = DiscoveredDependencySerializer(paginated_qs, many=True)
191+
192+
return Response(serializer.data)
193+
183194
@action(detail=True)
184195
def errors(self, request, *args, **kwargs):
185196
project = self.get_object()

scanpipe/filters.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from packageurl.contrib.django.filters import PackageURLFilter
3333

3434
from scanpipe.models import CodebaseResource
35+
from scanpipe.models import DiscoveredDependency
3536
from scanpipe.models import DiscoveredPackage
3637
from scanpipe.models import Project
3738
from scanpipe.models import ProjectError
@@ -159,6 +160,7 @@ class ProjectFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
159160
"created_date",
160161
"name",
161162
"discoveredpackages_count",
163+
"discovereddependencies_count",
162164
"codebaseresources_count",
163165
"projecterrors_count",
164166
],
@@ -169,6 +171,8 @@ class ProjectFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
169171
("-name", "Name (z-A)"),
170172
("-discoveredpackages_count", "Packages (+)"),
171173
("discoveredpackages_count", "Packages (-)"),
174+
("-discovereddependencies_count", "Dependencies (+)"),
175+
("discovereddependencies_count", "Dependencies (-)"),
172176
("-codebaseresources_count", "Resources (+)"),
173177
("codebaseresources_count", "Resources (-)"),
174178
("-projecterrors_count", "Errors (+)"),
@@ -364,6 +368,32 @@ class Meta:
364368
]
365369

366370

371+
class DependencyFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
372+
search = django_filters.CharFilter(
373+
label="Search", field_name="name", lookup_expr="icontains"
374+
)
375+
purl = PackageURLFilter(label="Package URL")
376+
377+
class Meta:
378+
model = DiscoveredDependency
379+
fields = [
380+
"search",
381+
"purl",
382+
"dependency_uid",
383+
"type",
384+
"namespace",
385+
"name",
386+
"version",
387+
"qualifiers",
388+
"subpath",
389+
"scope",
390+
"is_runtime",
391+
"is_optional",
392+
"is_resolved",
393+
"datasource_id",
394+
]
395+
396+
367397
class ErrorFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
368398
search = django_filters.CharFilter(
369399
label="Search", field_name="message", lookup_expr="icontains"
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Generated by Django 4.0.6 on 2022-08-13 00:27
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
import scanpipe.models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('scanpipe', '0021_codebaseresource_package_data'),
12+
]
13+
14+
operations = [
15+
migrations.RenameField(
16+
model_name='discoveredpackage',
17+
old_name='dependencies',
18+
new_name='dependencies_data',
19+
),
20+
migrations.CreateModel(
21+
name='DiscoveredDependency',
22+
fields=[
23+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
24+
('dependency_uid', models.CharField(help_text='The unique identifier of this dependency.', max_length=1024)),
25+
('purl', models.CharField(help_text='The Package URL of this dependency.', max_length=1024)),
26+
('extracted_requirement', models.CharField(blank=True, help_text='The version requirements of this dependency.', max_length=64)),
27+
('scope', models.CharField(blank=True, help_text='The scope of this dependency, how it is used in a project.', max_length=64)),
28+
('is_runtime', models.BooleanField(default=False)),
29+
('is_optional', models.BooleanField(default=False)),
30+
('is_resolved', models.BooleanField(default=False)),
31+
('datasource_id', models.CharField(blank=True, help_text='The identifier for the datafile handler used to obtain this dependency.', max_length=64)),
32+
('datafile_resource', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='dependencies', to='scanpipe.codebaseresource')),
33+
('for_package', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='dependencies', to='scanpipe.discoveredpackage')),
34+
('project', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='discovereddependencies', to='scanpipe.project')),
35+
],
36+
options={
37+
'verbose_name': 'discovered dependency',
38+
'verbose_name_plural': 'discovered dependencies',
39+
'ordering': ['-is_runtime', '-is_resolved', 'is_optional', 'dependency_uid'],
40+
},
41+
bases=(models.Model, scanpipe.models.SaveProjectErrorMixin),
42+
),
43+
migrations.AddConstraint(
44+
model_name='discovereddependency',
45+
constraint=models.UniqueConstraint(condition=models.Q(('dependency_uid', ''), _negated=True), fields=('project', 'dependency_uid'), name='scanpipe_discovereddependency_unique_dependency_uid_within_project'),
46+
),
47+
migrations.AddField(
48+
model_name='discovereddependency',
49+
name='name',
50+
field=models.CharField(blank=True, help_text='Name of the package.', max_length=100),
51+
),
52+
migrations.AddField(
53+
model_name='discovereddependency',
54+
name='namespace',
55+
field=models.CharField(blank=True, help_text='Package name prefix, such as Maven groupid, Docker image owner, GitHub user or organization, etc.', max_length=255),
56+
),
57+
migrations.AddField(
58+
model_name='discovereddependency',
59+
name='qualifiers',
60+
field=models.CharField(blank=True, help_text='Extra qualifying data for a package such as the name of an OS, architecture, distro, etc.', max_length=1024),
61+
),
62+
migrations.AddField(
63+
model_name='discovereddependency',
64+
name='subpath',
65+
field=models.CharField(blank=True, help_text='Extra subpath within a package, relative to the package root.', max_length=200),
66+
),
67+
migrations.AddField(
68+
model_name='discovereddependency',
69+
name='type',
70+
field=models.CharField(blank=True, help_text='A short code to identify the type of this package. For example: gem for a Rubygem, docker for a container, pypi for a Python Wheel or Egg, maven for a Maven Jar, deb for a Debian package, etc.', max_length=16),
71+
),
72+
migrations.AddField(
73+
model_name='discovereddependency',
74+
name='version',
75+
field=models.CharField(blank=True, help_text='Version of the package.', max_length=100),
76+
),
77+
migrations.AlterModelOptions(
78+
name="discovereddependency",
79+
options={
80+
"ordering": [
81+
"-is_runtime",
82+
"-is_resolved",
83+
"is_optional",
84+
"dependency_uid",
85+
"for_package",
86+
"datafile_resource",
87+
"datasource_id",
88+
],
89+
"verbose_name": "discovered dependency",
90+
"verbose_name_plural": "discovered dependencies",
91+
},
92+
),
93+
]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Generated by Django 4.0.6 on 2022-08-12 23:13
2+
3+
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
4+
from django.db import migrations
5+
6+
7+
def migrate_dependencies_to_discovereddependencies(apps, schema_editor):
8+
DiscoveredPackage = apps.get_model('scanpipe', 'DiscoveredPackage')
9+
DiscoveredDependency = apps.get_model('scanpipe', 'DiscoveredDependency')
10+
11+
package_with_dependencies = DiscoveredPackage.objects.exclude(dependencies_data=[])
12+
13+
for package in package_with_dependencies:
14+
for dependency_data in package.dependencies_data:
15+
project = package.project
16+
17+
# Remove non-supported fields from the data dict
18+
dependency_data.pop("extra_data", None)
19+
dependency_data.pop("resolved_package", None)
20+
21+
for_package_uid = dependency_data.get("for_package_uid")
22+
try:
23+
for_package = project.discoveredpackages.get(package_uid=for_package_uid)
24+
except (ObjectDoesNotExist, MultipleObjectsReturned):
25+
for_package = None
26+
27+
datafile_path = dependency_data.get("datafile_path")
28+
try:
29+
datafile_resource = project.codebaseresources.get(path=datafile_path)
30+
except (ObjectDoesNotExist, MultipleObjectsReturned):
31+
datafile_resource = None
32+
33+
DiscoveredDependency.objects.create(
34+
project=project,
35+
for_package=for_package,
36+
datafile_resource=datafile_resource,
37+
**dependency_data,
38+
)
39+
40+
41+
class Migration(migrations.Migration):
42+
43+
dependencies = [
44+
('scanpipe', '0022_rename_dependencies_discoveredpackage_dependencies_data_and_more'),
45+
]
46+
47+
operations = [
48+
migrations.RunPython(migrate_dependencies_to_discovereddependencies),
49+
]

0 commit comments

Comments
 (0)