From: Simon Falsig <sfal...@verity.ch> This provides support for building SBOMs in CycloneDX format.
A target is added alongside the other reports, that (based on the fast-bsp-report) extracts name, version, cpe and license of each target package, and puts these into a final sbom-report in CycloneDX/JSON format. This requires a working Python3 setup with the cyclonedx-bom package installed. --- bin/ptxdist | 3 +- rules/post/ptxd_make_report.make | 15 ++++++-- scripts/lib/ptxd_make_report.sh | 16 +++++++++ scripts/lib/ptxd_make_sbom_report.py | 54 ++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 scripts/lib/ptxd_make_sbom_report.py diff --git a/bin/ptxdist b/bin/ptxdist index dfb619cbd..15be851f5 100755 --- a/bin/ptxdist +++ b/bin/ptxdist @@ -780,6 +780,7 @@ Misc: full-bsp-report generate a yaml file that describes the BSP and all packages. More data but will build all packages if necessary. + sbom-report generate a CycloneDX json SBOM print <var> print the contents of a variable, in the way it is known by "make" printnext <var> assumes that the contents of <var> is another @@ -1807,7 +1808,7 @@ EOF ptxd_make_log export_src EXPORTDIR="${1}" exit ;; - fast-bsp-report|full-bsp-report) + fast-bsp-report|full-bsp-report|sbom-report) check_premake_compiler && ptxd_make_log "${cmd}" exit diff --git a/rules/post/ptxd_make_report.make b/rules/post/ptxd_make_report.make index eecd2a577..ffa398c95 100644 --- a/rules/post/ptxd_make_report.make +++ b/rules/post/ptxd_make_report.make @@ -10,7 +10,9 @@ ptx/report-env = \ $(image/env) \ ptx_report_target="$(strip $(1))" \ ptx_packages_selected="$(filter-out $(IMAGE_PACKAGES),$(PTX_PACKAGES_SELECTED))" \ - ptx_image_packages="$(IMAGE_PACKAGES)" + ptx_image_packages="$(IMAGE_PACKAGES)" \ + ptx_target_packages="$(PACKAGES)" + PHONY += full-bsp-report full-bsp-report: $(RELEASEDIR)/full-bsp-report.yaml @@ -26,13 +28,22 @@ $(RELEASEDIR)/full-bsp-report.yaml: \ @$(call ptx/report-env, $@) ptxd_make_full_bsp_report @$(call finish) + PHONY += fast-bsp-report fast-bsp-report: $(RELEASEDIR)/fast-bsp-report.yaml - $(RELEASEDIR)/fast-bsp-report.yaml: $(addprefix $(STATEDIR)/,$(addsuffix .fast-report,$(PTX_PACKAGES_SELECTED))) @$(call targetinfo) @$(call ptx/report-env, $@) ptxd_make_fast_bsp_report @$(call finish) + +PHONY += sbom-report +sbom-report: $(RELEASEDIR)/sbom-report.json + +$(RELEASEDIR)/sbom-report.json: $(addprefix $(STATEDIR)/,$(addsuffix .fast-report,$(PACKAGES))) + @$(call targetinfo) + @$(call ptx/report-env, $@) ptxd_make_sbom_report + @$(call finish) + # vim: syntax=make diff --git a/scripts/lib/ptxd_make_report.sh b/scripts/lib/ptxd_make_report.sh index a363ca5b3..e2da4c05f 100644 --- a/scripts/lib/ptxd_make_report.sh +++ b/scripts/lib/ptxd_make_report.sh @@ -144,3 +144,19 @@ ptxd_make_fast_bsp_report() { } export -f ptxd_make_fast_bsp_report +ptxd_make_sbom_report() { + local -a ptxd_reply + local pkg_lic pkg + + ptxd_make_layer_init || return + + echo "Generating $(ptxd_print_path "${ptx_report_target}") ..." + echo + + mkdir -p "$(dirname "${ptx_report_target}")" && + python3 ${PTXDIST_LIB_DIR}/ptxd_make_sbom_report.py "${ptx_report_dir}/fast/" ${ptx_target_packages} > ${PTXDIST_TEMPDIR}/sbom-report && + mv "${PTXDIST_TEMPDIR}/sbom-report" "${ptx_report_target}" || + ptxd_bailout "failed to create SBOM report" +} +export -f ptxd_make_sbom_report + diff --git a/scripts/lib/ptxd_make_sbom_report.py b/scripts/lib/ptxd_make_sbom_report.py new file mode 100644 index 000000000..cc6a6f703 --- /dev/null +++ b/scripts/lib/ptxd_make_sbom_report.py @@ -0,0 +1,54 @@ +from cyclonedx.factory.license import LicenseFactory +from cyclonedx.factory.license import LicenseChoiceFactory +from cyclonedx.model.bom import Bom +from cyclonedx.model.component import Component +from cyclonedx.output.json import JsonV1Dot4 +import sys +import re + +lFac = LicenseFactory() +lcFac = LicenseChoiceFactory(license_factory=lFac) +bom = Bom() + +for i in range(2, len(sys.argv)): + pkg_report = sys.argv[1] + sys.argv[i] + ".yaml" + with open(pkg_report, 'r') as file: + content = file.read() + name_ = re.search("name: \'(.+)\'", content).group(1) + version_ = re.search("version: \'(.+)\'", content).group(1) + + # First see if we have a full CPE specified, then use that + cpe_match = re.search("cpe: \'(.+)\'", content) + cpe_ = None + if cpe_match is not None: + cpe_ = cpe_match.group(1) + else: + # See if we have the individual components + cpe_vendor_match = re.search("cpe_vendor: \'(.+)\'", content) + cpe_product_match = re.search("cpe_product: \'(.+)\'", content) + cpe_version_match = re.search("cpe_version: \'(.+)\'", content) + if cpe_vendor_match is not None and cpe_product_match is not None and cpe_version_match is not None: + cpe_ = "cpe:2.3:a:{vendor}:{product}:{version}:*:*:*:*:*:*:*".format(vendor=cpe_vendor_match.group(1), + product=cpe_product_match.group(1), + version=cpe_version_match.group(1)) + + if cpe_ is not None: + # We have a CPE, let's validate it. Regex from: https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd + if not re.fullmatch("cpe:2\.3:[aho\*\-](:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!\"#$$%&'\(\)\+,/:;" + "<=>@\[\]\^`\{\|}~]))+(\?*|\*?))|[\*\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)" + "|[\*\-]))(:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!\"#$$%&'\(\)\+,/:;<=>@\[\]\^`\{\|}~]" + "))+(\?*|\*?))|[\*\-])){4}", cpe_): + raise ValueError("Constructed CPE is not valid: {cpe}".format(cpe=cpe_)) + + licenses_ = re.search("licenses: \'(.+)\'", content).group(1) + comp = Component( + name=name_, + version=version_, + cpe=cpe_, + licenses=[lcFac.make_with_license(licenses_)], + bom_ref=name_ + "@" + version_ + ) + bom.components.add(comp) + +serializedJSON = JsonV1Dot4(bom).output_as_string() +print(serializedJSON) -- 2.25.1