2323 endor_components_remove ,
2424 endor_components_rename ,
2525 get_semver_from_release_version ,
26- is_valid_purl ,
2726 process_component_special_cases ,
2827)
2928from endorctl_utils import EndorCtl
@@ -54,7 +53,6 @@ def emit(self, record):
5453# Add the handler to the logger
5554logger .addHandler (warning_handler )
5655
57-
5856# Get the absolute path of the script file and directory
5957script_path = Path (__file__ ).resolve ()
6058script_directory = script_path .parent
@@ -66,6 +64,65 @@ def emit(self, record):
6664REGEX_RELEASE_BRANCH = r"^v\d\.\d$"
6765REGEX_RELEASE_TAG = r"^r\d\.\d.\d(-\w*)?$"
6866
67+ # ################ PURL Validation ################
68+ REGEX_STR_PURL_OPTIONAL = ( # Optional Version (any chars except ? @ #)
69+ r"(?:@[^?@#]*)?"
70+ # Optional Qualifiers (any chars except @ #)
71+ r"(?:\?[^@#]*)?"
72+ # Optional Subpath (any chars)
73+ r"(?:#.*)?$"
74+ )
75+
76+ REGEX_PURL = {
77+ # deb PURL. https://github.com/package-url/purl-spec/blob/main/types-doc/deb-definition.md
78+ "deb" : re .compile (
79+ r"^pkg:deb/" # Scheme and type
80+ # Namespace (organization/user), letters must be lowercase
81+ r"(debian|ubuntu)+"
82+ r"/"
83+ r"[a-z0-9._-]+" + REGEX_STR_PURL_OPTIONAL # Name
84+ ),
85+ # Generic PURL. https://github.com/package-url/purl-spec/blob/main/types-doc/generic-definition.md
86+ "generic" : re .compile (
87+ r"^pkg:generic/" # Scheme and type
88+ r"([a-zA-Z0-9._-]+/)?" # Optional namespace segment
89+ r"[a-zA-Z0-9._-]+" + REGEX_STR_PURL_OPTIONAL # Name (required)
90+ ),
91+ # GitHub PURL. https://github.com/package-url/purl-spec/blob/main/types-doc/github-definition.md
92+ "github" : re .compile (
93+ r"^pkg:github/" # Scheme and type
94+ # Namespace (organization/user), letters must be lowercase
95+ r"[a-z0-9-]+"
96+ r"/"
97+ r"[a-z0-9._-]+" + REGEX_STR_PURL_OPTIONAL # Name (repository)
98+ ),
99+ # PyPI PURL. https://github.com/package-url/purl-spec/blob/main/types-doc/pypi-definition.md
100+ "pypi" : re .compile (
101+ r"^pkg:pypi/" # Scheme and type
102+ r"[a-z0-9_-]+" # Name, letters must be lowercase, dashes, underscore
103+ + REGEX_STR_PURL_OPTIONAL
104+ ),
105+ }
106+
107+
108+ # Metadata SBOM requirements
109+ METADATA_FIELDS_REQUIRED = [
110+ "type" ,
111+ "bom-ref" ,
112+ "group" ,
113+ "name" ,
114+ "version" ,
115+ "description" ,
116+ "licenses" ,
117+ "copyright" ,
118+ "externalReferences" ,
119+ "scope" ,
120+ ]
121+ METADATA_FIELDS_ONE_OF = [
122+ ["author" , "supplier" ],
123+ ["purl" , "cpe" ],
124+ ]
125+
69126# endregion init
70127
71128
@@ -80,7 +137,11 @@ def __init__(self):
80137 try :
81138 self .repo_root = Path (
82139 subprocess .run (
83- "git rev-parse --show-toplevel" , shell = True , text = True , capture_output = True
140+ "git rev-parse --show-toplevel" ,
141+ shell = True ,
142+ text = True ,
143+ capture_output = True ,
144+ check = True ,
84145 ).stdout .strip ()
85146 )
86147 self ._repo = Repo (self .repo_root )
@@ -170,6 +231,15 @@ def extract_repo_from_git_url(git_url: str) -> dict:
170231 }
171232
172233
234+ def is_valid_purl (purl : str ) -> bool :
235+ """Validate a GitHub or Generic PURL"""
236+ for purl_type , regex in REGEX_PURL .items ():
237+ if regex .match (purl ):
238+ logger .debug (f"PURL: { purl } matched PURL type '{ purl_type } ' regex '{ regex .pattern } '" )
239+ return True
240+ return False
241+
242+
173243def sbom_components_to_dict (sbom : dict , with_version : bool = False ) -> dict :
174244 """Create a dict of SBOM components with a version-less PURL as the key"""
175245 components = sbom ["components" ]
@@ -185,6 +255,23 @@ def sbom_components_to_dict(sbom: dict, with_version: bool = False) -> dict:
185255 return components_dict
186256
187257
258+ def check_metadata_sbom (meta_bom : dict ) -> None :
259+ for component in meta_bom ["components" ]:
260+ for field in METADATA_FIELDS_REQUIRED :
261+ if field not in component :
262+ logger .warning (
263+ f"METADATA: '{ component ['bom-ref' ] or component ['name' ]} is missing required field '{ field } '."
264+ )
265+ for fields in METADATA_FIELDS_ONE_OF :
266+ found = False
267+ for field in fields :
268+ found = found or field in component
269+ if not found :
270+ logger .warning (
271+ f"METADATA: '{ component ['bom-ref' ] or component ['name' ]} is missing one of fields '{ fields } '."
272+ )
273+
274+
188275def read_sbom_json_file (file_path : str ) -> dict :
189276 """Load a JSON SBOM file (schema is not validated)"""
190277 try :
@@ -204,8 +291,8 @@ def write_sbom_json_file(sbom_dict: dict, file_path: str) -> None:
204291 try :
205292 file_path = os .path .abspath (file_path )
206293 with open (file_path , "w" , encoding = "utf-8" ) as output_json :
207- json .dump (sbom_dict , output_json , indent = 2 )
208- output_json .write (" \n " )
294+ formatted_sbom = json .dumps (sbom_dict , indent = 2 ) + " \n "
295+ output_json .write (formatted_sbom )
209296 except Exception as e :
210297 logger .error (f"Error writing SBOM file to { file_path } " )
211298 logger .error (e )
@@ -449,6 +536,8 @@ def main() -> None:
449536 endor_bom = endorctl .get_sbom_for_branch (git_info .project , git_info .branch )
450537 elif target == "project" :
451538 endor_bom = endorctl .get_sbom_for_project (git_info .project )
539+ else :
540+ endor_bom = None
452541
453542 if not endor_bom :
454543 logger .error ("Empty result for Endor SBOM!" )
@@ -466,9 +555,6 @@ def main() -> None:
466555
467556 ## remove uneeded components ##
468557 # [list]endor_components_remove is defined in config.py
469- # Endor Labs includes the main component in 'components'. This is not standard, so we remove it.
470- endor_components_remove .append (f"pkg:github/{ git_info .org } /{ git_info .repo } " )
471-
472558 # Reverse iterate the SBOM components list to safely modify in situ
473559 for i in range (len (endor_bom ["components" ]) - 1 , - 1 , - 1 ):
474560 component = endor_bom ["components" ][i ]
@@ -529,6 +615,9 @@ def main() -> None:
529615 meta_bom ["components" ].sort (key = lambda c : c ["bom-ref" ])
530616 prev_bom ["components" ].sort (key = lambda c : c ["bom-ref" ])
531617
618+ # Check metadata SBOM for completeness
619+ check_metadata_sbom (meta_bom )
620+
532621 # Create SBOM component lookup dicts
533622 endor_components = sbom_components_to_dict (endor_bom )
534623 prev_components = sbom_components_to_dict (prev_bom )
@@ -537,7 +626,7 @@ def main() -> None:
537626
538627 # Attempt to determine the MongoDB Version being scanned
539628 logger .debug (
540- f"Available MongoDB version options, tag: { git_info .release_tag } , branch: { git_info .branch } , previous SBOM: { prev_bom ['metadata' ]. get ( 'component' ,{}). get ( 'version' ) } "
629+ f"Available MongoDB version options, tag: { git_info .release_tag } , branch: { git_info .branch } , previous SBOM: { prev_bom ['metadata' ][ 'component' ][ 'version' ] } "
541630 )
542631 meta_bom_ref = meta_bom ["metadata" ]["component" ]["bom-ref" ]
543632
0 commit comments