diff options
| -rw-r--r-- | meta/lib/oe/sbom30.py | 159 | ||||
| -rw-r--r-- | meta/lib/oe/spdx30_tasks.py | 60 | ||||
| -rw-r--r-- | meta/lib/oeqa/selftest/cases/spdx.py | 22 |
3 files changed, 106 insertions, 135 deletions
diff --git a/meta/lib/oe/sbom30.py b/meta/lib/oe/sbom30.py index 9a3b188dbb..29cb9e45ad 100644 --- a/meta/lib/oe/sbom30.py +++ b/meta/lib/oe/sbom30.py | |||
| @@ -21,45 +21,8 @@ VEX_VERSION = "1.0.0" | |||
| 21 | 21 | ||
| 22 | SPDX_BUILD_TYPE = "http://openembedded.org/bitbake" | 22 | SPDX_BUILD_TYPE = "http://openembedded.org/bitbake" |
| 23 | 23 | ||
| 24 | 24 | OE_ALIAS_PREFIX = "http://spdxdocs.org/openembedded-alias/by-doc-hash/" | |
| 25 | @oe.spdx30.register(OE_SPDX_BASE + "link-extension") | 25 | OE_DOC_ALIAS_PREFIX = "http://spdxdocs.org/openembedded-alias/doc/" |
| 26 | class OELinkExtension(oe.spdx30.extension_Extension): | ||
| 27 | """ | ||
| 28 | This custom extension controls if an Element creates a symlink based on | ||
| 29 | its SPDX ID in the deploy directory. Some elements may not be able to be | ||
| 30 | linked because they are duplicated in multiple documents (e.g. the bitbake | ||
| 31 | Build Element). Those elements can add this extension and set link_spdx_id | ||
| 32 | to False | ||
| 33 | |||
| 34 | It is in internal extension that should be removed when writing out a final | ||
| 35 | SBoM | ||
| 36 | """ | ||
| 37 | |||
| 38 | CLOSED = True | ||
| 39 | INTERNAL = True | ||
| 40 | |||
| 41 | @classmethod | ||
| 42 | def _register_props(cls): | ||
| 43 | super()._register_props() | ||
| 44 | cls._add_property( | ||
| 45 | "link_spdx_id", | ||
| 46 | oe.spdx30.BooleanProp(), | ||
| 47 | OE_SPDX_BASE + "link-spdx-id", | ||
| 48 | min_count=1, | ||
| 49 | max_count=1, | ||
| 50 | ) | ||
| 51 | |||
| 52 | # The symlinks written to the deploy directory are based on the hash of | ||
| 53 | # the SPDX ID. While this makes it easy to look them up, it can be | ||
| 54 | # difficult to trace a Element to the hashed symlink name. As a | ||
| 55 | # debugging aid, this property is set to the basename of the symlink | ||
| 56 | # when the symlink is created to make it easier to trace | ||
| 57 | cls._add_property( | ||
| 58 | "link_name", | ||
| 59 | oe.spdx30.StringProp(), | ||
| 60 | OE_SPDX_BASE + "link-name", | ||
| 61 | max_count=1, | ||
| 62 | ) | ||
| 63 | 26 | ||
| 64 | 27 | ||
| 65 | @oe.spdx30.register(OE_SPDX_BASE + "id-alias") | 28 | @oe.spdx30.register(OE_SPDX_BASE + "id-alias") |
| @@ -185,18 +148,6 @@ def get_element_link_id(e): | |||
| 185 | return e._id | 148 | return e._id |
| 186 | 149 | ||
| 187 | 150 | ||
| 188 | def set_alias(obj, alias): | ||
| 189 | for ext in obj.extension: | ||
| 190 | if not isinstance(ext, OEIdAliasExtension): | ||
| 191 | continue | ||
| 192 | ext.alias = alias | ||
| 193 | return ext | ||
| 194 | |||
| 195 | ext = OEIdAliasExtension(alias=alias) | ||
| 196 | obj.extension.append(ext) | ||
| 197 | return ext | ||
| 198 | |||
| 199 | |||
| 200 | def get_alias(obj): | 151 | def get_alias(obj): |
| 201 | for ext in obj.extension: | 152 | for ext in obj.extension: |
| 202 | if not isinstance(ext, OEIdAliasExtension): | 153 | if not isinstance(ext, OEIdAliasExtension): |
| @@ -206,6 +157,10 @@ def get_alias(obj): | |||
| 206 | return None | 157 | return None |
| 207 | 158 | ||
| 208 | 159 | ||
| 160 | def hash_id(_id): | ||
| 161 | return hashlib.sha256(_id.encode("utf-8")).hexdigest() | ||
| 162 | |||
| 163 | |||
| 209 | def to_list(l): | 164 | def to_list(l): |
| 210 | if isinstance(l, set): | 165 | if isinstance(l, set): |
| 211 | l = sorted(list(l)) | 166 | l = sorted(list(l)) |
| @@ -220,6 +175,7 @@ class ObjectSet(oe.spdx30.SHACLObjectSet): | |||
| 220 | def __init__(self, d): | 175 | def __init__(self, d): |
| 221 | super().__init__() | 176 | super().__init__() |
| 222 | self.d = d | 177 | self.d = d |
| 178 | self.alias_prefix = None | ||
| 223 | 179 | ||
| 224 | def create_index(self): | 180 | def create_index(self): |
| 225 | self.by_sha256_hash = {} | 181 | self.by_sha256_hash = {} |
| @@ -230,11 +186,10 @@ class ObjectSet(oe.spdx30.SHACLObjectSet): | |||
| 230 | if isinstance(obj, oe.spdx30.Element): | 186 | if isinstance(obj, oe.spdx30.Element): |
| 231 | if not obj._id: | 187 | if not obj._id: |
| 232 | raise ValueError("Element missing ID") | 188 | raise ValueError("Element missing ID") |
| 233 | for ext in obj.extension: | 189 | |
| 234 | if not isinstance(ext, OEIdAliasExtension): | 190 | alias_ext = get_alias(obj) |
| 235 | continue | 191 | if alias_ext is not None and alias_ext.alias: |
| 236 | if ext.alias: | 192 | self.obj_by_id[alias_ext.alias] = obj |
| 237 | self.obj_by_id[ext.alias] = obj | ||
| 238 | 193 | ||
| 239 | for v in obj.verifiedUsing: | 194 | for v in obj.verifiedUsing: |
| 240 | if not isinstance(v, oe.spdx30.Hash): | 195 | if not isinstance(v, oe.spdx30.Hash): |
| @@ -248,6 +203,9 @@ class ObjectSet(oe.spdx30.SHACLObjectSet): | |||
| 248 | super().add_index(obj) | 203 | super().add_index(obj) |
| 249 | if isinstance(obj, oe.spdx30.SpdxDocument): | 204 | if isinstance(obj, oe.spdx30.SpdxDocument): |
| 250 | self.doc = obj | 205 | self.doc = obj |
| 206 | alias_ext = get_alias(obj) | ||
| 207 | if alias_ext is not None and alias_ext.alias: | ||
| 208 | self.alias_prefix = OE_ALIAS_PREFIX + hash_id(alias_ext.alias) + "/" | ||
| 251 | 209 | ||
| 252 | def __filter_obj(self, obj, attr_filter): | 210 | def __filter_obj(self, obj, attr_filter): |
| 253 | return all(getattr(obj, k) == v for k, v in attr_filter.items()) | 211 | return all(getattr(obj, k) == v for k, v in attr_filter.items()) |
| @@ -307,6 +265,21 @@ class ObjectSet(oe.spdx30.SHACLObjectSet): | |||
| 307 | for o in self.foreach_type(oe.spdx30.Element): | 265 | for o in self.foreach_type(oe.spdx30.Element): |
| 308 | self.set_element_alias(o) | 266 | self.set_element_alias(o) |
| 309 | 267 | ||
| 268 | def new_alias_id(self, obj, replace): | ||
| 269 | unihash = self.d.getVar("BB_UNIHASH") | ||
| 270 | namespace = self.get_namespace() + "/" | ||
| 271 | if unihash not in obj._id: | ||
| 272 | bb.warn(f"Unihash {unihash} not found in {obj._id}") | ||
| 273 | return None | ||
| 274 | |||
| 275 | if namespace not in obj._id: | ||
| 276 | bb.warn(f"Namespace {namespace} not found in {obj._id}") | ||
| 277 | return None | ||
| 278 | |||
| 279 | return obj._id.replace(unihash, "UNIHASH").replace( | ||
| 280 | namespace, replace + self.d.getVar("PN") | ||
| 281 | ) | ||
| 282 | |||
| 310 | def remove_internal_extensions(self): | 283 | def remove_internal_extensions(self): |
| 311 | def remove(o): | 284 | def remove(o): |
| 312 | o.extension = [e for e in o.extension if not getattr(e, "INTERNAL", False)] | 285 | o.extension = [e for e in o.extension if not getattr(e, "INTERNAL", False)] |
| @@ -334,21 +307,17 @@ class ObjectSet(oe.spdx30.SHACLObjectSet): | |||
| 334 | 307 | ||
| 335 | alias_ext = get_alias(e) | 308 | alias_ext = get_alias(e) |
| 336 | if alias_ext is None: | 309 | if alias_ext is None: |
| 337 | unihash = self.d.getVar("BB_UNIHASH") | 310 | alias_id = self.new_alias_id(e, self.alias_prefix) |
| 338 | namespace = self.get_namespace() | 311 | if alias_id is not None: |
| 339 | if unihash not in e._id: | 312 | e.extension.append(OEIdAliasExtension(alias=alias_id)) |
| 340 | bb.warn(f"Unihash {unihash} not found in {e._id}") | 313 | elif ( |
| 341 | elif namespace not in e._id: | 314 | alias_ext.alias |
| 342 | bb.warn(f"Namespace {namespace} not found in {e._id}") | 315 | and not isinstance(e, oe.spdx30.SpdxDocument) |
| 343 | else: | 316 | and not alias_ext.alias.startswith(self.alias_prefix) |
| 344 | alias_ext = set_alias( | 317 | ): |
| 345 | e, | 318 | bb.warn( |
| 346 | e._id.replace(unihash, "UNIHASH").replace( | 319 | f"Element {e._id} has alias {alias_ext.alias}, but it should have prefix {self.alias_prefix}" |
| 347 | namespace, | 320 | ) |
| 348 | "http://spdx.org/spdxdocs/openembedded-alias/" | ||
| 349 | + self.d.getVar("PN"), | ||
| 350 | ), | ||
| 351 | ) | ||
| 352 | 321 | ||
| 353 | def new_spdxid(self, *suffix, include_unihash=True): | 322 | def new_spdxid(self, *suffix, include_unihash=True): |
| 354 | items = [self.get_namespace()] | 323 | items = [self.get_namespace()] |
| @@ -812,9 +781,17 @@ class ObjectSet(oe.spdx30.SHACLObjectSet): | |||
| 812 | _id=objset.new_spdxid("document", name), | 781 | _id=objset.new_spdxid("document", name), |
| 813 | name=name, | 782 | name=name, |
| 814 | ) | 783 | ) |
| 815 | document.extension.append(OEIdAliasExtension()) | 784 | |
| 816 | document.extension.append(OELinkExtension(link_spdx_id=False)) | 785 | document.extension.append( |
| 786 | OEIdAliasExtension( | ||
| 787 | alias=objset.new_alias_id( | ||
| 788 | document, | ||
| 789 | OE_DOC_ALIAS_PREFIX + d.getVar("PN") + "/" + name + "/", | ||
| 790 | ), | ||
| 791 | ) | ||
| 792 | ) | ||
| 817 | objset.doc = document | 793 | objset.doc = document |
| 794 | objset.add_index(document) | ||
| 818 | 795 | ||
| 819 | if copy_from_bitbake_doc: | 796 | if copy_from_bitbake_doc: |
| 820 | bb_objset = objset.import_bitbake_build_objset() | 797 | bb_objset = objset.import_bitbake_build_objset() |
| @@ -907,9 +884,7 @@ def jsonld_arch_path(d, arch, subdir, name, deploydir=None): | |||
| 907 | return deploydir / arch / subdir / (name + ".spdx.json") | 884 | return deploydir / arch / subdir / (name + ".spdx.json") |
| 908 | 885 | ||
| 909 | 886 | ||
| 910 | def jsonld_hash_path(_id): | 887 | def jsonld_hash_path(h): |
| 911 | h = hashlib.sha256(_id.encode("utf-8")).hexdigest() | ||
| 912 | |||
| 913 | return Path("by-spdxid-hash") / h[:2], h | 888 | return Path("by-spdxid-hash") / h[:2], h |
| 914 | 889 | ||
| 915 | 890 | ||
| @@ -981,7 +956,7 @@ def write_recipe_jsonld_doc( | |||
| 981 | dest = jsonld_arch_path(d, pkg_arch, subdir, objset.doc.name, deploydir=deploydir) | 956 | dest = jsonld_arch_path(d, pkg_arch, subdir, objset.doc.name, deploydir=deploydir) |
| 982 | 957 | ||
| 983 | def link_id(_id): | 958 | def link_id(_id): |
| 984 | hash_path = jsonld_hash_path(_id) | 959 | hash_path = jsonld_hash_path(hash_id(_id)) |
| 985 | 960 | ||
| 986 | link_name = jsonld_arch_path( | 961 | link_name = jsonld_arch_path( |
| 987 | d, | 962 | d, |
| @@ -1005,28 +980,9 @@ def write_recipe_jsonld_doc( | |||
| 1005 | 980 | ||
| 1006 | try: | 981 | try: |
| 1007 | if create_spdx_id_links: | 982 | if create_spdx_id_links: |
| 1008 | for o in objset.foreach_type(oe.spdx30.Element): | 983 | alias_ext = get_alias(objset.doc) |
| 1009 | if not o._id or o._id.startswith("_:"): | 984 | if alias_ext is not None and alias_ext.alias: |
| 1010 | continue | 985 | alias_ext.link_name = link_id(alias_ext.alias) |
| 1011 | |||
| 1012 | ext = None | ||
| 1013 | for e in o.extension: | ||
| 1014 | if not isinstance(e, OELinkExtension): | ||
| 1015 | continue | ||
| 1016 | |||
| 1017 | ext = e | ||
| 1018 | break | ||
| 1019 | |||
| 1020 | if ext is None: | ||
| 1021 | ext = OELinkExtension(link_spdx_id=True) | ||
| 1022 | o.extension.append(ext) | ||
| 1023 | |||
| 1024 | if ext.link_spdx_id: | ||
| 1025 | ext.link_name = link_id(o._id) | ||
| 1026 | |||
| 1027 | alias_ext = get_alias(o) | ||
| 1028 | if alias_ext is not None and alias_ext.alias: | ||
| 1029 | alias_ext.link_name = link_id(alias_ext.alias) | ||
| 1030 | 986 | ||
| 1031 | finally: | 987 | finally: |
| 1032 | # It is really helpful for debugging if the JSON document is written | 988 | # It is really helpful for debugging if the JSON document is written |
| @@ -1055,7 +1011,10 @@ def load_obj_in_jsonld(d, arch, subdir, fn_name, obj_type, **attr_filter): | |||
| 1055 | 1011 | ||
| 1056 | 1012 | ||
| 1057 | def find_by_spdxid(d, spdxid, *, required=False): | 1013 | def find_by_spdxid(d, spdxid, *, required=False): |
| 1058 | return find_jsonld(d, *jsonld_hash_path(spdxid), required=required) | 1014 | if spdxid.startswith(OE_ALIAS_PREFIX): |
| 1015 | h = spdxid[len(OE_ALIAS_PREFIX) :].split("/", 1)[0] | ||
| 1016 | return find_jsonld(d, *jsonld_hash_path(h), required=required) | ||
| 1017 | return find_jsonld(d, *jsonld_hash_path(hash_id(spdxid)), required=required) | ||
| 1059 | 1018 | ||
| 1060 | 1019 | ||
| 1061 | def create_sbom(d, name, root_elements, add_objectsets=[]): | 1020 | def create_sbom(d, name, root_elements, add_objectsets=[]): |
diff --git a/meta/lib/oe/spdx30_tasks.py b/meta/lib/oe/spdx30_tasks.py index 3d7035909f..036c58bf4b 100644 --- a/meta/lib/oe/spdx30_tasks.py +++ b/meta/lib/oe/spdx30_tasks.py | |||
| @@ -56,6 +56,7 @@ def add_license_expression(d, objset, license_expression, license_data): | |||
| 56 | name=name, | 56 | name=name, |
| 57 | ) | 57 | ) |
| 58 | ) | 58 | ) |
| 59 | objset.set_element_alias(lic) | ||
| 59 | simple_license_text[name] = lic | 60 | simple_license_text[name] = lic |
| 60 | 61 | ||
| 61 | if name == "PD": | 62 | if name == "PD": |
| @@ -106,7 +107,9 @@ def add_license_expression(d, objset, license_expression, license_data): | |||
| 106 | 107 | ||
| 107 | spdx_license = "LicenseRef-" + l | 108 | spdx_license = "LicenseRef-" + l |
| 108 | if spdx_license not in license_text_map: | 109 | if spdx_license not in license_text_map: |
| 109 | license_text_map[spdx_license] = add_license_text(l)._id | 110 | license_text_map[spdx_license] = oe.sbom30.get_element_link_id( |
| 111 | add_license_text(l) | ||
| 112 | ) | ||
| 110 | 113 | ||
| 111 | return spdx_license | 114 | return spdx_license |
| 112 | 115 | ||
| @@ -277,7 +280,7 @@ def collect_dep_objsets(d, build): | |||
| 277 | for dep in deps: | 280 | for dep in deps: |
| 278 | bb.debug(1, "Fetching SPDX for dependency %s" % (dep.pn)) | 281 | bb.debug(1, "Fetching SPDX for dependency %s" % (dep.pn)) |
| 279 | dep_build, dep_objset = oe.sbom30.find_root_obj_in_jsonld( | 282 | dep_build, dep_objset = oe.sbom30.find_root_obj_in_jsonld( |
| 280 | d, "recipes", dep.pn, oe.spdx30.build_Build | 283 | d, "recipes", "recipe-" + dep.pn, oe.spdx30.build_Build |
| 281 | ) | 284 | ) |
| 282 | # If the dependency is part of the taskhash, return it to be linked | 285 | # If the dependency is part of the taskhash, return it to be linked |
| 283 | # against. Otherwise, it cannot be linked against because this recipe | 286 | # against. Otherwise, it cannot be linked against because this recipe |
| @@ -461,7 +464,7 @@ def create_spdx(d): | |||
| 461 | if not include_vex in ("none", "current", "all"): | 464 | if not include_vex in ("none", "current", "all"): |
| 462 | bb.fatal("SPDX_INCLUDE_VEX must be one of 'none', 'current', 'all'") | 465 | bb.fatal("SPDX_INCLUDE_VEX must be one of 'none', 'current', 'all'") |
| 463 | 466 | ||
| 464 | build_objset = oe.sbom30.ObjectSet.new_objset(d, d.getVar("PN")) | 467 | build_objset = oe.sbom30.ObjectSet.new_objset(d, "recipe-" + d.getVar("PN")) |
| 465 | 468 | ||
| 466 | build = build_objset.new_task_build("recipe", "recipe") | 469 | build = build_objset.new_task_build("recipe", "recipe") |
| 467 | build_objset.set_element_alias(build) | 470 | build_objset.set_element_alias(build) |
| @@ -501,8 +504,11 @@ def create_spdx(d): | |||
| 501 | bb.debug(1, "Skipping %s since it is already fixed upstream" % cve) | 504 | bb.debug(1, "Skipping %s since it is already fixed upstream" % cve) |
| 502 | continue | 505 | continue |
| 503 | 506 | ||
| 507 | spdx_cve = build_objset.new_cve_vuln(cve) | ||
| 508 | build_objset.set_element_alias(spdx_cve) | ||
| 509 | |||
| 504 | cve_by_status.setdefault(decoded_status["mapping"], {})[cve] = ( | 510 | cve_by_status.setdefault(decoded_status["mapping"], {})[cve] = ( |
| 505 | build_objset.new_cve_vuln(cve), | 511 | spdx_cve, |
| 506 | decoded_status["detail"], | 512 | decoded_status["detail"], |
| 507 | decoded_status["description"], | 513 | decoded_status["description"], |
| 508 | ) | 514 | ) |
| @@ -574,7 +580,7 @@ def create_spdx(d): | |||
| 574 | 580 | ||
| 575 | bb.debug(1, "Creating SPDX for package %s" % pkg_name) | 581 | bb.debug(1, "Creating SPDX for package %s" % pkg_name) |
| 576 | 582 | ||
| 577 | pkg_objset = oe.sbom30.ObjectSet.new_objset(d, pkg_name) | 583 | pkg_objset = oe.sbom30.ObjectSet.new_objset(d, "package-" + pkg_name) |
| 578 | 584 | ||
| 579 | spdx_package = pkg_objset.add_root( | 585 | spdx_package = pkg_objset.add_root( |
| 580 | oe.spdx30.software_Package( | 586 | oe.spdx30.software_Package( |
| @@ -662,20 +668,21 @@ def create_spdx(d): | |||
| 662 | for status, cves in cve_by_status.items(): | 668 | for status, cves in cve_by_status.items(): |
| 663 | for cve, items in cves.items(): | 669 | for cve, items in cves.items(): |
| 664 | spdx_cve, detail, description = items | 670 | spdx_cve, detail, description = items |
| 671 | spdx_cve_id = oe.sbom30.get_element_link_id(spdx_cve) | ||
| 665 | 672 | ||
| 666 | all_cves.add(spdx_cve._id) | 673 | all_cves.add(spdx_cve_id) |
| 667 | 674 | ||
| 668 | if status == "Patched": | 675 | if status == "Patched": |
| 669 | pkg_objset.new_vex_patched_relationship( | 676 | pkg_objset.new_vex_patched_relationship( |
| 670 | [spdx_cve._id], [spdx_package] | 677 | [spdx_cve_id], [spdx_package] |
| 671 | ) | 678 | ) |
| 672 | elif status == "Unpatched": | 679 | elif status == "Unpatched": |
| 673 | pkg_objset.new_vex_unpatched_relationship( | 680 | pkg_objset.new_vex_unpatched_relationship( |
| 674 | [spdx_cve._id], [spdx_package] | 681 | [spdx_cve_id], [spdx_package] |
| 675 | ) | 682 | ) |
| 676 | elif status == "Ignored": | 683 | elif status == "Ignored": |
| 677 | spdx_vex = pkg_objset.new_vex_ignored_relationship( | 684 | spdx_vex = pkg_objset.new_vex_ignored_relationship( |
| 678 | [spdx_cve._id], | 685 | [spdx_cve_id], |
| 679 | [spdx_package], | 686 | [spdx_package], |
| 680 | impact_statement=description, | 687 | impact_statement=description, |
| 681 | ) | 688 | ) |
| @@ -810,7 +817,7 @@ def create_package_spdx(d): | |||
| 810 | d, | 817 | d, |
| 811 | pkg_arch, | 818 | pkg_arch, |
| 812 | "packages-staging", | 819 | "packages-staging", |
| 813 | pkg_name, | 820 | "package-" + pkg_name, |
| 814 | oe.spdx30.software_Package, | 821 | oe.spdx30.software_Package, |
| 815 | software_primaryPurpose=oe.spdx30.software_SoftwarePurpose.install, | 822 | software_primaryPurpose=oe.spdx30.software_SoftwarePurpose.install, |
| 816 | ) | 823 | ) |
| @@ -849,7 +856,7 @@ def create_package_spdx(d): | |||
| 849 | dep_spdx_package, _ = oe.sbom30.find_root_obj_in_jsonld( | 856 | dep_spdx_package, _ = oe.sbom30.find_root_obj_in_jsonld( |
| 850 | d, | 857 | d, |
| 851 | "packages-staging", | 858 | "packages-staging", |
| 852 | dep_pkg, | 859 | "package-" + dep_pkg, |
| 853 | oe.spdx30.software_Package, | 860 | oe.spdx30.software_Package, |
| 854 | software_primaryPurpose=oe.spdx30.software_SoftwarePurpose.install, | 861 | software_primaryPurpose=oe.spdx30.software_SoftwarePurpose.install, |
| 855 | ) | 862 | ) |
| @@ -949,13 +956,14 @@ def write_bitbake_spdx(d): | |||
| 949 | ) | 956 | ) |
| 950 | 957 | ||
| 951 | for obj in objset.foreach_type(oe.spdx30.Element): | 958 | for obj in objset.foreach_type(oe.spdx30.Element): |
| 952 | obj.extension.append(oe.sbom30.OELinkExtension(link_spdx_id=False)) | ||
| 953 | obj.extension.append(oe.sbom30.OEIdAliasExtension()) | 959 | obj.extension.append(oe.sbom30.OEIdAliasExtension()) |
| 954 | 960 | ||
| 955 | oe.sbom30.write_jsonld_doc(d, objset, deploy_dir_spdx / "bitbake.spdx.json") | 961 | oe.sbom30.write_jsonld_doc(d, objset, deploy_dir_spdx / "bitbake.spdx.json") |
| 956 | 962 | ||
| 957 | 963 | ||
| 958 | def collect_build_package_inputs(d, objset, build, packages): | 964 | def collect_build_package_inputs(d, objset, build, packages): |
| 965 | import oe.sbom30 | ||
| 966 | |||
| 959 | providers = oe.spdx_common.collect_package_providers(d) | 967 | providers = oe.spdx_common.collect_package_providers(d) |
| 960 | 968 | ||
| 961 | build_deps = set() | 969 | build_deps = set() |
| @@ -972,11 +980,11 @@ def collect_build_package_inputs(d, objset, build, packages): | |||
| 972 | pkg_spdx, _ = oe.sbom30.find_root_obj_in_jsonld( | 980 | pkg_spdx, _ = oe.sbom30.find_root_obj_in_jsonld( |
| 973 | d, | 981 | d, |
| 974 | "packages", | 982 | "packages", |
| 975 | pkg_name, | 983 | "package-" + pkg_name, |
| 976 | oe.spdx30.software_Package, | 984 | oe.spdx30.software_Package, |
| 977 | software_primaryPurpose=oe.spdx30.software_SoftwarePurpose.install, | 985 | software_primaryPurpose=oe.spdx30.software_SoftwarePurpose.install, |
| 978 | ) | 986 | ) |
| 979 | build_deps.add(pkg_spdx._id) | 987 | build_deps.add(oe.sbom30.get_element_link_id(pkg_spdx)) |
| 980 | 988 | ||
| 981 | if missing_providers: | 989 | if missing_providers: |
| 982 | bb.fatal( | 990 | bb.fatal( |
| @@ -1002,7 +1010,9 @@ def create_rootfs_spdx(d): | |||
| 1002 | with root_packages_file.open("r") as f: | 1010 | with root_packages_file.open("r") as f: |
| 1003 | packages = json.load(f) | 1011 | packages = json.load(f) |
| 1004 | 1012 | ||
| 1005 | objset = oe.sbom30.ObjectSet.new_objset(d, "%s-%s" % (image_basename, machine)) | 1013 | objset = oe.sbom30.ObjectSet.new_objset( |
| 1014 | d, "%s-%s-rootfs" % (image_basename, machine) | ||
| 1015 | ) | ||
| 1006 | 1016 | ||
| 1007 | rootfs = objset.add_root( | 1017 | rootfs = objset.add_root( |
| 1008 | oe.spdx30.software_Package( | 1018 | oe.spdx30.software_Package( |
| @@ -1030,6 +1040,8 @@ def create_rootfs_spdx(d): | |||
| 1030 | 1040 | ||
| 1031 | 1041 | ||
| 1032 | def create_image_spdx(d): | 1042 | def create_image_spdx(d): |
| 1043 | import oe.sbom30 | ||
| 1044 | |||
| 1033 | image_deploy_dir = Path(d.getVar("IMGDEPLOYDIR")) | 1045 | image_deploy_dir = Path(d.getVar("IMGDEPLOYDIR")) |
| 1034 | manifest_path = Path(d.getVar("IMAGE_OUTPUT_MANIFEST")) | 1046 | manifest_path = Path(d.getVar("IMAGE_OUTPUT_MANIFEST")) |
| 1035 | spdx_work_dir = Path(d.getVar("SPDXIMAGEWORK")) | 1047 | spdx_work_dir = Path(d.getVar("SPDXIMAGEWORK")) |
| @@ -1037,7 +1049,9 @@ def create_image_spdx(d): | |||
| 1037 | image_basename = d.getVar("IMAGE_BASENAME") | 1049 | image_basename = d.getVar("IMAGE_BASENAME") |
| 1038 | machine = d.getVar("MACHINE") | 1050 | machine = d.getVar("MACHINE") |
| 1039 | 1051 | ||
| 1040 | objset = oe.sbom30.ObjectSet.new_objset(d, "%s-%s" % (image_basename, machine)) | 1052 | objset = oe.sbom30.ObjectSet.new_objset( |
| 1053 | d, "%s-%s-image" % (image_basename, machine) | ||
| 1054 | ) | ||
| 1041 | 1055 | ||
| 1042 | with manifest_path.open("r") as f: | 1056 | with manifest_path.open("r") as f: |
| 1043 | manifest = json.load(f) | 1057 | manifest = json.load(f) |
| @@ -1090,7 +1104,7 @@ def create_image_spdx(d): | |||
| 1090 | rootfs_image, _ = oe.sbom30.find_root_obj_in_jsonld( | 1104 | rootfs_image, _ = oe.sbom30.find_root_obj_in_jsonld( |
| 1091 | d, | 1105 | d, |
| 1092 | "rootfs", | 1106 | "rootfs", |
| 1093 | "%s-%s" % (image_basename, machine), | 1107 | "%s-%s-rootfs" % (image_basename, machine), |
| 1094 | oe.spdx30.software_Package, | 1108 | oe.spdx30.software_Package, |
| 1095 | # TODO: Should use a purpose to filter here? | 1109 | # TODO: Should use a purpose to filter here? |
| 1096 | ) | 1110 | ) |
| @@ -1098,7 +1112,7 @@ def create_image_spdx(d): | |||
| 1098 | builds, | 1112 | builds, |
| 1099 | oe.spdx30.RelationshipType.hasInput, | 1113 | oe.spdx30.RelationshipType.hasInput, |
| 1100 | oe.spdx30.LifecycleScopeType.build, | 1114 | oe.spdx30.LifecycleScopeType.build, |
| 1101 | [rootfs_image._id], | 1115 | [oe.sbom30.get_element_link_id(rootfs_image)], |
| 1102 | ) | 1116 | ) |
| 1103 | 1117 | ||
| 1104 | objset.add_aliases() | 1118 | objset.add_aliases() |
| @@ -1107,6 +1121,8 @@ def create_image_spdx(d): | |||
| 1107 | 1121 | ||
| 1108 | 1122 | ||
| 1109 | def create_image_sbom_spdx(d): | 1123 | def create_image_sbom_spdx(d): |
| 1124 | import oe.sbom30 | ||
| 1125 | |||
| 1110 | image_name = d.getVar("IMAGE_NAME") | 1126 | image_name = d.getVar("IMAGE_NAME") |
| 1111 | image_basename = d.getVar("IMAGE_BASENAME") | 1127 | image_basename = d.getVar("IMAGE_BASENAME") |
| 1112 | image_link_name = d.getVar("IMAGE_LINK_NAME") | 1128 | image_link_name = d.getVar("IMAGE_LINK_NAME") |
| @@ -1121,17 +1137,17 @@ def create_image_sbom_spdx(d): | |||
| 1121 | rootfs_image, _ = oe.sbom30.find_root_obj_in_jsonld( | 1137 | rootfs_image, _ = oe.sbom30.find_root_obj_in_jsonld( |
| 1122 | d, | 1138 | d, |
| 1123 | "rootfs", | 1139 | "rootfs", |
| 1124 | "%s-%s" % (image_basename, machine), | 1140 | "%s-%s-rootfs" % (image_basename, machine), |
| 1125 | oe.spdx30.software_Package, | 1141 | oe.spdx30.software_Package, |
| 1126 | # TODO: Should use a purpose here? | 1142 | # TODO: Should use a purpose here? |
| 1127 | ) | 1143 | ) |
| 1128 | root_elements.append(rootfs_image._id) | 1144 | root_elements.append(oe.sbom30.get_element_link_id(rootfs_image)) |
| 1129 | 1145 | ||
| 1130 | image_objset, _ = oe.sbom30.find_jsonld( | 1146 | image_objset, _ = oe.sbom30.find_jsonld( |
| 1131 | d, "image", "%s-%s" % (image_basename, machine), required=True | 1147 | d, "image", "%s-%s-image" % (image_basename, machine), required=True |
| 1132 | ) | 1148 | ) |
| 1133 | for o in image_objset.foreach_root(oe.spdx30.software_File): | 1149 | for o in image_objset.foreach_root(oe.spdx30.software_File): |
| 1134 | root_elements.append(o._id) | 1150 | root_elements.append(oe.sbom30.get_element_link_id(o)) |
| 1135 | 1151 | ||
| 1136 | objset, sbom = oe.sbom30.create_sbom(d, image_name, root_elements) | 1152 | objset, sbom = oe.sbom30.create_sbom(d, image_name, root_elements) |
| 1137 | 1153 | ||
diff --git a/meta/lib/oeqa/selftest/cases/spdx.py b/meta/lib/oeqa/selftest/cases/spdx.py index 9b35793d13..f3b955ed2b 100644 --- a/meta/lib/oeqa/selftest/cases/spdx.py +++ b/meta/lib/oeqa/selftest/cases/spdx.py | |||
| @@ -143,35 +143,31 @@ class SPDX30Check(SPDX3CheckBase, OESelftestTestCase): | |||
| 143 | def test_base_files(self): | 143 | def test_base_files(self): |
| 144 | self.check_recipe_spdx( | 144 | self.check_recipe_spdx( |
| 145 | "base-files", | 145 | "base-files", |
| 146 | "{DEPLOY_DIR_SPDX}/{MACHINE_ARCH}/packages/base-files.spdx.json", | 146 | "{DEPLOY_DIR_SPDX}/{MACHINE_ARCH}/packages/package-base-files.spdx.json", |
| 147 | ) | 147 | ) |
| 148 | 148 | ||
| 149 | |||
| 150 | def test_gcc_include_source(self): | 149 | def test_gcc_include_source(self): |
| 151 | import oe.spdx30 | ||
| 152 | |||
| 153 | objset = self.check_recipe_spdx( | 150 | objset = self.check_recipe_spdx( |
| 154 | "gcc", | 151 | "gcc", |
| 155 | "{DEPLOY_DIR_SPDX}/{SSTATE_PKGARCH}/recipes/gcc.spdx.json", | 152 | "{DEPLOY_DIR_SPDX}/{SSTATE_PKGARCH}/recipes/recipe-gcc.spdx.json", |
| 156 | extraconf=textwrap.dedent( | 153 | extraconf="""\ |
| 157 | """\ | ||
| 158 | SPDX_INCLUDE_SOURCES = "1" | 154 | SPDX_INCLUDE_SOURCES = "1" |
| 159 | """ | 155 | """, |
| 160 | ), | ||
| 161 | ) | 156 | ) |
| 162 | 157 | ||
| 163 | gcc_pv = get_bb_var("PV", "gcc") | 158 | gcc_pv = get_bb_var("PV", "gcc") |
| 164 | filename = f'gcc-{gcc_pv}/README' | 159 | filename = f"gcc-{gcc_pv}/README" |
| 165 | found = False | 160 | found = False |
| 166 | for software_file in objset.foreach_type(oe.spdx30.software_File): | 161 | for software_file in objset.foreach_type(oe.spdx30.software_File): |
| 167 | if software_file.name == filename: | 162 | if software_file.name == filename: |
| 168 | found = True | 163 | found = True |
| 169 | self.logger.info(f"The spdxId of {filename} in gcc.spdx.json is {software_file.spdxId}") | 164 | self.logger.info( |
| 165 | f"The spdxId of {filename} in recipe-gcc.spdx.json is {software_file.spdxId}" | ||
| 166 | ) | ||
| 170 | break | 167 | break |
| 171 | 168 | ||
| 172 | self.assertTrue( | 169 | self.assertTrue( |
| 173 | found, | 170 | found, f"Not found source file {filename} in recipe-gcc.spdx.json\n" |
| 174 | f"Not found source file {filename} in gcc.spdx.json\n" | ||
| 175 | ) | 171 | ) |
| 176 | 172 | ||
| 177 | def test_core_image_minimal(self): | 173 | def test_core_image_minimal(self): |
