Skip to content

Commit 44711ea

Browse files
Address feedback and refactor code
Signed-off-by: Ayan Sinha Mahapatra <ayansmahapatra@gmail.com>
1 parent fae73bf commit 44711ea

6 files changed

Lines changed: 137 additions & 43 deletions

File tree

scanpipe/migrations/0062_dependency_resolver_update.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,50 @@ class Migration(migrations.Migration):
1313
migrations.AddField(
1414
model_name="discovereddependency",
1515
name="is_direct",
16-
field=models.BooleanField(default=False),
16+
field=models.BooleanField(
17+
default=False,
18+
help_text="True if this is a direct, first-level dependency relationship for a package.",
19+
),
1720
),
1821
migrations.AddField(
1922
model_name="discoveredpackage",
2023
name="is_private",
21-
field=models.BooleanField(default=False),
24+
field=models.BooleanField(
25+
default=False,
26+
help_text="True if this is a private package, either not meant to be published on a repository, and/or a local package without a name and version used primarily to track dependencies and other information.",
27+
),
2228
),
2329
migrations.AddField(
2430
model_name="discoveredpackage",
2531
name="is_virtual",
26-
field=models.BooleanField(default=False),
32+
field=models.BooleanField(
33+
default=False,
34+
help_text="True if this package is created only from a manifest or lockfile, and not from its actual packaged code. The files of this package are not present in the codebase.",
35+
),
36+
),
37+
migrations.AlterField(
38+
model_name="discovereddependency",
39+
name="is_optional",
40+
field=models.BooleanField(
41+
default=False,
42+
help_text="True if this dependency is an optional dependency",
43+
),
44+
),
45+
migrations.AlterField(
46+
model_name="discovereddependency",
47+
name="is_resolved",
48+
field=models.BooleanField(
49+
default=False,
50+
help_text="True if this dependency version requirement has been pinned and this dependency points to an exact version.",
51+
),
52+
),
53+
migrations.AlterField(
54+
model_name="discovereddependency",
55+
name="is_runtime",
56+
field=models.BooleanField(
57+
default=False,
58+
help_text="True if this dependency is a runtime dependency.",
59+
),
2760
),
2861
migrations.AddIndex(
2962
model_name="discovereddependency",

scanpipe/models.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3074,8 +3074,23 @@ class AbstractPackage(models.Model):
30743074
blank=True,
30753075
help_text=_("A notice text for this package."),
30763076
)
3077-
is_private = models.BooleanField(default=False)
3078-
is_virtual = models.BooleanField(default=False)
3077+
is_private = models.BooleanField(
3078+
default=False,
3079+
help_text=_(
3080+
"True if this is a private package, either not meant to be "
3081+
"published on a repository, and/or a local package without a "
3082+
"name and version used primarily to track dependencies and "
3083+
"other information."
3084+
),
3085+
)
3086+
is_virtual = models.BooleanField(
3087+
default=False,
3088+
help_text=_(
3089+
"True if this package is created only from a manifest or lockfile, "
3090+
"and not from its actual packaged code. The files of this package "
3091+
"are not present in the codebase."
3092+
),
3093+
)
30793094
datasource_ids = models.JSONField(
30803095
default=list,
30813096
blank=True,

scanpipe/pipes/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,12 @@ def update_or_create_dependency(
287287
dependencies.append(dependency)
288288

289289
for dependency in dependencies:
290-
# This dependency relationship is for a new package
291-
if (
290+
is_for_new_package = (
292291
for_package
293292
and dependency.for_package
294293
and dependency.for_package != for_package
295-
):
294+
)
295+
if is_for_new_package:
296296
DiscoveredDependency.populate_dependency_uuid(dependency_data)
297297
dependency = DiscoveredDependency.create_from_data(
298298
project,

scanpipe/pipes/scancode.py

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -489,38 +489,73 @@ def process_package_data(project, static_resolve=False):
489489
for resource in project.codebaseresources.has_package_data():
490490
logger.info(f" Processing: {resource.path}")
491491
for package_mapping in resource.package_data:
492-
pd = packagedcode_models.PackageData.from_dict(mapping=package_mapping)
493-
if not pd.can_assemble:
494-
continue
492+
create_packages_and_dependencies_from_mapping(
493+
project=project,
494+
resource=resource,
495+
package_mapping=package_mapping,
496+
find_package=False,
497+
)
495498

496-
logger.info(f" Package data: {pd.purl}")
499+
if static_resolve:
500+
resolve_dependencies(project)
497501

498-
package_data = pd.to_dict()
499-
dependencies = package_data.pop("dependencies")
500502

501-
package = None
502-
if pd.purl:
503-
package = pipes.update_or_create_package(
504-
project=project,
505-
package_data=package_data,
506-
codebase_resources=[resource],
507-
)
503+
def create_packages_and_dependencies_from_mapping(
504+
project,
505+
resource,
506+
package_mapping,
507+
find_package=False,
508+
):
509+
"""
510+
Create or update packages and dependencies from a `package_mapping`,
511+
for a respective `resource` and `project`.
508512
509-
for dep in dependencies:
510-
pipes.update_or_create_dependency(
511-
project=project,
512-
dependency_data=dep,
513-
for_package=package,
514-
datafile_resource=resource,
515-
datasource_id=pd.datasource_id,
516-
)
513+
If `find_package` is True, find the package with the respective purl data,
514+
instead of trying to create it.
515+
"""
516+
pd = packagedcode_models.PackageData.from_dict(mapping=package_mapping)
517+
if not pd.can_assemble:
518+
return
517519

518-
if static_resolve:
519-
resolve_dependencies(project)
520+
logger.info(f" Package data: {pd.purl}")
520521

522+
package_data = pd.to_dict()
523+
dependencies = package_data.pop("dependencies")
521524

522-
def resolve_dependencies(project):
525+
package = None
526+
if pd.purl:
527+
if find_package:
528+
purl_data = DiscoveredPackage.extract_purl_data(package_mapping)
529+
packages = DiscoveredPackage.objects.filter(
530+
project=project,
531+
**purl_data,
532+
)
523533

534+
for package in packages:
535+
if resource.location in package.datafile_paths:
536+
break
537+
else:
538+
package = pipes.update_or_create_package(
539+
project=project,
540+
package_data=package_data,
541+
codebase_resources=[resource],
542+
)
543+
544+
update_packages_and_dependencies(
545+
project=project,
546+
dependencies=dependencies,
547+
package=package,
548+
resource=resource,
549+
datasource_id=pd.datasource_id,
550+
process_resolved=False,
551+
)
552+
553+
554+
def resolve_dependencies(project):
555+
"""
556+
Match and merge resolved dependencies to create a dependency graph of
557+
direct dependency relations between resolved packages.
558+
"""
524559
logger.info(f"Project {project} resolve_dependencies:")
525560
for resource in project.codebaseresources.has_package_data():
526561
for package_mapping in resource.package_data:
@@ -555,6 +590,7 @@ def update_packages_and_dependencies(
555590
package,
556591
resource,
557592
datasource_id,
593+
process_resolved=True,
558594
):
559595
"""
560596
Create DiscoveredPackage and DiscoveredDependency objects from
@@ -564,7 +600,7 @@ def update_packages_and_dependencies(
564600
for dep in dependencies:
565601
resolved_package = dep.get("resolved_package") or {}
566602
resolved_to_package = None
567-
if resolved_package:
603+
if process_resolved and resolved_package:
568604
resolved_to_package = pipes.update_or_create_package(
569605
project=project,
570606
package_data=resolved_package,

scanpipe/tests/test_models.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from django.utils import timezone
5050

5151
from packagedcode.models import PackageData
52+
from packageurl import PackageURL
5253
from requests.exceptions import RequestException
5354
from rq.job import JobStatus
5455

@@ -65,6 +66,7 @@
6566
from scanpipe.models import UUIDTaggedItem
6667
from scanpipe.models import convert_glob_to_django_regex
6768
from scanpipe.models import get_project_work_directory
69+
from scanpipe.models import normalize_package_url_data
6870
from scanpipe.pipes.fetch import Download
6971
from scanpipe.pipes.input import copy_input
7072
from scanpipe.tests import dependency_data1
@@ -730,6 +732,17 @@ def test_scanpipe_project_get_ignored_dependency_scopes_index(self):
730732
expected = {"npm": ["devDependencies"], "pypi": ["tests", "build"]}
731733
self.assertEqual(expected, self.project1.get_ignored_dependency_scopes_index())
732734

735+
def test_scanpipe_normalize_package_url_data(self):
736+
purl = PackageURL.from_string("pkg:npm/athena-express@6.0.4")
737+
purl_data = normalize_package_url_data(purl_mapping=purl.to_dict())
738+
self.assertEqual(purl_data.get("namespace"), "")
739+
740+
purl_data = normalize_package_url_data(
741+
purl_mapping=purl.to_dict(),
742+
ignore_nulls=True,
743+
)
744+
self.assertEqual(purl_data.get("namespace"), None)
745+
733746
def test_scanpipe_project_get_ignored_vulnerabilities_set(self):
734747
self.project1.settings = {
735748
"ignored_vulnerabilities": [

scanpipe/tests/test_pipelines.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,12 @@ def _normalize_package_uids(self, data):
557557
Return the `data`, where any `package_uid` value has been normalized
558558
with `purl_with_fake_uuid()`
559559
"""
560+
fields_with_package_uids = [
561+
"package_uid",
562+
"dependency_uid",
563+
"for_package_uid",
564+
"resolved_to_package_uid",
565+
]
560566
if isinstance(data, list):
561567
return [self._normalize_package_uids(entry) for entry in data]
562568

@@ -568,16 +574,7 @@ def _normalize_package_uids(self, data):
568574
for key, value in data.items():
569575
if isinstance(value, (list, dict)):
570576
value = self._normalize_package_uids(value)
571-
if (
572-
key
573-
in (
574-
"package_uid",
575-
"dependency_uid",
576-
"for_package_uid",
577-
"resolved_to_package_uid",
578-
)
579-
and value
580-
):
577+
if key in fields_with_package_uids and value:
581578
value = purl_with_fake_uuid(value)
582579
if key == "for_packages" and value:
583580
value = sorted(

0 commit comments

Comments
 (0)