@@ -3772,6 +3772,18 @@ class DiscoveredDependencyQuerySet(
37723772 VulnerabilityQuerySetMixin ,
37733773 ProjectRelatedQuerySet ,
37743774):
3775+ def project_dependencies (self ):
3776+ return self .filter (for_package__isnull = True )
3777+
3778+ def package_dependencies (self ):
3779+ return self .filter (for_package__isnull = False )
3780+
3781+ def resolved (self ):
3782+ return self .filter (resolved_to_package__isnull = False )
3783+
3784+ def unresolved (self ):
3785+ return self .filter (resolved_to_package__isnull = True )
3786+
37753787 def prefetch_for_serializer (self ):
37763788 """
37773789 Optimized prefetching for a QuerySet to be consumed by the
@@ -3816,6 +3828,26 @@ class DiscoveredDependency(
38163828 system and application packages discovered in the code under analysis.
38173829 Dependencies are usually collected from parsed package data such as a package
38183830 manifest or lockfile.
3831+
3832+ This class manages dependencies with the following considerations:
3833+
3834+ 1. A dependency can be associated with a Package via the ``for_package`` field.
3835+ In this case, it is termed a "Package's dependency".
3836+ If there is no such association, the dependency is considered a
3837+ "Project's dependency".
3838+
3839+ 2. A dependency can also be linked to a Package through the ``resolved_to_package``
3840+ field. When this link exists, the dependency is considered "resolved".
3841+
3842+ 3. Dependencies can be either direct or transitive:
3843+ - A **direct dependency** is explicitly declared in a package manifest or
3844+ lockfile.
3845+ - A **transitive dependency** is not declared directly, but is required by one
3846+ of the project's direct dependencies.
3847+
3848+ Understanding the distinction between direct and transitive dependencies is
3849+ important for analyzing dependency trees, resolving version conflicts, and
3850+ assessing potential security risks.
38193851 """
38203852
38213853 # Overrides the `project` field to set the proper `related_name`.
@@ -3966,6 +3998,24 @@ def datafile_path(self):
39663998 if self .datafile_resource :
39673999 return self .datafile_resource .path
39684000
4001+ @property
4002+ def is_project_dependency (self ):
4003+ """
4004+ Return True if the dependency is directly associated with the project
4005+ (not tied to a specific package).
4006+ """
4007+ return not bool (self .for_package_id )
4008+
4009+ @property
4010+ def is_package_dependency (self ):
4011+ """Return True if the dependency is explicitly associated with a package."""
4012+ return bool (self .for_package_id )
4013+
4014+ @property
4015+ def is_resolved_to_package (self ):
4016+ """Return True if the dependency is resolved to a package."""
4017+ return bool (self .resolved_to_package_id )
4018+
39694019 @classmethod
39704020 def create_from_data (
39714021 cls ,
@@ -3981,6 +4031,14 @@ def create_from_data(
39814031 Create and returns a DiscoveredDependency for a `project` from the
39824032 `dependency_data`.
39834033
4034+ The `for_package` and `resolved_to_package` FKs can be provided as args,
4035+ or in the `dependency_data` using the `for_package_uid` and
4036+ `resolve_to_package_uid`.
4037+
4038+ Note that a dependency:
4039+ - without a `for_package` FK is a "Project's dependency"
4040+ - without a `resolve_to_package` is "unresolved".
4041+
39844042 If `strip_datafile_path_root` is True, then `create_from_data()` will
39854043 strip the root path segment from the `datafile_path` of
39864044 `dependency_data` before looking up the corresponding CodebaseResource
@@ -3989,51 +4047,36 @@ def create_from_data(
39894047 not stripped for `datafile_path`.
39904048 """
39914049 dependency_data = dependency_data .copy ()
3992- required_fields = ["purl" , "dependency_uid" ]
3993- missing_values = [
3994- field_name
3995- for field_name in required_fields
3996- if not dependency_data .get (field_name )
3997- ]
4050+ project_packages_qs = project .discoveredpackages
39984051
3999- if missing_values :
4000- message = (
4001- f"No values for the following required fields: "
4002- f"{ ', ' .join (missing_values )} "
4003- )
4052+ if not dependency_data .get ("dependency_uid" ):
4053+ dependency_data ["dependency_uid" ] = str (uuid .uuid4 ())
40044054
4005- project .add_warning (description = message , model = cls , details = dependency_data )
4006- return
4007-
4008- if not for_package :
4009- for_package_uid = dependency_data .get ("for_package_uid" )
4010- if for_package_uid :
4011- for_package = project .discoveredpackages .get (
4012- package_uid = for_package_uid
4013- )
4055+ for_package_uid = dependency_data .get ("for_package_uid" )
4056+ if not for_package and for_package_uid :
4057+ for_package = project_packages_qs .get_or_none (package_uid = for_package_uid )
40144058
4015- if not resolved_to_package :
4016- resolved_to_uid = dependency_data .get ("resolved_to_uid" )
4017- if resolved_to_uid :
4018- resolved_to_package = project .discoveredpackages .get (
4019- package_uid = resolved_to_uid
4020- )
4059+ resolve_to_package_uid = dependency_data .get ("resolve_to_package_uid" )
4060+ if not resolved_to_package and resolve_to_package_uid :
4061+ resolved_to_package = project_packages_qs .get_or_none (
4062+ package_uid = resolve_to_package_uid
4063+ )
40214064
4022- if not datafile_resource :
4023- datafile_path = dependency_data .get ("datafile_path" )
4024- if datafile_path :
4025- if strip_datafile_path_root :
4026- segments = datafile_path .split ("/" )
4027- datafile_path = "/" .join (segments [1 :])
4028- datafile_resource = project .codebaseresources .get (path = datafile_path )
4065+ datafile_path = dependency_data .get ("datafile_path" )
4066+ if not datafile_resource and datafile_path :
4067+ if strip_datafile_path_root :
4068+ segments = datafile_path .split ("/" )
4069+ datafile_path = "/" .join (segments [1 :])
4070+ datafile_resource = project .codebaseresources .get (path = datafile_path )
40294071
40304072 if datasource_id :
40314073 dependency_data ["datasource_id" ] = datasource_id
40324074
4033- # Set purl fields from ` purl`
4075+ # Set package_url fields from the `` purl`` string.
40344076 purl = dependency_data .get ("purl" )
4035- purl_mapping = PackageURL .from_string (purl ).to_dict ()
4036- dependency_data .update (** purl_mapping )
4077+ if purl :
4078+ purl_data_dict = PackageURL .from_string (purl ).to_dict ()
4079+ dependency_data .update (** purl_data_dict )
40374080
40384081 cleaned_data = {
40394082 field_name : value
@@ -4072,7 +4115,7 @@ def spdx_id(self):
40724115 # "SPDXID is a unique string containing letters, numbers, ., and/or -"
40734116 return f"SPDXRef-scancodeio-{ self ._meta .model_name } -{ self .uuid } "
40744117
4075- def as_spdx (self ):
4118+ def as_spdx_package (self ):
40764119 """Return this Dependency as an SPDX Package entry."""
40774120 from scanpipe .pipes import spdx
40784121
0 commit comments