1111 * metadata.component.name starts with --name-prefix (if given)
1212 * metadata.component has a non-empty version
1313 * at least one component or component property recorded
14+ * optional --min-properties N on metadata.component.properties
15+ * optional --require-dep-version NAME: a components[] entry with that
16+ name must exist and carry a non-empty version
1417
1518 SPDX (*.spdx.json):
1619 * spdxVersion starts with "SPDX-2"
1720 * has a name and at least one package
21+ * optional --require-dep-version NAME: a packages[] entry whose name
22+ contains NAME must carry a non-empty versionInfo
1823
1924The file kind is detected by content, so argument order does not matter.
2025
2126Usage:
22- validate_sbom.py [--name-prefix PREFIX] FILE [FILE ...]
27+ validate_sbom.py [--name-prefix PREFIX] [--min-properties N]
28+ [--require-dep-version NAME] FILE [FILE ...]
2329"""
2430
2531import argparse
@@ -32,7 +38,7 @@ def fail(path, msg):
3238 sys .exit (1 )
3339
3440
35- def validate_cyclonedx (path , d , name_prefix ):
41+ def validate_cyclonedx (path , d , name_prefix , min_properties , require_deps ):
3642 if d .get ("bomFormat" ) != "CycloneDX" :
3743 fail (path , f"bomFormat != CycloneDX (got { d .get ('bomFormat' )!r} )" )
3844 if d .get ("specVersion" ) != "1.6" :
@@ -44,21 +50,45 @@ def validate_cyclonedx(path, d, name_prefix):
4450 f"{ name_prefix !r} (got { name !r} )" )
4551 if not comp .get ("version" ):
4652 fail (path , "metadata.component.version is empty" )
47- if not d .get ("components" ) and not comp .get ("properties" ):
53+ props = comp .get ("properties" ) or []
54+ if not d .get ("components" ) and not props :
4855 fail (path , "no components or component properties recorded" )
56+ if min_properties is not None and len (props ) < min_properties :
57+ fail (path , f"metadata.component.properties has { len (props )} entries, "
58+ f"need at least { min_properties } (config capture likely "
59+ f"empty — check --options-h vs --cflags)" )
60+ for dep_name in require_deps :
61+ matches = [c for c in (d .get ("components" ) or [])
62+ if c .get ("name" ) == dep_name ]
63+ if not matches :
64+ fail (path , f"required dependency component { dep_name !r} missing" )
65+ if not matches [0 ].get ("version" ):
66+ fail (path , f"dependency component { dep_name !r} has no version "
67+ f"(pass --dep-version or set WOLFSSL_DIR)" )
4968 print (f"OK [{ path } ]: CycloneDX 1.6, component "
50- f"{ comp .get ('name' )} { comp .get ('version' )} " )
69+ f"{ comp .get ('name' )} { comp .get ('version' )} , "
70+ f"{ len (props )} properties" )
5171
5272
53- def validate_spdx (path , d ):
73+ def validate_spdx (path , d , require_deps ):
5474 ver = d .get ("spdxVersion" , "" )
5575 if not ver .startswith ("SPDX-2" ):
5676 fail (path , f"spdxVersion not SPDX-2.x (got { ver !r} )" )
5777 if not d .get ("name" ):
5878 fail (path , "document name is empty" )
59- if not d .get ("packages" ):
79+ pkgs = d .get ("packages" ) or []
80+ if not pkgs :
6081 fail (path , "no packages recorded" )
61- print (f"OK [{ path } ]: { ver } , { len (d ['packages' ])} package(s)" )
82+ for dep_name in require_deps :
83+ matches = [p for p in pkgs
84+ if dep_name .lower () in (p .get ("name" ) or "" ).lower ()]
85+ if not matches :
86+ fail (path , f"required dependency package matching { dep_name !r} "
87+ f"missing" )
88+ if not matches [0 ].get ("versionInfo" ):
89+ fail (path , f"dependency package { matches [0 ].get ('name' )!r} has "
90+ f"no versionInfo" )
91+ print (f"OK [{ path } ]: { ver } , { len (pkgs )} package(s)" )
6292
6393
6494def main (argv ):
@@ -67,6 +97,13 @@ def main(argv):
6797 formatter_class = argparse .RawDescriptionHelpFormatter )
6898 ap .add_argument ("--name-prefix" , default = "" ,
6999 help = "Require metadata.component.name to start with this." )
100+ ap .add_argument ("--min-properties" , type = int , default = None ,
101+ help = "Require at least N CycloneDX component properties "
102+ "(guards empty --cflags captures)." )
103+ ap .add_argument ("--require-dep-version" , action = "append" , default = [],
104+ metavar = "NAME" ,
105+ help = "Require a dependency component/package NAME with "
106+ "a non-empty version (repeatable)." )
70107 ap .add_argument ("files" , nargs = "+" )
71108 args = ap .parse_args (argv [1 :])
72109
@@ -79,9 +116,10 @@ def main(argv):
79116 except json .JSONDecodeError as e :
80117 fail (path , f"invalid JSON: { e } " )
81118 if "bomFormat" in d or path .endswith (".cdx.json" ):
82- validate_cyclonedx (path , d , args .name_prefix )
119+ validate_cyclonedx (path , d , args .name_prefix ,
120+ args .min_properties , args .require_dep_version )
83121 elif "spdxVersion" in d or path .endswith (".spdx.json" ):
84- validate_spdx (path , d )
122+ validate_spdx (path , d , args . require_dep_version )
85123 else :
86124 fail (path , "unrecognized SBOM format (neither CycloneDX nor SPDX)" )
87125 print ("All SBOMs valid." )
0 commit comments