@@ -3772,6 +3772,18 @@ class DiscoveredDependencyQuerySet(
3772
3772
VulnerabilityQuerySetMixin ,
3773
3773
ProjectRelatedQuerySet ,
3774
3774
):
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
+
3775
3787
def prefetch_for_serializer (self ):
3776
3788
"""
3777
3789
Optimized prefetching for a QuerySet to be consumed by the
@@ -3816,6 +3828,26 @@ class DiscoveredDependency(
3816
3828
system and application packages discovered in the code under analysis.
3817
3829
Dependencies are usually collected from parsed package data such as a package
3818
3830
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.
3819
3851
"""
3820
3852
3821
3853
# Overrides the `project` field to set the proper `related_name`.
@@ -3966,6 +3998,24 @@ def datafile_path(self):
3966
3998
if self .datafile_resource :
3967
3999
return self .datafile_resource .path
3968
4000
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
+
3969
4019
@classmethod
3970
4020
def create_from_data (
3971
4021
cls ,
@@ -3981,6 +4031,14 @@ def create_from_data(
3981
4031
Create and returns a DiscoveredDependency for a `project` from the
3982
4032
`dependency_data`.
3983
4033
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
+
3984
4042
If `strip_datafile_path_root` is True, then `create_from_data()` will
3985
4043
strip the root path segment from the `datafile_path` of
3986
4044
`dependency_data` before looking up the corresponding CodebaseResource
@@ -3989,51 +4047,36 @@ def create_from_data(
3989
4047
not stripped for `datafile_path`.
3990
4048
"""
3991
4049
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
3998
4051
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 ())
4004
4054
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 )
4014
4058
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
+ )
4021
4064
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 )
4029
4071
4030
4072
if datasource_id :
4031
4073
dependency_data ["datasource_id" ] = datasource_id
4032
4074
4033
- # Set purl fields from ` purl`
4075
+ # Set package_url fields from the `` purl`` string.
4034
4076
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 )
4037
4080
4038
4081
cleaned_data = {
4039
4082
field_name : value
@@ -4072,7 +4115,7 @@ def spdx_id(self):
4072
4115
# "SPDXID is a unique string containing letters, numbers, ., and/or -"
4073
4116
return f"SPDXRef-scancodeio-{ self ._meta .model_name } -{ self .uuid } "
4074
4117
4075
- def as_spdx (self ):
4118
+ def as_spdx_package (self ):
4076
4119
"""Return this Dependency as an SPDX Package entry."""
4077
4120
from scanpipe .pipes import spdx
4078
4121
0 commit comments