@@ -41,6 +41,8 @@ class InputFileType(str, Enum):
4141 REQUIREMENTS = "requirements"
4242 # Poetry lock file ("poetry.lock")
4343 POETRY_LOCK = "poetry.lock"
44+ # uv lock file ("uv.lock")
45+ UV_LOCK = "uv.lock"
4446
4547
4648@dataclass
@@ -406,11 +408,15 @@ def determine_file_type(self, full_filename: str) -> InputFileType:
406408 return InputFileType .REQUIREMENTS
407409
408410 if (filename == "poetry.lock" ):
409- data = self .read_poetry_lock_file (full_filename )
411+ data = self .read_lock_file (full_filename , filename )
410412 if data :
411413 LOG .debug ("Guessing poetry.lock file" )
412414 return InputFileType .POETRY_LOCK
413415
416+ if (filename == "uv.lock" ):
417+ LOG .debug ("Guessing uv.lock file" )
418+ return InputFileType .UV_LOCK
419+
414420 # default
415421 LOG .debug ("Use default type: requirements file" )
416422 return InputFileType .REQUIREMENTS
@@ -435,27 +441,33 @@ def read_toml_file(self, filename: str, err_hint: str = "") -> Dict[str, Any]:
435441
436442 return {}
437443
438- def read_poetry_lock_file (self , filename : str ) -> Dict [str , Any ]:
444+ def read_lock_file (self , filename : str , hint : str ) -> Dict [str , Any ]:
439445 """
440446 Ready a poetry.lock file, a TOML file.
441447 """
442- return self .read_toml_file (filename , "poetry.lock" )
448+ return self .read_toml_file (filename , hint )
443449
444450 def read_pyproject_file (self , filename : str ) -> Dict [str , Any ]:
445451 """
446452 Ready a pyproject.toml file, a TOML file.
447453 """
448454 return self .read_toml_file (filename , "pyproject.toml" )
449455
450- def get_all_lock_file_entries (self , lock_filename : str ) -> List [LockFileEntry ]:
456+ def get_all_poetry_lock_file_entries (self , lock_filename : str ) -> List [LockFileEntry ]:
451457 """Extract information about *all* dependencies from the lock file."""
452- poetry_lock = self .read_poetry_lock_file (lock_filename )
453- poetry_lock_metadata = poetry_lock ["metadata" ]
458+ poetry_lock = self .read_lock_file (lock_filename , "poetry.lock" )
459+
460+ if "metadata" in poetry_lock :
461+ poetry_lock_metadata = poetry_lock ["metadata" ]
462+ else :
463+ poetry_lock_metadata = {
464+ "lock-version" : "2.1"
465+ }
454466 entry_list : List [LockFileEntry ] = []
455467 try :
456468 poetry_lock_version = tuple (int (p ) for p in str (poetry_lock_metadata ["lock-version" ]).split ("." ))
457469 except Exception :
458- poetry_lock_version = ( 0 , )
470+ poetry_lock_version = tuple (( 2 , 0 ) )
459471 LOG .debug (f"poetry_lock_version: { poetry_lock_version } " )
460472
461473 for package in poetry_lock ["package" ]:
@@ -469,13 +481,15 @@ def get_all_lock_file_entries(self, lock_filename: str) -> List[LockFileEntry]:
469481 continue
470482
471483 entry = LockFileEntry (name , version , description , [], [], False )
472- package_files = package ["files" ] \
473- if poetry_lock_version >= (2 ,) \
474- else poetry_lock_metadata ["files" ][package ["name" ]]
475- for file_metadata in package_files :
476- entry .files .append (FileEntry (
477- file_metadata ["file" ],
478- file_metadata ["hash" ]))
484+ if "files" in package :
485+ # poetry.lock version 2.x
486+ package_files = package ["files" ] \
487+ if poetry_lock_version >= (2 ,) \
488+ else poetry_lock_metadata ["files" ][package ["name" ]]
489+ for file_metadata in package_files :
490+ entry .files .append (FileEntry (
491+ file_metadata ["file" ],
492+ file_metadata ["hash" ]))
479493
480494 for dep in package .get ("dependencies" , []):
481495 dep_name = GetPythonDependencies .normalize_packagename (dep )
@@ -486,6 +500,35 @@ def get_all_lock_file_entries(self, lock_filename: str) -> List[LockFileEntry]:
486500
487501 return entry_list
488502
503+ def get_all_uv_lock_file_entries (self , lock_filename : str ) -> List [LockFileEntry ]:
504+ """Extract information about *all* dependencies from the lock file."""
505+ uv_lock = self .read_lock_file (lock_filename , "uv.lock" )
506+
507+ entry_list : List [LockFileEntry ] = []
508+ try :
509+ uv_lock_version = tuple ((uv_lock .get ("version" , 0 ), uv_lock .get ("revision" , 0 )))
510+ except Exception :
511+ uv_lock_version = tuple ((1 , 3 ))
512+ LOG .debug (f"uv_lock_version: { uv_lock_version } " )
513+
514+ for package in uv_lock ["package" ]:
515+ name = GetPythonDependencies .normalize_packagename (package .get ("name" , "" ).strip ())
516+ version = package .get ("version" , "" ).strip ()
517+ # no description in uv.lock
518+ LOG .debug (f" Processing raw package: { name } , { version } " )
519+
520+ entry = LockFileEntry (name , version , "" , [], [], False )
521+ # no files in uv.lock
522+
523+ for dep in package .get ("dependencies" , []):
524+ dep_name = GetPythonDependencies .normalize_packagename (dep ["name" ])
525+ LOG .debug (f" Dependency: { dep_name } " )
526+ entry .dependencies .append (dep_name )
527+
528+ entry_list .append (entry )
529+
530+ return entry_list
531+
489532 def find_lock_entry (self , name : str , entries : List [LockFileEntry ]) -> Optional [LockFileEntry ]:
490533 for entry in entries :
491534 if name == entry .name :
@@ -528,16 +571,17 @@ def get_lock_file_entries_for_sbom(self,
528571 # => return all dependencies
529572 return all_entries
530573
531- poetry2xflag = False
574+ new_pyproject_format = False
532575 if "project" in pyproject_info :
576+ # this is for poetry >= 2.0 and uv
533577 cfg = pyproject_info ["project" ]
534- poetry2xflag = True
578+ new_pyproject_format = True
535579 else :
536580 cfg = pyproject_info ["tool" ]["poetry" ]
537581 # get only real dependencies
538582 dependencies = cfg .get ("dependencies" , [])
539583 for dep in dependencies :
540- if poetry2xflag :
584+ if new_pyproject_format :
541585 dep = self .get_pure_dep_name (dep )
542586 dep_name = GetPythonDependencies .normalize_packagename (dep )
543587 if dep_name .lower () == "python" :
@@ -568,7 +612,56 @@ def sbom_from_poetry_lock_file(self, filename: str, search_meta_data: bool, pack
568612 pyproject_file = os .path .join (folder , "pyproject.toml" )
569613 creator = SbomCreator ()
570614 sbom = creator .create ([], addlicense = True , addprofile = True , addtools = True )
571- entry_list_all = self .get_all_lock_file_entries (filename )
615+ entry_list_all = self .get_all_poetry_lock_file_entries (filename )
616+ entry_list = self .get_lock_file_entries_for_sbom (pyproject_file , entry_list_all )
617+ for package in entry_list :
618+ purl = PackageURL (type = "pypi" , name = package .name , version = package .version )
619+ cxcomp = Component (
620+ name = package .name ,
621+ version = package .version ,
622+ purl = purl ,
623+ bom_ref = purl .to_string (),
624+ description = package .description )
625+
626+ prop = Property (
627+ name = CycloneDxSupport .CDX_PROP_LANGUAGE ,
628+ value = "Python" )
629+ cxcomp .properties .add (prop )
630+
631+ if search_meta_data :
632+ self .add_meta_data_to_bomitem (cxcomp , package_source )
633+ else :
634+ LOG .debug (" Processing package_files" )
635+ for file_metadata in package .files :
636+ LOG .debug (f" Processing file_metadata: { file_metadata } " )
637+ try :
638+ cxcomp .external_references .add (ExternalReference (
639+ type = ExternalReferenceType .DISTRIBUTION ,
640+ url = XsUri (cxcomp .get_pypi_url ()),
641+ # comment=f'Distribution file: {file_metadata.file}',
642+ comment = CaPyCliBom .BINARY_URL_COMMENT ,
643+ hashes = [HashType .from_composite_str (file_metadata .hash )]
644+ ))
645+ except Exception as ex :
646+ # IGNORE
647+ LOG .debug (" Ignored error: " + repr (ex ))
648+ pass
649+
650+ sbom .components .add (cxcomp )
651+
652+ return sbom
653+
654+ def sbom_from_uv_lock_file (self , filename : str , search_meta_data : bool , package_source : str = "" ) -> Bom :
655+ folder = os .path .dirname (filename )
656+
657+ if self .proj_file_override :
658+ # override for unit tests
659+ pyproject_file = self .proj_file_override
660+ else :
661+ pyproject_file = os .path .join (folder , "pyproject.toml" )
662+ creator = SbomCreator ()
663+ sbom = creator .create ([], addlicense = True , addprofile = True , addtools = True )
664+ entry_list_all = self .get_all_uv_lock_file_entries (filename )
572665 entry_list = self .get_lock_file_entries_for_sbom (pyproject_file , entry_list_all )
573666 for package in entry_list :
574667 purl = PackageURL (type = "pypi" , name = package .name , version = package .version )
@@ -700,6 +793,8 @@ def run(self, args: Any) -> None:
700793 datatype = self .determine_file_type (args .inputfile )
701794 if datatype == InputFileType .POETRY_LOCK :
702795 sbom = self .sbom_from_poetry_lock_file (args .inputfile , args .search_meta_data , args .package_source )
796+ elif datatype == InputFileType .UV_LOCK :
797+ sbom = self .sbom_from_uv_lock_file (args .inputfile , args .search_meta_data , args .package_source )
703798 else :
704799 print_text ("Reading input file " + args .inputfile )
705800 package_list = self .requirements_to_package_list (args .inputfile )
0 commit comments