3737from vulnerabilities .severity_systems import ScoringSystem
3838from vulnerabilities .utils import classproperty
3939from vulnerabilities .utils import get_reference_id
40+ from vulnerabilities .utils import is_commit
4041from vulnerabilities .utils import is_cve
4142from vulnerabilities .utils import nearest_patched_package
4243from vulnerabilities .utils import purl_to_dict
@@ -194,6 +195,65 @@ def from_url(cls, url):
194195 return cls (url = url )
195196
196197
198+ """
199+ For VCS URLs that can currently be formed into PURLs (github, bitbucket, and gitlab),
200+ we support full code commit collection.
201+
202+ For any VCS URL types not included in this set, CodeCommit objects will not be
203+ created at this time. Instead, unsupported VCS URLs will be stored only as
204+ references, serving as a fallback until we support them.
205+ """
206+ VCS_URLS_SUPPORTED_TYPES = {"github" , "bitbucket" , "gitlab" }
207+
208+
209+ @dataclasses .dataclass (eq = True )
210+ @functools .total_ordering
211+ class CodePatchData :
212+ commit_hash : str
213+ vcs_url : str
214+ commit_patch : Optional [str ] = None
215+
216+ def __post_init__ (self ):
217+ if not self .commit_hash :
218+ raise ValueError ("Commit must have a non-empty commit_hash." )
219+
220+ if not is_commit (self .commit_hash ):
221+ raise ValueError (f"Commit must be a valid a commit_hash: { self .commit_hash } ." )
222+
223+ if not self .vcs_url :
224+ raise ValueError ("Commit must have a non-empty vcs_url." )
225+
226+ def __lt__ (self , other ):
227+ if not isinstance (other , CodePatchData ):
228+ return NotImplemented
229+ return self ._cmp_key () < other ._cmp_key ()
230+
231+ # TODO: Add cache
232+ def _cmp_key (self ):
233+ return (
234+ self .commit_hash ,
235+ self .vcs_url ,
236+ self .commit_patch ,
237+ )
238+
239+ def to_dict (self ) -> dict :
240+ """Return a normalized dictionary representation of the commit."""
241+ return {
242+ "commit_hash" : self .commit_hash ,
243+ "vcs_url" : self .vcs_url ,
244+ "commit_patch" : self .commit_patch ,
245+ }
246+
247+ @classmethod
248+ def from_dict (cls , data : dict ):
249+ """Create a Commit instance from a dictionary."""
250+ return cls (
251+ commit_hash = data .get ("commit_hash" ),
252+ vcs_url = data .get ("vcs_url" ),
253+ commit_patch = data .get ("commit_patch" ),
254+ )
255+
256+
197257class UnMergeablePackageError (Exception ):
198258 """
199259 Raised when a package cannot be merged with another one.
@@ -344,21 +404,28 @@ class AffectedPackageV2:
344404 """
345405 Relate a Package URL with a range of affected versions and fixed versions.
346406 The Package URL must *not* have a version.
347- AffectedPackage must contain either ``affected_version_range`` or ``fixed_version_range``.
407+ AffectedPackage must contain either ``affected_version_range`` or ``fixed_version_range`` or ``introduced_by_commits`` or ``fixed_by_commits`` .
348408 """
349409
350410 package : PackageURL
351411 affected_version_range : Optional [VersionRange ] = None
352412 fixed_version_range : Optional [VersionRange ] = None
413+ introduced_by_commits : List [CodePatchData ] = dataclasses .field (default_factory = list )
414+ fixed_by_commits : List [CodePatchData ] = dataclasses .field (default_factory = list )
353415
354416 def __post_init__ (self ):
355417 if self .package .version :
356418 raise ValueError (f"Affected Package URL { self .package !r} cannot have a version." )
357419
358- if not (self .affected_version_range or self .fixed_version_range ):
420+ if not (
421+ self .affected_version_range
422+ or self .fixed_version_range
423+ or self .introduced_by_commits
424+ or self .fixed_by_commits
425+ ):
359426 raise ValueError (
360- f"Affected Package { self .package !r} should have either fixed version range or an "
361- "affected version range."
427+ f"Affected package { self .package !r} must have either a fixed version range, "
428+ "an affected version range, introduced commits, or fixed commits ."
362429 )
363430
364431 def __lt__ (self , other ):
@@ -372,6 +439,8 @@ def _cmp_key(self):
372439 str (self .package ),
373440 str (self .affected_version_range or "" ),
374441 str (self .fixed_version_range or "" ),
442+ str (self .introduced_by_commits or []),
443+ str (self .fixed_by_commits or []),
375444 )
376445
377446 def to_dict (self ):
@@ -385,6 +454,8 @@ def to_dict(self):
385454 "package" : purl_to_dict (self .package ),
386455 "affected_version_range" : affected_version_range ,
387456 "fixed_version_range" : fixed_version_range ,
457+ "introduced_by_commits" : [commit .to_dict () for commit in self .introduced_by_commits ],
458+ "fixed_by_commits" : [commit .to_dict () for commit in self .fixed_by_commits ],
388459 }
389460
390461 @classmethod
@@ -396,6 +467,8 @@ def from_dict(cls, affected_pkg: dict):
396467 fixed_version_range = None
397468 affected_range = affected_pkg ["affected_version_range" ]
398469 fixed_range = affected_pkg ["fixed_version_range" ]
470+ introduced_by_commits = affected_pkg .get ("introduced_by_commits" ) or []
471+ fixed_by_commits = affected_pkg .get ("fixed_by_commits" ) or []
399472
400473 try :
401474 affected_version_range = VersionRange .from_string (affected_range )
@@ -417,6 +490,10 @@ def from_dict(cls, affected_pkg: dict):
417490 package = package ,
418491 affected_version_range = affected_version_range ,
419492 fixed_version_range = fixed_version_range ,
493+ introduced_by_commits = [
494+ CodePatchData .from_dict (commit ) for commit in introduced_by_commits
495+ ],
496+ fixed_by_commits = [CodePatchData .from_dict (commit ) for commit in fixed_by_commits ],
420497 )
421498
422499
0 commit comments