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,57 @@ def from_url(cls, url):
194195 return cls (url = url )
195196
196197
198+ @dataclasses .dataclass (eq = True )
199+ @functools .total_ordering
200+ class Commit :
201+ commit_hash : str
202+ vcs_url : str
203+
204+ commit_rank : Optional [int ] = 0
205+ commit_author : Optional [str ] = None
206+ commit_message : Optional [str ] = None
207+ # commit_date: Optional[datetime] = None
208+
209+ def __post_init__ (self ):
210+ if not self .commit_hash :
211+ raise ValueError ("Commit must have a non-empty commit_hash." )
212+ if not self .vcs_url :
213+ raise ValueError ("Commit must have a non-empty vcs_url." )
214+ if not isinstance (self .commit_hash , str ):
215+ self .commit_hash = str (self .commit_hash )
216+
217+ def _cmp_key (self ):
218+ return (self .commit_rank , self .commit_hash , self .vcs_url )
219+
220+ def __lt__ (self , other ):
221+ if not isinstance (other , Commit ):
222+ return NotImplemented
223+ return self .commit_rank < other .commit_rank
224+
225+ def to_dict (self ) -> dict :
226+ """Return a normalized dictionary representation of the commit."""
227+ return {
228+ "commit_hash" : self .commit_hash ,
229+ "vcs_url" : self .vcs_url ,
230+ "commit_rank" : self .commit_rank ,
231+ "commit_author" : self .commit_author ,
232+ "commit_message" : self .commit_message ,
233+ # "commit_date": self.commit_date.isoformat() if self.commit_date else None,
234+ }
235+
236+ @classmethod
237+ def from_dict (cls , data : dict ) -> "Commit" :
238+ """Create a Commit instance from a dictionary."""
239+ return cls (
240+ commit_hash = str (data .get ("commit_hash" , "" )),
241+ vcs_url = data .get ("vcs_url" , "" ),
242+ commit_rank = data .get ("commit_rank" , 0 ),
243+ commit_author = data .get ("commit_author" ),
244+ commit_message = data .get ("commit_message" ),
245+ # commit_date=data.get("commit_date"),
246+ )
247+
248+
197249class UnMergeablePackageError (Exception ):
198250 """
199251 Raised when a package cannot be merged with another one.
@@ -218,6 +270,8 @@ class AffectedPackage:
218270 package : PackageURL
219271 affected_version_range : Optional [VersionRange ] = None
220272 fixed_version : Optional [Version ] = None
273+ fixed_by_commits : List [Commit ] = dataclasses .field (default_factory = list )
274+ affected_by_commits : List [Commit ] = dataclasses .field (default_factory = list )
221275
222276 def __post_init__ (self ):
223277 if self .package .version :
@@ -248,6 +302,8 @@ def _cmp_key(self):
248302 str (self .package ),
249303 str (self .affected_version_range or "" ),
250304 str (self .fixed_version or "" ),
305+ str (self .affected_by_commits or []),
306+ str (self .fixed_by_commits or []),
251307 )
252308
253309 @classmethod
@@ -294,6 +350,12 @@ def to_dict(self):
294350 "package" : purl_to_dict (self .package ),
295351 "affected_version_range" : affected_version_range ,
296352 "fixed_version" : str (self .fixed_version ) if self .fixed_version else None ,
353+ "affected_by_commits" : [
354+ affected_by_commit .to_dict () for affected_by_commit in self .affected_by_commits
355+ ],
356+ "fixed_by_commits" : [
357+ fixed_by_commit .to_dict () for fixed_by_commit in self .fixed_by_commits
358+ ],
297359 }
298360
299361 @classmethod
@@ -304,6 +366,8 @@ def from_dict(cls, affected_pkg: dict):
304366 package = PackageURL (** affected_pkg ["package" ])
305367 affected_version_range = None
306368 affected_range = affected_pkg ["affected_version_range" ]
369+ affected_by_commits = affected_pkg .get ("affected_by_commits" ) or []
370+ fixed_by_commits = affected_pkg .get ("fixed_by_commits" ) or []
307371
308372 # TODO: "None" is a likely bug
309373 if affected_range and affected_range != "None" :
@@ -335,6 +399,12 @@ def from_dict(cls, affected_pkg: dict):
335399 package = package ,
336400 affected_version_range = affected_version_range ,
337401 fixed_version = fixed_version ,
402+ affected_by_commits = [
403+ Commit .from_dict (affected_by_commit ) for affected_by_commit in affected_by_commits
404+ ],
405+ fixed_by_commits = [
406+ Commit .from_dict (fixed_by_commit ) for fixed_by_commit in fixed_by_commits
407+ ],
338408 )
339409
340410
@@ -350,6 +420,8 @@ class AffectedPackageV2:
350420 package : PackageURL
351421 affected_version_range : Optional [VersionRange ] = None
352422 fixed_version_range : Optional [VersionRange ] = None
423+ fixed_by_commits : List [Commit ] = dataclasses .field (default_factory = list )
424+ affected_by_commits : List [Commit ] = dataclasses .field (default_factory = list )
353425
354426 def __post_init__ (self ):
355427 if self .package .version :
@@ -372,6 +444,8 @@ def _cmp_key(self):
372444 str (self .package ),
373445 str (self .affected_version_range or "" ),
374446 str (self .fixed_version_range or "" ),
447+ str (self .affected_by_commits or []),
448+ str (self .fixed_by_commits or []),
375449 )
376450
377451 def to_dict (self ):
@@ -385,6 +459,12 @@ def to_dict(self):
385459 "package" : purl_to_dict (self .package ),
386460 "affected_version_range" : affected_version_range ,
387461 "fixed_version_range" : fixed_version_range ,
462+ "affected_by_commits" : [
463+ affected_by_commit .to_dict () for affected_by_commit in self .affected_by_commits
464+ ],
465+ "fixed_by_commits" : [
466+ fixed_by_commit .to_dict () for fixed_by_commit in self .fixed_by_commits
467+ ],
388468 }
389469
390470 @classmethod
@@ -396,6 +476,8 @@ def from_dict(cls, affected_pkg: dict):
396476 fixed_version_range = None
397477 affected_range = affected_pkg ["affected_version_range" ]
398478 fixed_range = affected_pkg ["fixed_version_range" ]
479+ affected_by_commits = affected_pkg .get ("affected_by_commits" ) or []
480+ fixed_by_commits = affected_pkg .get ("fixed_by_commits" ) or []
399481
400482 try :
401483 affected_version_range = VersionRange .from_string (affected_range )
@@ -413,10 +495,27 @@ def from_dict(cls, affected_pkg: dict):
413495 )
414496 return
415497
498+ invalid_fix_commits = [c for c in fixed_by_commits if not is_commit (c .commit_hash )]
499+ invalid_affected_commits = [c for c in affected_by_commits if not is_commit (c .commit_hash )]
500+
501+ if invalid_fix_commits or invalid_affected_commits :
502+ logger .error (
503+ f"Invalid commit hash(es) found. "
504+ f"Invalid fixed_by_commits: { invalid_fix_commits } , "
505+ f"Invalid affected_by_commits: { invalid_affected_commits } "
506+ )
507+ return
508+
416509 return cls (
417510 package = package ,
418511 affected_version_range = affected_version_range ,
419512 fixed_version_range = fixed_version_range ,
513+ affected_by_commits = [
514+ Commit .from_dict (affected_by_commit ) for affected_by_commit in affected_by_commits
515+ ],
516+ fixed_by_commits = [
517+ Commit .from_dict (fixed_by_commit ) for fixed_by_commit in fixed_by_commits
518+ ],
420519 )
421520
422521
0 commit comments