From patchwork Wed Mar 11 10:19:42 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Benjamin Robin X-Patchwork-Id: 83095 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 774BF1049515 for ; Wed, 11 Mar 2026 10:20:08 +0000 (UTC) Received: from smtpout-03.galae.net (smtpout-03.galae.net [185.246.85.4]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.17905.1773224402795268239 for ; Wed, 11 Mar 2026 03:20:03 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: body hash did not verify" header.i=@bootlin.com header.s=dkim header.b=Sk9edmGT; spf=pass (domain: bootlin.com, ip: 185.246.85.4, mailfrom: benjamin.robin@bootlin.com) Received: from smtpout-01.galae.net (smtpout-01.galae.net [212.83.139.233]) by smtpout-03.galae.net (Postfix) with ESMTPS id 8FD704E42616; Wed, 11 Mar 2026 10:20:00 +0000 (UTC) Received: from mail.galae.net (mail.galae.net [212.83.136.155]) by smtpout-01.galae.net (Postfix) with ESMTPS id 66B9C60004; Wed, 11 Mar 2026 10:20:00 +0000 (UTC) Received: from [127.0.0.1] (localhost [127.0.0.1]) by localhost (Mailerdaemon) with ESMTPSA id B7DBE10369CBA; Wed, 11 Mar 2026 11:19:58 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bootlin.com; s=dkim; t=1773224399; h=from:subject:date:message-id:to:cc:mime-version:content-type: content-transfer-encoding:in-reply-to:references; bh=k6FSRcEQgCOD1NdA9N1Qg3Eb6PsZ6ZEh46C2HEF3lkA=; b=Sk9edmGTuzk5uYko27Y/3gZG6yWhNlwd2qKC1vI72RB9l9JlgQUJ7RMADyP9Rty2QrrlTQ IbK0HvAER86RCEsKY8vu5pgERqSW8B4f+e50Kx3M6ChBoVkG0uZ0G7rMfGbt6w3x4QipUj fpzOqi9J+hE0SqcuRRYD05lVXHLYycd7WD/lcJde7w2rOO4PJ4hRxSRoLvHr6/oM9ov/cw q+j8UbJ3NZ+5hRpxu7sWNPOPFTYGfuHMTzIjhYcK0OxI51mNjdNY6Nw+czO1Uv46EF+BGC EjUWc+GZ+Y9W/ioVd05bZWs4JYl7RhpVfKBdsZjkfEU+Q35a88tc7lBbJvnhVg== From: Benjamin Robin Date: Wed, 11 Mar 2026 11:19:42 +0100 Subject: [PATCH v4] sbom-cve-check: Add class for post-build CVE analysis MIME-Version: 1.0 Message-Id: <20260311-add-sbom-cve-check-v4-1-f4e6c4cee8ca@bootlin.com> References: <20260311-add-sbom-cve-check-v4-0-f4e6c4cee8ca@bootlin.com> In-Reply-To: <20260311-add-sbom-cve-check-v4-0-f4e6c4cee8ca@bootlin.com> To: openembedded-core@lists.openembedded.org Cc: ross.burton@arm.com, peter.marko@siemens.com, jpewhacker@gmail.com, olivier.benjamin@bootlin.com, antonin.godard@bootlin.com, mathieu.dubois-briand@bootlin.com, thomas.petazzoni@bootlin.com, Benjamin Robin X-Mailer: b4 0.15-dev X-Last-TLS-Session-Version: TLSv1.3 List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 11 Mar 2026 10:20:08 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/232862 By default, the sbom-cve-check class generates these export files: - A JSON in `cve-check` format, named `${IMAGE_NAME}.cve-check.json` - An SPDX 3.0 SBOM, named `${IMAGE_NAME}.cve-check.spdx.json`. An user can add or remove export file formats by using the `SBOM_CVE_CHECK_EXPORT_VARS` variables. By default, the CVE databases are downloaded using the following recipes: - sbom-cve-check-update-cvelist-native.bb - sbom-cve-check-update-nvd-native.bb The database download logic is implemented in sbom-cve-check-update-db.inc The CVE databases are stored in the download directory (`DL_DIR`) by default. This can be configured by the `SBOM_CVE_CHECK_DATABASES_DIR` variable. Access to the database is managed using an exclusive file lock (`flock`) on the directory. During CVE analysis, sbom-cve-check acquires a shared lock, allowing multiple analyses to run in parallel. However, if the database is being updated, any ongoing CVE analysis is temporarily paused. This design ensures that, under normal circumstances, `sbom-cve-check` can run without requiring network access. If a user needs network access during execution (e.g., to download annotation databases), they can set `SBOM_CVE_CHECK_ALLOW_NETWORK` to "1". The CVE analysis runs only if either the original SBOM changes or the CVE databases are updated. In the two CVE database-fetching recipes, a file in the sysroot is written, containing the Git revision of the fetched CVE database. This leverages BitBake's checksum computation for sysroot files to determine if dependent tasks need re-execution. Execute `sbom-cve-check` with the generated VEX manifest only if enabled and if `SPDX_INCLUDE_VEX` is set to a value other than "all". When `SPDX_INCLUDE_VEX=all`, the SPDX 3.0 file already contains all the necessary information for CVE analysis, making the VEX manifest redundant. Signed-off-by: Benjamin Robin --- meta/classes-recipe/sbom-cve-check.bbclass | 115 +++++++++++++++++++++ meta/conf/distro/include/maintainers.inc | 2 + meta/recipes-core/meta/sbom-cve-check-config.inc | 4 + .../meta/sbom-cve-check-update-cvelist-native.bb | 8 ++ .../recipes-core/meta/sbom-cve-check-update-db.inc | 98 ++++++++++++++++++ .../meta/sbom-cve-check-update-nvd-native.bb | 8 ++ 6 files changed, 235 insertions(+) diff --git a/meta/classes-recipe/sbom-cve-check.bbclass b/meta/classes-recipe/sbom-cve-check.bbclass new file mode 100644 index 000000000000..3709fa98d97e --- /dev/null +++ b/meta/classes-recipe/sbom-cve-check.bbclass @@ -0,0 +1,115 @@ +# SPDX-License-Identifier: MIT + +require recipes-core/meta/sbom-cve-check-config.inc + +SBOM_CVE_CHECK_DEPLOYDIR = "${WORKDIR}/sbom_cve_check/image-deploy" + +SBOM_CVE_CHECK_EXTRA_ARGS[doc] = "Allow to specify extra arguments to sbom-cve-check. \ + For example to add export flags for filtering (e.g., only export vulnerable CVEs). \ +" +SBOM_CVE_CHECK_EXTRA_ARGS ??= "" + +SBOM_CVE_CHECK_EXPORT_VARS[doc] = "List of variables that declare export files to generate. \ + Each variable must have a 'type' and an 'ext' flag set. \ + The 'type' flag contains the value that is passed to the --export-type command flags. \ + The 'ext' flag contains the filename extension (suffix). The output filename is going \ + to be ${IMAGE_NAME}${ext} \ +" +SBOM_CVE_CHECK_EXPORT_VARS ?= "SBOM_CVE_CHECK_EXPORT_SPDX3 SBOM_CVE_CHECK_EXPORT_CVECHECK" + +SBOM_CVE_CHECK_EXPORT_SPDX3[doc] = "Export configuration to generate an SPDX3 SBOM file, \ + with the following name: ${IMAGE_NAME}.cve-check.spdx.json \ +" +SBOM_CVE_CHECK_EXPORT_SPDX3[type] ?= "spdx3" +SBOM_CVE_CHECK_EXPORT_SPDX3[ext] ?= ".cve-check.spdx.json" + +SBOM_CVE_CHECK_EXPORT_CVECHECK[doc] = "Export configuration to generate a JSON manifest \ + in the same format as the cve-check class, with the following name: \ + ${IMAGE_NAME}.cve-check.json \ +" +SBOM_CVE_CHECK_EXPORT_CVECHECK[type] ?= "yocto-cve-check-manifest" +SBOM_CVE_CHECK_EXPORT_CVECHECK[ext] ?= ".cve-check.json" + +SBOM_CVE_CHECK_ALLOW_NETWORK[doc] = "Set to 1 to enable network usage." +SBOM_CVE_CHECK_ALLOW_NETWORK ?= "0" + +python do_sbom_cve_check() { + """ + Task: Run sbom-cve-check analysis on SBOM. + """ + import os + import bb + from oe.cve_check import update_symlinks + + if not bb.data.inherits_class("create-spdx-3.0", d): + bb.fatal("Cannot execute sbom-cve-check missing create-spdx-3.0 inherit.") + + sbom_path = d.expand("${DEPLOY_DIR_IMAGE}/${IMAGE_LINK_NAME}.spdx.json") + vex_manifest_path = d.expand("${DEPLOY_DIR_IMAGE}/${IMAGE_LINK_NAME}.json") + dl_db_dir = d.getVar("SBOM_CVE_CHECK_DATABASES_DIR") + deploy_dir = d.getVar("SBOM_CVE_CHECK_DEPLOYDIR") + img_link_name = d.getVar("IMAGE_LINK_NAME") + img_name = d.getVar("IMAGE_NAME") + + export_files = [] + for export_var in d.getVar("SBOM_CVE_CHECK_EXPORT_VARS").split(): + export_ext = d.getVarFlag(export_var, "ext") + export_path = f"{deploy_dir}/{img_name}{export_ext}" + export_link = f"{deploy_dir}/{img_link_name}{export_ext}" + export_type = d.getVarFlag(export_var, "type") + export_files.append((export_type, export_path, export_link)) + + cmd_env = os.environ.copy() + cmd_env["SBOM_CVE_CHECK_DATABASES_DIR"] = dl_db_dir + + cmd_args = [ + d.expand("${STAGING_BINDIR_NATIVE}/sbom-cve-check"), + "--sbom-path", + sbom_path, + ] + + # Assume that SPDX_INCLUDE_VEX is set globally to "all", and not only for the + # image recipe, which is very unlikely. This is not an issue to include the + # VEX manifest even if not needed. + if bb.data.inherits_class("vex", d) and d.getVar("SPDX_INCLUDE_VEX") != "all": + cmd_args.extend(["--yocto-vex-manifest", vex_manifest_path]) + + for export_file in export_files: + cmd_args.extend( + ["--export-type", export_file[0], "--export-path", export_file[1]] + ) + + cmd_args.extend(d.getVar("SBOM_CVE_CHECK_EXTRA_ARGS").split()) + + try: + bb.note("Running: {}".format(" ".join(cmd_args))) + bb.process.run(cmd_args, env=cmd_env) + except bb.process.ExecutionError as e: + bb.error(f"sbom-cve-check failed: {e}") + return + + for export_file in export_files: + bb.note(f"sbom-cve-check exported: {export_file[1]}") + update_symlinks(export_file[1], export_file[2]) +} + +addtask do_sbom_cve_check after do_create_image_sbom_spdx before do_build + +SSTATETASKS += "do_sbom_cve_check" +do_sbom_cve_check[cleandirs] = "${SBOM_CVE_CHECK_DEPLOYDIR}" +do_sbom_cve_check[sstate-inputdirs] = "${SBOM_CVE_CHECK_DEPLOYDIR}" +do_sbom_cve_check[sstate-outputdirs] = "${DEPLOY_DIR_IMAGE}" +do_sbom_cve_check[depends] += " \ + python3-sbom-cve-check-native:do_populate_sysroot \ + ${@oe.utils.conditional('SBOM_CVE_CHECK_ALLOW_NETWORK','0',' \ + sbom-cve-check-update-cvelist-native:do_populate_sysroot \ + sbom-cve-check-update-nvd-native:do_populate_sysroot \ + ','',d)} \ +" + +do_sbom_cve_check[network] = "${SBOM_CVE_CHECK_ALLOW_NETWORK}" + +python do_sbom_cve_check_setscene() { + sstate_setscene(d) +} +addtask do_sbom_cve_check_setscene diff --git a/meta/conf/distro/include/maintainers.inc b/meta/conf/distro/include/maintainers.inc index 3c7fc4974de3..71e5d77cb7d6 100644 --- a/meta/conf/distro/include/maintainers.inc +++ b/meta/conf/distro/include/maintainers.inc @@ -790,6 +790,8 @@ RECIPE_MAINTAINER:pn-sassc = "Simone Weiß " RECIPE_MAINTAINER:pn-sato-icon-theme = "Richard Purdie " RECIPE_MAINTAINER:pn-sato-screenshot = "Ross Burton " RECIPE_MAINTAINER:pn-sbc = "Unassigned " +RECIPE_MAINTAINER:pn-sbom-cve-check-update-cvelist-native = "Benjamin Robin " +RECIPE_MAINTAINER:pn-sbom-cve-check-update-nvd-native = "Benjamin Robin " RECIPE_MAINTAINER:pn-scdoc = "Alex Kiernan " RECIPE_MAINTAINER:pn-screen = "Unassigned " RECIPE_MAINTAINER:pn-seatd = "Unassigned " diff --git a/meta/recipes-core/meta/sbom-cve-check-config.inc b/meta/recipes-core/meta/sbom-cve-check-config.inc new file mode 100644 index 000000000000..a2a645825878 --- /dev/null +++ b/meta/recipes-core/meta/sbom-cve-check-config.inc @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: MIT + +SBOM_CVE_CHECK_DATABASES_DIR ??= "${DL_DIR}/sbom_cve_check/databases" +SBOM_CVE_CHECK_DATABASES_DIR[doc] = "Download directory path where to store the CVE databases" diff --git a/meta/recipes-core/meta/sbom-cve-check-update-cvelist-native.bb b/meta/recipes-core/meta/sbom-cve-check-update-cvelist-native.bb new file mode 100644 index 000000000000..75f7370839e6 --- /dev/null +++ b/meta/recipes-core/meta/sbom-cve-check-update-cvelist-native.bb @@ -0,0 +1,8 @@ +SUMMARY = "Updates the CVE List database" +LICENSE = "MIT" + +HOMEPAGE = "https://github.com/CVEProject/cvelistV5" +SBOM_CVE_CHECK_DB_NAME = "cvelist" +SBOM_CVE_CHECK_FETCH_URL = "https://github.com/CVEProject/cvelistV5.git" + +require sbom-cve-check-update-db.inc diff --git a/meta/recipes-core/meta/sbom-cve-check-update-db.inc b/meta/recipes-core/meta/sbom-cve-check-update-db.inc new file mode 100644 index 000000000000..8f384fa80d47 --- /dev/null +++ b/meta/recipes-core/meta/sbom-cve-check-update-db.inc @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: MIT + +INHIBIT_DEFAULT_DEPS = "1" +EXCLUDE_FROM_WORLD = "1" + +inherit native +require sbom-cve-check-config.inc + +SBOM_CVE_CHECK_DB_NAME[doc] = "Database name, which is the Git repository directory name. \ + The git repository will be stored in ${SBOM_CVE_CHECK_DATABASES_DIR)/" + +SBOM_CVE_CHECK_FETCH_URL[doc] = "Git clone URL of the CVE database" + +SBOM_CVE_CHECK_FETCH_INTERVAL ??= "57600" +SBOM_CVE_CHECK_FETCH_INTERVAL[doc] = "\ + CVE database update interval, in seconds. By default every 16 hours. \ + Use 0 to force the update. Use a negative value to skip the update. \ +" + +python do_fetch() { + from datetime import datetime, timezone, timedelta + import fcntl + import os + import pathlib + import subprocess + + bb.utils.export_proxies(d) + + fetch_interval = int(d.get("SBOM_CVE_CHECK_FETCH_INTERVAL")) + git_url = d.getVar("SBOM_CVE_CHECK_FETCH_URL") + git_name = d.getVar("SBOM_CVE_CHECK_DB_NAME") + git_dir = pathlib.Path(d.getVar("SBOM_CVE_CHECK_DATABASES_DIR")).joinpath(git_name) + git_dir.mkdir(parents=True, exist_ok=True) + + def _exec_git_cmd(args): + cmd = ["git"] + cmd.extend(args) + return subprocess.run( + cmd, + input="", + capture_output=True, + check=True, + cwd=git_dir, + encoding="utf-8", + ) + + # Lock the git directory: take an exclusive lock + lock_fd = os.open(git_dir, os.O_RDONLY | os.O_NOCTTY) + try: + fcntl.flock(lock_fd, fcntl.LOCK_EX) + + # Clone the git repository if it does not exist + if not git_dir.joinpath(".git", "HEAD").is_file(): + _exec_git_cmd(["clone", "--depth", "1", "--single-branch", git_url, "."]) + return + + # Check if an update is necessary + if fetch_interval < 0: + return + + if fetch_interval > 0: + # Get date of last commit + r = _exec_git_cmd(["show", "-s", "--format=%ct", "HEAD"]) + commit_date = datetime.fromtimestamp(int(r.stdout.strip()), tz=timezone.utc) + delta_last_commit = datetime.now(timezone.utc) - commit_date + if delta_last_commit < timedelta(seconds=fetch_interval): + return + + _exec_git_cmd(["pull"]) + except subprocess.SubprocessError as e: + bb.error(f"{e.cmd} failed:\n{e.stdout}\n---\n{e.stderr}\n") + finally: + # Release the exclusive lock + os.close(lock_fd) +} + +do_fetch[file-checksums] = "" +do_fetch[vardeps] += " \ + SBOM_CVE_CHECK_DATABASES_DIR \ + SBOM_CVE_CHECK_DB_NAME \ + SBOM_CVE_CHECK_FETCH_URL \ + SBOM_CVE_CHECK_FETCH_INTERVAL \ +" + +do_fetch[nostamp] = "1" + +# Leverage BitBake's checksum computation for populated sysroot files to determine +# whether other recipe tasks dependent on this output need to be re-executed. +# This serves as a workaround to avoid unnecessary runs of `sbom-cve-check` when +# the database remains unchanged, given that `do_fetch[nostamp] = "1"` is set. +do_compile() { + git_dir="${SBOM_CVE_CHECK_DATABASES_DIR}/${SBOM_CVE_CHECK_DB_NAME}" + git -C "${git_dir}" rev-parse --verify "HEAD^{object}" > "${S}/${SBOM_CVE_CHECK_DB_NAME}.rev" +} + +do_install() { + install -m 644 -D -t "${D}${datadir}/sbom_cve_check/databases/" "${S}/${SBOM_CVE_CHECK_DB_NAME}.rev" +} diff --git a/meta/recipes-core/meta/sbom-cve-check-update-nvd-native.bb b/meta/recipes-core/meta/sbom-cve-check-update-nvd-native.bb new file mode 100644 index 000000000000..fb7b304ac7e7 --- /dev/null +++ b/meta/recipes-core/meta/sbom-cve-check-update-nvd-native.bb @@ -0,0 +1,8 @@ +SUMMARY = "Updates the NVD CVE database" +LICENSE = "MIT" + +HOMEPAGE = "https://github.com/fkie-cad/nvd-json-data-feeds" +SBOM_CVE_CHECK_DB_NAME = "nvd-fkie" +SBOM_CVE_CHECK_FETCH_URL = "https://github.com/fkie-cad/nvd-json-data-feeds.git" + +require sbom-cve-check-update-db.inc