27
27
from macaron .json_tools import json_extract
28
28
from macaron .malware_analyzer .datetime_parser import parse_datetime
29
29
from 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
31
31
32
32
if TYPE_CHECKING :
33
33
from macaron .slsa_analyzer .specs .package_registry_spec import PackageRegistryInfo
@@ -469,6 +469,33 @@ def extract_attestation(attestation_data: dict) -> dict | None:
469
469
return attestations [0 ]
470
470
471
471
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
+
472
499
@dataclass
473
500
class PyPIPackageJsonAsset :
474
501
"""The package JSON hosted on the PyPI registry."""
@@ -491,6 +518,9 @@ class PyPIPackageJsonAsset:
491
518
#: the source code temporary location name
492
519
package_sourcecode_path : str
493
520
521
+ #: the pypi inspector information about this package
522
+ inspector_asset : PyPIInspectorAsset
523
+
494
524
#: The size of the asset (in bytes). This attribute is added to match the AssetLocator
495
525
#: protocol and is not used because pypi API registry does not provide it.
496
526
@property
@@ -762,6 +792,91 @@ def get_sha256(self) -> str | None:
762
792
logger .debug ("Found sha256 hash: %s" , artifact_hash )
763
793
return artifact_hash
764
794
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
+
765
880
766
881
def find_or_create_pypi_asset (
767
882
asset_name : str , asset_version : str | None , pypi_registry_info : PackageRegistryInfo
@@ -799,6 +914,8 @@ def find_or_create_pypi_asset(
799
914
logger .debug ("Failed to create PyPIPackageJson asset." )
800
915
return None
801
916
802
- asset = PyPIPackageJsonAsset (asset_name , asset_version , False , package_registry , {}, "" )
917
+ asset = PyPIPackageJsonAsset (
918
+ asset_name , asset_version , False , package_registry , {}, "" , PyPIInspectorAsset ("" , [], {})
919
+ )
803
920
pypi_registry_info .metadata .append (asset )
804
921
return asset
0 commit comments