2727from  macaron .json_tools  import  json_extract 
2828from  macaron .malware_analyzer .datetime_parser  import  parse_datetime 
2929from  macaron .slsa_analyzer .package_registry .package_registry  import  PackageRegistry 
30- from  macaron .util  import  send_get_http_raw 
30+ from  macaron .util  import  send_get_http_raw ,  send_head_http_raw 
3131
3232if  TYPE_CHECKING :
3333    from  macaron .slsa_analyzer .specs .package_registry_spec  import  PackageRegistryInfo 
@@ -469,6 +469,33 @@ def extract_attestation(attestation_data: dict) -> dict | None:
469469        return  attestations [0 ]
470470
471471
472+ # as per https://github.com/pypi/inspector/blob/main/inspector/main.py line 125 
473+ INSPECTOR_TEMPLATE  =  (
474+     "{inspector_url_scheme}://{inspector_url_netloc}/project/" 
475+     "{name}/{version}/packages/{first}/{second}/{rest}/{filename}" 
476+ )
477+ 
478+ 
479+ @dataclass  
480+ class  PyPIInspectorAsset :
481+     """The package PyPI inspector information.""" 
482+ 
483+     #: the pypi inspector link to the tarball 
484+     package_sdist_link : str 
485+ 
486+     #: the pypi inspector link(s) to the wheel(s) 
487+     package_whl_links : list [str ]
488+ 
489+     #: a mapping of inspector links to whether they are reachable 
490+     package_link_reachability : dict [str , bool ]
491+ 
492+     def  __bool__ (self ) ->  bool :
493+         """Determine if this inspector object is empty.""" 
494+         if  (self .package_sdist_link  or  self .package_whl_links ) and  self .package_link_reachability :
495+             return  True 
496+         return  False 
497+ 
498+ 
472499@dataclass  
473500class  PyPIPackageJsonAsset :
474501    """The package JSON hosted on the PyPI registry.""" 
@@ -491,6 +518,9 @@ class PyPIPackageJsonAsset:
491518    #: the source code temporary location name 
492519    package_sourcecode_path : str 
493520
521+     #: the pypi inspector information about this package 
522+     inspector_asset : PyPIInspectorAsset 
523+ 
494524    #: The size of the asset (in bytes). This attribute is added to match the AssetLocator 
495525    #: protocol and is not used because pypi API registry does not provide it. 
496526    @property  
@@ -762,6 +792,91 @@ def get_sha256(self) -> str | None:
762792        logger .debug ("Found sha256 hash: %s" , artifact_hash )
763793        return  artifact_hash 
764794
795+     def  get_inspector_links (self ) ->  bool :
796+         """Generate PyPI inspector links for this package version's distributions and fill in the inspector asset. 
797+ 
798+         Returns 
799+         ------- 
800+         bool 
801+             True if the link generation was successful, False otherwise. 
802+         """ 
803+         if  self .inspector_asset :
804+             return  True 
805+ 
806+         if  not  self .package_json  and  not  self .download ("" ):
807+             logger .warning ("No package metadata available, cannot get links" )
808+             return  False 
809+ 
810+         releases  =  self .get_releases ()
811+         if  releases  is  None :
812+             logger .warning ("Package has no releases, cannot create inspector links." )
813+             return  False 
814+ 
815+         version  =  self .component_version 
816+         if  self .component_version  is  None :
817+             version  =  self .get_latest_version ()
818+ 
819+         if  version  is  None :
820+             logger .warning ("No version set, and no latest version exists. cannot create inspector links." )
821+             return  False 
822+ 
823+         distributions  =  json_extract (releases , [version ], list )
824+ 
825+         if  not  distributions :
826+             logger .warning (
827+                 "Package has no distributions for release version %s. Cannot create inspector links." , version 
828+             )
829+             return  False 
830+ 
831+         for  distribution  in  distributions :
832+             package_type  =  json_extract (distribution , ["packagetype" ], str )
833+             if  package_type  is  None :
834+                 logger .warning ("The version %s has no 'package type' field in a distribution" , version )
835+                 continue 
836+ 
837+             name  =  json_extract (self .package_json , ["info" , "name" ], str )
838+             if  name  is  None :
839+                 logger .warning ("The version %s has no 'name' field in a distribution" , version )
840+                 continue 
841+ 
842+             blake2b_256  =  json_extract (distribution , ["digests" , "blake2b_256" ], str )
843+             if  blake2b_256  is  None :
844+                 logger .warning ("The version %s has no 'blake2b_256' field in a distribution" , version )
845+                 continue 
846+ 
847+             filename  =  json_extract (distribution , ["filename" ], str )
848+             if  filename  is  None :
849+                 logger .warning ("The version %s has no 'filename' field in a distribution" , version )
850+                 continue 
851+ 
852+             link  =  INSPECTOR_TEMPLATE .format (
853+                 inspector_url_scheme = self .pypi_registry .inspector_url_scheme ,
854+                 inspector_url_netloc = self .pypi_registry .inspector_url_netloc ,
855+                 name = name ,
856+                 version = version ,
857+                 first = blake2b_256 [0 :2 ],
858+                 second = blake2b_256 [2 :4 ],
859+                 rest = blake2b_256 [4 :],
860+                 filename = filename ,
861+             )
862+ 
863+             # use a head request because we don't care about the response contents 
864+             reachable  =  False 
865+             if  send_head_http_raw (link ):
866+                 reachable  =  True   # link was reachable 
867+ 
868+             if  package_type  ==  "sdist" :
869+                 self .inspector_asset .package_sdist_link  =  link 
870+                 self .inspector_asset .package_link_reachability [link ] =  reachable 
871+             elif  package_type  ==  "bdist_wheel" :
872+                 self .inspector_asset .package_whl_links .append (link )
873+                 self .inspector_asset .package_link_reachability [link ] =  reachable 
874+             else :  # no other package types exist, so else statement should never occur 
875+                 logger .debug ("Unknown package distribution type: %s" , package_type )
876+ 
877+         # if all distributions were invalid and went along a 'continue' path 
878+         return  bool (self .inspector_asset )
879+ 
765880
766881def  find_or_create_pypi_asset (
767882    asset_name : str , asset_version : str  |  None , pypi_registry_info : PackageRegistryInfo 
@@ -799,6 +914,8 @@ def find_or_create_pypi_asset(
799914        logger .debug ("Failed to create PyPIPackageJson asset." )
800915        return  None 
801916
802-     asset  =  PyPIPackageJsonAsset (asset_name , asset_version , False , package_registry , {}, "" )
917+     asset  =  PyPIPackageJsonAsset (
918+         asset_name , asset_version , False , package_registry , {}, "" , PyPIInspectorAsset ("" , [], {})
919+     )
803920    pypi_registry_info .metadata .append (asset )
804921    return  asset 
0 commit comments