Skip to content

Commit 5cf11ac

Browse files
Resolve dependencies from lockfiles #1237
Reference: #1237 Reference: #1066 Signed-off-by: Ayan Sinha Mahapatra <ayansmahapatra@gmail.com>
1 parent 49f9c07 commit 5cf11ac

38 files changed

Lines changed: 2169 additions & 249 deletions

scanpipe/api/serializers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,8 @@ class Meta:
380380
"source_packages",
381381
"extra_data",
382382
"package_uid",
383+
"is_private",
384+
"is_virtual",
383385
"datasource_ids",
384386
"datafile_paths",
385387
"file_references",
@@ -405,6 +407,7 @@ class Meta:
405407
"is_runtime",
406408
"is_optional",
407409
"is_resolved",
410+
"is_direct",
408411
"dependency_uid",
409412
"for_package_uid",
410413
"resolved_to_package_uid",

scanpipe/filters.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,7 @@ class DependencyFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
727727
"is_runtime",
728728
"is_optional",
729729
"is_resolved",
730+
"is_direct",
730731
"datasource_id",
731732
"is_vulnerable",
732733
]
@@ -747,6 +748,7 @@ class DependencyFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
747748
"is_runtime",
748749
"is_optional",
749750
"is_resolved",
751+
"is_direct",
750752
"for_package",
751753
"resolved_to_package",
752754
"datafile_resource",
@@ -761,6 +763,7 @@ class DependencyFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
761763
is_runtime = StrictBooleanFilter()
762764
is_optional = StrictBooleanFilter()
763765
is_resolved = StrictBooleanFilter()
766+
is_direct = StrictBooleanFilter()
764767
is_vulnerable = IsVulnerable(field_name="affected_by_vulnerabilities")
765768

766769
class Meta:
@@ -779,6 +782,7 @@ class Meta:
779782
"is_runtime",
780783
"is_optional",
781784
"is_resolved",
785+
"is_direct",
782786
"datasource_id",
783787
"is_vulnerable",
784788
]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Generated by Django 5.0.6 on 2024-06-04 20:48
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("scanpipe", "0060_discovereddependency_renames"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="discovereddependency",
15+
name="is_direct",
16+
field=models.BooleanField(default=False),
17+
),
18+
migrations.AddField(
19+
model_name="discoveredpackage",
20+
name="is_private",
21+
field=models.BooleanField(default=False),
22+
),
23+
migrations.AddField(
24+
model_name="discoveredpackage",
25+
name="is_virtual",
26+
field=models.BooleanField(default=False),
27+
),
28+
migrations.AddIndex(
29+
model_name="discovereddependency",
30+
index=models.Index(
31+
fields=["is_direct"], name="scanpipe_di_is_dire_6dc594_idx"
32+
),
33+
),
34+
]

scanpipe/models.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,19 @@ def walk_codebase_path(self):
10031003
"""Return files and directories path of the codebase/ directory recursively."""
10041004
return self.codebase_path.rglob("*")
10051005

1006+
def get_resource(self, path):
1007+
"""
1008+
Return the codebase resource present for a given path,
1009+
or None the resource with that path does not exist.
1010+
This path is relative to the scan location.
1011+
This is same as the Codebase.get_resource() function.
1012+
"""
1013+
# We don't want to raise an exception if there is no resource
1014+
# as this function is also called from the SCTK side
1015+
resource = self.codebaseresources.get_or_none(path=path)
1016+
if resource:
1017+
return resource
1018+
10061019
@cached_property
10071020
def can_change_inputs(self):
10081021
"""
@@ -2971,6 +2984,8 @@ class AbstractPackage(models.Model):
29712984
blank=True,
29722985
help_text=_("A notice text for this package."),
29732986
)
2987+
is_private = models.BooleanField(default=False)
2988+
is_virtual = models.BooleanField(default=False)
29742989
datasource_ids = models.JSONField(
29752990
default=list,
29762991
blank=True,
@@ -3432,6 +3447,7 @@ class DiscoveredDependency(
34323447
is_runtime = models.BooleanField(default=False)
34333448
is_optional = models.BooleanField(default=False)
34343449
is_resolved = models.BooleanField(default=False)
3450+
is_direct = models.BooleanField(default=False)
34353451

34363452
objects = DiscoveredDependencyQuerySet.as_manager()
34373453

@@ -3452,6 +3468,7 @@ class Meta:
34523468
models.Index(fields=["is_runtime"]),
34533469
models.Index(fields=["is_optional"]),
34543470
models.Index(fields=["is_resolved"]),
3471+
models.Index(fields=["is_direct"]),
34553472
]
34563473
constraints = [
34573474
models.UniqueConstraint(
@@ -3498,6 +3515,7 @@ def create_from_data(
34983515
project,
34993516
dependency_data,
35003517
for_package=None,
3518+
resolved_to_package=None,
35013519
datafile_resource=None,
35023520
datasource_id=None,
35033521
strip_datafile_path_root=False,
@@ -3537,6 +3555,13 @@ def create_from_data(
35373555
package_uid=for_package_uid
35383556
)
35393557

3558+
if not resolved_to_package:
3559+
resolved_to_uid = dependency_data.get("resolved_to_uid")
3560+
if resolved_to_uid:
3561+
resolved_to_package = project.discoveredpackages.get(
3562+
package_uid=resolved_to_uid
3563+
)
3564+
35403565
if not datafile_resource:
35413566
datafile_path = dependency_data.get("datafile_path")
35423567
if datafile_path:
@@ -3562,10 +3587,36 @@ def create_from_data(
35623587
return cls.objects.create(
35633588
project=project,
35643589
for_package=for_package,
3590+
resolved_to_package=resolved_to_package,
35653591
datafile_resource=datafile_resource,
35663592
**cleaned_data,
35673593
)
35683594

3595+
@classmethod
3596+
def extract_purl_data(cls, dependency_data, ignore_nulls=False):
3597+
purl_mapping = PackageURL.from_string(
3598+
purl=dependency_data.get("purl"),
3599+
).to_dict()
3600+
purl_data = {}
3601+
3602+
for field_name in PURL_FIELDS:
3603+
value = purl_mapping.get(field_name)
3604+
if field_name == "qualifiers":
3605+
value = normalize_qualifiers(value, encode=True)
3606+
if not ignore_nulls:
3607+
purl_data[field_name] = value or ""
3608+
else:
3609+
if value:
3610+
purl_data[field_name] = value or ""
3611+
3612+
return purl_data
3613+
3614+
@classmethod
3615+
def populate_dependency_uuid(cls, dependency_data):
3616+
purl = PackageURL.from_string(purl=dependency_data.get("purl"))
3617+
purl.qualifiers["uuid"] = str(uuid.uuid4())
3618+
dependency_data["dependency_uid"] = purl.to_string()
3619+
35693620
@property
35703621
def spdx_id(self):
35713622
return f"SPDXRef-scancodeio-{self._meta.model_name}-{self.dependency_uid}"

scanpipe/pipelines/inspect_packages.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
2121
# Visit https://github.com/nexB/scancode.io for support and download.
2222

23+
from scanpipe.pipelines import group
2324
from scanpipe.pipelines.scan_codebase import ScanCodebase
2425
from scanpipe.pipes import scancode
2526

@@ -49,23 +50,26 @@ def steps(cls):
4950
cls.flag_empty_files,
5051
cls.flag_ignored_resources,
5152
cls.scan_for_application_packages,
52-
cls.create_packages_and_dependencies,
53+
cls.resolve_dependencies,
5354
)
5455

5556
def scan_for_application_packages(self):
5657
"""
5758
Scan resources for package information to add DiscoveredPackage
5859
and DiscoveredDependency objects from detected package data.
5960
"""
60-
# `assemble` is set to False because here in this pipeline we
61-
# only detect package_data in resources and create
62-
# Package/Dependency instances directly instead of assembling
63-
# the packages and assigning files to them
6461
scancode.scan_for_application_packages(
6562
project=self.project,
66-
assemble=False,
63+
assemble=True,
6764
package_only=True,
65+
progress_logger=self.log,
6866
)
6967

70-
def create_packages_and_dependencies(self):
71-
scancode.process_package_data(self.project)
68+
@group("Static Resolver")
69+
def resolve_dependencies(self):
70+
"""
71+
Create packages and dependency relationships from
72+
lockfiles or manifests containing pre-resolved
73+
dependencies.
74+
"""
75+
scancode.resolve_dependencies(project=self.project)

scanpipe/pipelines/resolve_dependencies.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
2121
# Visit https://github.com/nexB/scancode.io for support and download.
2222

23+
from scanpipe.pipelines import group
2324
from scanpipe.pipelines.scan_codebase import ScanCodebase
2425
from scanpipe.pipes import resolve
26+
from scanpipe.pipes import scancode
2527

2628

2729
class ResolveDependencies(ScanCodebase):
@@ -45,6 +47,8 @@ def steps(cls):
4547
cls.collect_and_create_codebase_resources,
4648
cls.flag_ignored_resources,
4749
cls.get_manifest_inputs,
50+
cls.scan_for_application_packages,
51+
cls.create_packages_and_dependencies,
4852
cls.get_packages_from_manifest,
4953
cls.create_resolved_packages,
5054
)
@@ -53,6 +57,20 @@ def get_manifest_inputs(self):
5357
"""Locate package manifest files with a supported package resolver."""
5458
self.manifest_resources = resolve.get_manifest_resources(self.project)
5559

60+
@group("Static Resolver")
61+
def scan_for_application_packages(self):
62+
scancode.scan_for_application_packages(
63+
self.project,
64+
assemble=False,
65+
resource_qs=self.manifest_resources,
66+
progress_logger=self.log,
67+
)
68+
69+
@group("Static Resolver")
70+
def create_packages_and_dependencies(self):
71+
scancode.process_package_data(self.project, static_resolve=True)
72+
73+
@group("Dynamic Resolver")
5674
def get_packages_from_manifest(self):
5775
"""
5876
Resolve package data from lockfiles/requirement files with package
@@ -65,6 +83,7 @@ def get_packages_from_manifest(self):
6583
model="get_packages_from_manifest",
6684
)
6785

86+
@group("Dynamic Resolver")
6887
def create_resolved_packages(self):
6988
"""Create the resolved packages and their dependencies in the database."""
7089
resolve.create_packages_and_dependencies(

scanpipe/pipes/__init__.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,12 @@ def _clean_package_data(package_data):
168168
return package_data
169169

170170

171-
def update_or_create_package(project, package_data, codebase_resources=None):
171+
def update_or_create_package(
172+
project,
173+
package_data,
174+
codebase_resources=None,
175+
is_virtual=False,
176+
):
172177
"""
173178
Get, update or create a DiscoveredPackage then return it.
174179
Use the `project` and `package_data` mapping to lookup and creates the
@@ -194,6 +199,9 @@ def update_or_create_package(project, package_data, codebase_resources=None):
194199
package = DiscoveredPackage.create_from_data(project, package_data)
195200

196201
if package:
202+
if is_virtual:
203+
package.update(is_virtual=is_virtual)
204+
197205
if datasource_id and datasource_id not in package.datasource_ids:
198206
datasource_ids = package.datasource_ids.copy()
199207
datasource_ids.append(datasource_id)
@@ -239,6 +247,7 @@ def update_or_create_dependency(
239247
project,
240248
dependency_data,
241249
for_package=None,
250+
resolved_to_package=None,
242251
datafile_resource=None,
243252
datasource_id=None,
244253
strip_datafile_path_root=False,
@@ -254,27 +263,44 @@ def update_or_create_dependency(
254263
corresponding CodebaseResource for `datafile_path`. This is used in the case
255264
where Dependency data is imported from a scancode-toolkit scan, where the
256265
root path segments are not stripped for `datafile_path`.
266+
If the dependency is resolved and a resolved package is created, we have the
267+
corresponsing package_uid at `resolved_to`.
257268
"""
258269
dependency = None
259270
dependency_uid = dependency_data.get("dependency_uid")
271+
extracted_requirement = dependency_data.get("extracted_requirement")
260272

261273
if ignore_dependency_scope(project, dependency_data):
262274
return # Do not create the DiscoveredDependency record.
263275

264276
if not dependency_uid:
265-
dependency_data["dependency_uid"] = uuid.uuid4()
277+
purl_data = DiscoveredDependency.extract_purl_data(dependency_data)
278+
dependency = DiscoveredDependency.objects.get_or_none(
279+
project=project,
280+
extracted_requirement=extracted_requirement,
281+
**purl_data,
282+
)
266283
else:
267-
dependency = project.discovereddependencies.get_or_none(
284+
dependency = DiscoveredDependency.objects.get_or_none(
285+
project=project,
268286
dependency_uid=dependency_uid,
269287
)
270288

271289
if dependency:
272290
dependency.update_from_data(dependency_data)
291+
if resolved_to_package and not dependency.resolved_to_package:
292+
dependency.update(resolved_to_package=resolved_to_package)
273293
else:
294+
is_direct = dependency_data.get("is_direct")
295+
if not is_direct:
296+
pass
297+
298+
DiscoveredDependency.populate_dependency_uuid(dependency_data)
274299
dependency = DiscoveredDependency.create_from_data(
275300
project,
276301
dependency_data,
277302
for_package=for_package,
303+
resolved_to_package=resolved_to_package,
278304
datafile_resource=datafile_resource,
279305
datasource_id=datasource_id,
280306
strip_datafile_path_root=strip_datafile_path_root,

0 commit comments

Comments
 (0)