diff --git a/.gitignore b/.gitignore index 0c0fb31..4a06565 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -repositories/* +/build +/poky +/meta-*/ +!meta-enapter-linux diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7b720b3..e07109e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,7 @@ variables: DOCKER_BUILDKIT: 1 COMPOSE_HTTP_TIMEOUT: 600 BUILD_STORAGE_DIR: /home/gitlab-runner + GIT_STRATEGY: clone Build Image: when: manual @@ -18,7 +19,6 @@ Build Image: script: - set -o allexport && source configs/versions.env && set +o allexport - source ./bin/prepare-environments.sh - - ./bin/fetch-repositories.sh configs/repositories.conf - docker-compose -f docker-compose-buildagent.yml run --rm intel-x86-64-build Git Pull: @@ -27,7 +27,7 @@ Git Pull: tags: - yocto script: - - ./bin/fetch-repositories.sh configs/repositories.conf + - echo "Done." Release: when: manual diff --git a/bin/fetch-repositories.sh b/bin/fetch-repositories.sh deleted file mode 100755 index 44dbfc8..0000000 --- a/bin/fetch-repositories.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -# SPDX-FileCopyrightText: 2024 Enapter -# SPDX-License-Identifier: Apache-2.0 - -# Check if the correct number of arguments are provided -if [ "$#" -ne 1 ]; then - echo "Usage: $0 " - exit 1 -fi - -grep -v "^#" "$1" | while read p; do ./bin/git-fetch.sh $p || exit 1; done diff --git a/bin/git-fetch.sh b/bin/git-fetch.sh deleted file mode 100755 index 7cda225..0000000 --- a/bin/git-fetch.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -# SPDX-FileCopyrightText: 2024 Enapter -# SPDX-License-Identifier: Apache-2.0 - -RED='\033[0;31m' -NC='\033[0m' # No Color - -info() { - echo "[INFO] $1" -} - -error() { - echo 1>&2 - echo -e "${RED}[ERROR] $1${NC}" 1>&2 - echo 1>&2 -} - -debug() { - echo "[DEBUG] $1" -} - -# Check if the correct number of arguments is provided -if [ $# -ne 3 ]; then - error "Usage: $0 " - exit 1 -fi - -FOLDER_NAME="$1" -REPOSITORY_PATH="$2" -REVISION="$3" - -info "Fetching $REPOSITORY_PATH to $FOLDER_NAME" - -# Check if the target folder is a git repository -if [ ! -d "$FOLDER_NAME/.git" ]; then - info "The directory $FOLDER_NAME is not a Git repository. Cloning repository..." - git clone "$REPOSITORY_PATH" "$FOLDER_NAME" || { error "Failed to clone repository."; exit 1; } -fi - -# Ensure working directory is clean -git --git-dir="$FOLDER_NAME/.git" --work-tree="$FOLDER_NAME" reset --hard HEAD || { error "Failed to reset working directory."; exit 1; } - -# Fetch and switch -git --git-dir="$FOLDER_NAME/.git" --work-tree="$FOLDER_NAME" fetch --all || { error "Failed to fetch repository updates."; exit 1; } - -# Try to reset to origin/$REVISION (for branches or tags) -if git --git-dir="$FOLDER_NAME/.git" --work-tree="$FOLDER_NAME" reset --hard "origin/$REVISION"; then - info "Successfully reset to origin/$REVISION." -else - # If it fails, try to reset directly to $REVISION (for commit hashes) - if git --git-dir="$FOLDER_NAME/.git" --work-tree="$FOLDER_NAME" reset --hard "$REVISION"; then - info "Successfully reset to $REVISION." - else - error "Failed to reset to both origin/$REVISION and $REVISION." - exit 1 - fi -fi diff --git a/bin/prepare-environments.sh b/bin/prepare-environments.sh index 480336b..4f33741 100755 --- a/bin/prepare-environments.sh +++ b/bin/prepare-environments.sh @@ -1,23 +1,30 @@ # SPDX-FileCopyrightText: 2024 Enapter # SPDX-License-Identifier: Apache-2.0 -export BB_ENV_PASSTHROUGH_ADDITIONS="$BB_ENV_PASSTHROUGH_ADDITIONS DISTRO_VERSION DL_DIR SSTATE_DIR TMPDIR SECURE_BOOT_SIGNING_KEY SECURE_BOOT_SIGNING_CERT SECURE_BOOT_SIGNING_CERT_DER" - -export SECURE_BOOT_SIGNING_CERT="/home/build/secure_boot_signing/sign.crt" +export SECURE_BOOT_SIGNING_CERT="/home/build/secure_boot_signing/sign.pem" export SECURE_BOOT_SIGNING_KEY="/home/build/secure_boot_signing/sign.key" -export SECURE_BOOT_SIGNING_CERT_DER="/home/build/secure_boot_signing/sign.cer" +export SECURE_BOOT_SIGNING_CERT_DER="/home/build/secure_boot_signing/sign.der" + +export MODSIGN_SIGNING_CERT="/home/build/modules_signing/sign.pem" +export MODSIGN_SIGNING_KEY="/home/build/modules_signing/sign.key" + +export RAUC_CERT="/home/build/rauc/production.pem" +export RAUC_KEY="/home/build/rauc/production.key" +export RAUC_KEYRING="/home/build/rauc/ca.cert.pem" if [ -z "$CI_COMMIT_TAG" ]; then export DISTRO_VERSION="${ENAPTER_LINUX_BASE_VERSION}-dev-${CI_PIPELINE_ID:-${CI_COMMIT_SHORT_SHA:-unknown}}" else - export DISTRO_VERSION="$CI_COMMIT_TAG" + export DISTRO_VERSION="${CI_COMMIT_TAG:-$CI_COMMIT_REF_SLUG}.$CI_PIPELINE_ID" fi +export DISTRO="enapter-industrial-linux" + export IMG_ARTIFACT_NAME="enapter-industrial-linux-${DISTRO_VERSION}.zip" export IMG_FILE_ARTIFACT_NAME="enapter-industrial-linux-${DISTRO_VERSION}.img" export UPDATE_ARTIFACT_NAME="enapter-industrial-linux-update-${DISTRO_VERSION}.zip" +export RAUC_UPDATE_ARTIFACT_NAME="enapter-industrial-linux-update-${DISTRO_VERSION}.raucb" export VMDK_ARTIFACT_NAME="enapter-industrial-linux-${DISTRO_VERSION}.vmdk" - -export SSTATE_DIR=/home/build/sstate-cache -export DL_DIR=/home/build/downloads -export TMPDIR=/home/build/tmp +export VEX_ARTIFACT_NAME="enapter-industrial-linux-${DISTRO_VERSION}-vex.json" +export ROOTFS_SPDX_ARTIFACT_NAME="enapter-industrial-linux-${DISTRO_VERSION}-rootfs-spdx.zip" +export INITRAMFS_SPDX_ARTIFACT_NAME="enapter-industrial-linux-${DISTRO_VERSION}-initrd-spdx.zip" diff --git a/bin/upload-artifacts.sh b/bin/upload-artifacts.sh index c2caaa6..423b402 100755 --- a/bin/upload-artifacts.sh +++ b/bin/upload-artifacts.sh @@ -35,9 +35,22 @@ release_id=$(echo "$create_release_response" | jq '.id') cd "$artifacts_dir" -sha256sum -b "$IMG_ARTIFACT_NAME" "$UPDATE_ARTIFACT_NAME" "$VMDK_ARTIFACT_NAME" > "$sha256sums_name" +sha256sum -b "$IMG_ARTIFACT_NAME" "$UPDATE_ARTIFACT_NAME" "$VMDK_ARTIFACT_NAME" "$RAUC_UPDATE_ARTIFACT_NAME" > "$sha256sums_name" upload_asset "$release_id" "$IMG_ARTIFACT_NAME" "$IMG_ARTIFACT_NAME" upload_asset "$release_id" "$UPDATE_ARTIFACT_NAME" "$UPDATE_ARTIFACT_NAME" upload_asset "$release_id" "$VMDK_ARTIFACT_NAME" "$VMDK_ARTIFACT_NAME" +upload_asset "$release_id" "$RAUC_UPDATE_ARTIFACT_NAME" "$RAUC_UPDATE_ARTIFACT_NAME" upload_asset "$release_id" "$sha256sums_name" "$sha256sums_name" + +if [ -e "$VEX_ARTIFACT_NAME" ]; then + upload_asset "$release_id" "$VEX_ARTIFACT_NAME" "$VEX_ARTIFACT_NAME" +fi + +if [ -e "$ROOTFS_SPDX_ARTIFACT_NAME" ]; then + upload_asset "$release_id" "$ROOTFS_SPDX_ARTIFACT_NAME" "$ROOTFS_SPDX_ARTIFACT_NAME" +fi + +if [ -e "$INITRAMFS_SPDX_ARTIFACT_NAME" ]; then + upload_asset "$release_id" "$INITRAMFS_SPDX_ARTIFACT_NAME" "$INITRAMFS_SPDX_ARTIFACT_NAME" +fi diff --git a/configs/enapter-industrial-linux.yml b/configs/enapter-industrial-linux.yml new file mode 100644 index 0000000..0d03f3c --- /dev/null +++ b/configs/enapter-industrial-linux.yml @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: 2024 Enapter +# SPDX-License-Identifier: Apache-2.0 + +header: + version: 17 + +distro: enapter-industrial-linux +target: enapter-industrial-linux-image +machine: intel-corei7-64 + +env: + DISTRO: null + DISTRO_VERSION: null + DL_DIR: /home/build/downloads + IMG_ARTIFACT_NAME: null + IMG_FILE_ARTIFACT_NAME: null + INITRAMFS_SPDX_ARTIFACT_NAME: null + MODSIGN_SIGNING_CERT: null + MODSIGN_SIGNING_KEY: null + NVDCVE_API_KEY: null + RAUC_CERT: null + RAUC_KEY: null + RAUC_KEYRING: null + RAUC_UPDATE_ARTIFACT_NAME: null + ROOTFS_SPDX_ARTIFACT_NAME: null + SECURE_BOOT_SIGNING_CERT: null + SECURE_BOOT_SIGNING_CERT_DER: null + SECURE_BOOT_SIGNING_KEY: null + SSTATE_DIR: /home/build/sstate-cache + TMPDIR: /home/build/tmp + UPDATE_ARTIFACT_NAME: null + VEX_ARTIFACT_NAME: null + VMDK_ARTIFACT_NAME: null + +repos: + meta-enapter-linux: + layers: + meta-enapter-linux: + + poky: + url: 'git://git.yoctoproject.org/poky.git' + tag: 'yocto-5.0.4' + patches: + patch0: + repo: meta-enapter-linux + path: configs/patches/0001-Backport-vex.bbclass-and-dependencies.patch + layers: + meta: + meta-poky: + meta-yocto-bsp: + + meta-enapter: + url: 'https://github.com/enapter/meta-enapter' + branch: 'release/3.0' + layers: + meta-enapter-core: + + meta-intel: + url: 'git://git.yoctoproject.org/meta-intel.git' + commit: 'c2bc8e27e9cc83654a23e8d89525fd9b1e781eb6' + + meta-openembedded: + url: 'git://git.openembedded.org/meta-openembedded' + commit: '2e3126c9c16bb3df0560f6b3896d01539a3bfad7' + layers: + meta-filesystems: + meta-initramfs: + meta-networking: + meta-oe: + meta-perl: + meta-python: + meta-webserver: + + meta-rauc: + url: 'https://github.com/rauc/meta-rauc.git' + commit: '1e3e6b334defd7fbf95cb43d23975e7b3de4b520' + + meta-virtualization: + url: 'git://git.yoctoproject.org/meta-virtualization' + commit: '6f3c1d8f90947408a6587be222fec575a1ca5195' + +local_conf_header: + 10-base: | + CONF_VERSION = "2" + SPDX_PRETTY = "1" + IMAGE_FSTYPES = "wic ext4" + SKIP_META_VIRT_SANITY_CHECK = "1" + INHERIT += "rm_work vex" diff --git a/configs/patches/0001-Backport-vex.bbclass-and-dependencies.patch b/configs/patches/0001-Backport-vex.bbclass-and-dependencies.patch new file mode 100644 index 0000000..1b78760 --- /dev/null +++ b/configs/patches/0001-Backport-vex.bbclass-and-dependencies.patch @@ -0,0 +1,949 @@ +From 8eba87c4848811c1f46a2d93ebaed2131540348f Mon Sep 17 00:00:00 2001 +From: Dmitry Myaskovskiy +Date: Fri, 4 Oct 2024 02:09:08 +0200 +Subject: [PATCH] Backport vex.bbclass and dependencies + +--- + meta/classes/cve-check.bbclass | 291 ++++++++++--------------------- + meta/classes/vex.bbclass | 310 +++++++++++++++++++++++++++++++++ + meta/lib/oe/cve_check.py | 72 ++++++-- + 3 files changed, 463 insertions(+), 210 deletions(-) + create mode 100644 meta/classes/vex.bbclass + +diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass +index 93a2a1413d..a5104f210b 100644 +--- a/meta/classes/cve-check.bbclass ++++ b/meta/classes/cve-check.bbclass +@@ -31,24 +31,20 @@ + CVE_PRODUCT ??= "${BPN}" + CVE_VERSION ??= "${PV}" + +-CVE_CHECK_DB_DIR ?= "${DL_DIR}/CVE_CHECK" +-CVE_CHECK_DB_FILE ?= "${CVE_CHECK_DB_DIR}/nvdcve_2-1.db" ++CVE_CHECK_DB_FILENAME ?= "nvdcve_2-1.db" ++CVE_CHECK_DB_DIR ?= "${STAGING_DIR}/CVE_CHECK" ++CVE_CHECK_DB_FILE ?= "${CVE_CHECK_DB_DIR}/${CVE_CHECK_DB_FILENAME}" + CVE_CHECK_DB_FILE_LOCK ?= "${CVE_CHECK_DB_FILE}.lock" + +-CVE_CHECK_LOG ?= "${T}/cve.log" +-CVE_CHECK_TMP_FILE ?= "${TMPDIR}/cve_check" + CVE_CHECK_SUMMARY_DIR ?= "${LOG_DIR}/cve" + CVE_CHECK_SUMMARY_FILE_NAME ?= "cve-summary" +-CVE_CHECK_SUMMARY_FILE ?= "${CVE_CHECK_SUMMARY_DIR}/${CVE_CHECK_SUMMARY_FILE_NAME}" + CVE_CHECK_SUMMARY_FILE_NAME_JSON = "cve-summary.json" + CVE_CHECK_SUMMARY_INDEX_PATH = "${CVE_CHECK_SUMMARY_DIR}/cve-summary-index.txt" + + CVE_CHECK_LOG_JSON ?= "${T}/cve.json" + + CVE_CHECK_DIR ??= "${DEPLOY_DIR}/cve" +-CVE_CHECK_RECIPE_FILE ?= "${CVE_CHECK_DIR}/${PN}" + CVE_CHECK_RECIPE_FILE_JSON ?= "${CVE_CHECK_DIR}/${PN}_cve.json" +-CVE_CHECK_MANIFEST ?= "${IMGDEPLOYDIR}/${IMAGE_NAME}.cve" + CVE_CHECK_MANIFEST_JSON_SUFFIX ?= "json" + CVE_CHECK_MANIFEST_JSON ?= "${IMGDEPLOYDIR}/${IMAGE_NAME}.${CVE_CHECK_MANIFEST_JSON_SUFFIX}" + CVE_CHECK_COPY_FILES ??= "1" +@@ -59,9 +55,6 @@ CVE_CHECK_REPORT_PATCHED ??= "1" + + CVE_CHECK_SHOW_WARNINGS ??= "1" + +-# Provide text output +-CVE_CHECK_FORMAT_TEXT ??= "1" +- + # Provide JSON output + CVE_CHECK_FORMAT_JSON ??= "1" + +@@ -151,20 +144,11 @@ python cve_save_summary_handler () { + import datetime + from oe.cve_check import update_symlinks + +- cve_tmp_file = d.getVar("CVE_CHECK_TMP_FILE") +- + cve_summary_name = d.getVar("CVE_CHECK_SUMMARY_FILE_NAME") + cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR") + bb.utils.mkdirhier(cvelogpath) + + timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S') +- cve_summary_file = os.path.join(cvelogpath, "%s-%s.txt" % (cve_summary_name, timestamp)) +- +- if os.path.exists(cve_tmp_file): +- shutil.copyfile(cve_tmp_file, cve_summary_file) +- cvefile_link = os.path.join(cvelogpath, cve_summary_name) +- update_symlinks(cve_summary_file, cvefile_link) +- bb.plain("Complete CVE report summary created at: %s" % cvefile_link) + + if d.getVar("CVE_CHECK_FORMAT_JSON") == "1": + json_summary_link_name = os.path.join(cvelogpath, d.getVar("CVE_CHECK_SUMMARY_FILE_NAME_JSON")) +@@ -188,24 +172,23 @@ python do_cve_check () { + patched_cves = get_patched_cves(d) + except FileNotFoundError: + bb.fatal("Failure in searching patches") +- ignored, patched, unpatched, status = check_cves(d, patched_cves) +- if patched or unpatched or (d.getVar("CVE_CHECK_COVERAGE") == "1" and status): +- cve_data = get_cve_info(d, patched + unpatched + ignored) +- cve_write_data(d, patched, unpatched, ignored, cve_data, status) ++ cve_data, status = check_cves(d, patched_cves) ++ if len(cve_data) or (d.getVar("CVE_CHECK_COVERAGE") == "1" and status): ++ get_cve_info(d, cve_data) ++ cve_write_data(d, cve_data, status) + else: + bb.note("No CVE database found, skipping CVE check") + + } + + addtask cve_check before do_build +-do_cve_check[depends] = "cve-update-nvd2-native:do_fetch" ++do_cve_check[depends] = "cve-update-nvd2-native:do_unpack" + do_cve_check[nostamp] = "1" + + python cve_check_cleanup () { + """ + Delete the file used to gather all the CVE information. + """ +- bb.utils.remove(e.data.getVar("CVE_CHECK_TMP_FILE")) + bb.utils.remove(e.data.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")) + } + +@@ -223,9 +206,6 @@ python cve_check_write_rootfs_manifest () { + from oe.cve_check import cve_check_merge_jsons, update_symlinks + + if d.getVar("CVE_CHECK_COPY_FILES") == "1": +- deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE") +- if os.path.exists(deploy_file): +- bb.utils.remove(deploy_file) + deploy_file_json = d.getVar("CVE_CHECK_RECIPE_FILE_JSON") + if os.path.exists(deploy_file_json): + bb.utils.remove(deploy_file_json) +@@ -245,19 +225,13 @@ python cve_check_write_rootfs_manifest () { + json_data = {"version":"1", "package": []} + text_data = "" + enable_json = d.getVar("CVE_CHECK_FORMAT_JSON") == "1" +- enable_text = d.getVar("CVE_CHECK_FORMAT_TEXT") == "1" + + save_pn = d.getVar("PN") + + for pkg in recipies: +- # To be able to use the CVE_CHECK_RECIPE_FILE variable we have to evaluate ++ # To be able to use the CVE_CHECK_RECIPE_FILE_JSON variable we have to evaluate + # it with the different PN names set each time. + d.setVar("PN", pkg) +- if enable_text: +- pkgfilepath = d.getVar("CVE_CHECK_RECIPE_FILE") +- if os.path.exists(pkgfilepath): +- with open(pkgfilepath) as pfile: +- text_data += pfile.read() + + if enable_json: + pkgfilepath = d.getVar("CVE_CHECK_RECIPE_FILE_JSON") +@@ -268,16 +242,6 @@ python cve_check_write_rootfs_manifest () { + + d.setVar("PN", save_pn) + +- if enable_text: +- link_path = os.path.join(deploy_dir, "%s.cve" % link_name) +- manifest_name = d.getVar("CVE_CHECK_MANIFEST") +- +- with open(manifest_name, "w") as f: +- f.write(text_data) +- +- update_symlinks(manifest_name, link_path) +- bb.plain("Image CVE report stored in: %s" % manifest_name) +- + if enable_json: + manifest_name_suffix = d.getVar("CVE_CHECK_MANIFEST_JSON_SUFFIX") + link_path = os.path.join(deploy_dir, "%s.%s" % (link_name, manifest_name_suffix)) +@@ -294,7 +258,51 @@ ROOTFS_POSTPROCESS_COMMAND:prepend = "${@'cve_check_write_rootfs_manifest ' if d + do_rootfs[recrdeptask] += "${@'do_cve_check' if d.getVar('CVE_CHECK_CREATE_MANIFEST') == '1' else ''}" + do_populate_sdk[recrdeptask] += "${@'do_cve_check' if d.getVar('CVE_CHECK_CREATE_MANIFEST') == '1' else ''}" + +-def check_cves(d, patched_cves): ++def cve_is_ignored(d, cve_data, cve): ++ if cve not in cve_data: ++ return False ++ if cve_data[cve]['abbrev-status'] == "Ignored": ++ return True ++ return False ++ ++def cve_is_patched(d, cve_data, cve): ++ if cve not in cve_data: ++ return False ++ if cve_data[cve]['abbrev-status'] == "Patched": ++ return True ++ return False ++ ++def cve_update(d, cve_data, cve, entry): ++ # If no entry, just add it ++ if cve not in cve_data: ++ cve_data[cve] = entry ++ return ++ # If we are updating, there might be change in the status ++ bb.debug("Trying CVE entry update for %s from %s to %s" % (cve, cve_data[cve]['abbrev-status'], entry['abbrev-status'])) ++ if cve_data[cve]['abbrev-status'] == "Unknown": ++ cve_data[cve] = entry ++ return ++ if cve_data[cve]['abbrev-status'] == entry['abbrev-status']: ++ return ++ # Update like in {'abbrev-status': 'Patched', 'status': 'version-not-in-range'} to {'abbrev-status': 'Unpatched', 'status': 'version-in-range'} ++ if entry['abbrev-status'] == "Unpatched" and cve_data[cve]['abbrev-status'] == "Patched": ++ if entry['status'] == "version-in-range" and cve_data[cve]['status'] == "version-not-in-range": ++ # New result from the scan, vulnerable ++ cve_data[cve] = entry ++ bb.debug("CVE entry %s update from Patched to Unpatched from the scan result" % cve) ++ return ++ if entry['abbrev-status'] == "Patched" and cve_data[cve]['abbrev-status'] == "Unpatched": ++ if entry['status'] == "version-not-in-range" and cve_data[cve]['status'] == "version-in-range": ++ # Range does not match the scan, but we already have a vulnerable match, ignore ++ bb.debug("CVE entry %s update from Patched to Unpatched from the scan result - not applying" % cve) ++ return ++ # If we have an "Ignored", it has a priority ++ if cve_data[cve]['abbrev-status'] == "Ignored": ++ bb.debug("CVE %s not updating because Ignored" % cve) ++ return ++ bb.warn("Unhandled CVE entry update for %s from %s to %s" % (cve, cve_data[cve], entry)) ++ ++def check_cves(d, cve_data): + """ + Connect to the NVD database and find unpatched cves. + """ +@@ -304,28 +312,19 @@ def check_cves(d, patched_cves): + real_pv = d.getVar("PV") + suffix = d.getVar("CVE_VERSION_SUFFIX") + +- cves_unpatched = [] +- cves_ignored = [] + cves_status = [] + cves_in_recipe = False + # CVE_PRODUCT can contain more than one product (eg. curl/libcurl) + products = d.getVar("CVE_PRODUCT").split() + # If this has been unset then we're not scanning for CVEs here (for example, image recipes) + if not products: +- return ([], [], [], []) ++ return ([], []) + pv = d.getVar("CVE_VERSION").split("+git")[0] + + # If the recipe has been skipped/ignored we return empty lists + if pn in d.getVar("CVE_CHECK_SKIP_RECIPE").split(): + bb.note("Recipe has been skipped by cve-check") +- return ([], [], [], []) +- +- # Convert CVE_STATUS into ignored CVEs and check validity +- cve_ignore = [] +- for cve in (d.getVarFlags("CVE_STATUS") or {}): +- decoded_status, _, _ = decode_cve_status(d, cve) +- if decoded_status == "Ignored": +- cve_ignore.append(cve) ++ return ([], []) + + import sqlite3 + db_file = d.expand("file:${CVE_CHECK_DB_FILE}?mode=ro") +@@ -344,11 +343,10 @@ def check_cves(d, patched_cves): + for cverow in cve_cursor: + cve = cverow[0] + +- if cve in cve_ignore: ++ if cve_is_ignored(d, cve_data, cve): + bb.note("%s-%s ignores %s" % (product, pv, cve)) +- cves_ignored.append(cve) + continue +- elif cve in patched_cves: ++ elif cve_is_patched(d, cve_data, cve): + bb.note("%s has been patched" % (cve)) + continue + # Write status once only for each product +@@ -364,7 +362,7 @@ def check_cves(d, patched_cves): + for row in product_cursor: + (_, _, _, version_start, operator_start, version_end, operator_end) = row + #bb.debug(2, "Evaluating row " + str(row)) +- if cve in cve_ignore: ++ if cve_is_ignored(d, cve_data, cve): + ignored = True + + version_start = convert_cve_version(version_start) +@@ -403,16 +401,16 @@ def check_cves(d, patched_cves): + if vulnerable: + if ignored: + bb.note("%s is ignored in %s-%s" % (cve, pn, real_pv)) +- cves_ignored.append(cve) ++ cve_update(d, cve_data, cve, {"abbrev-status": "Ignored"}) + else: + bb.note("%s-%s is vulnerable to %s" % (pn, real_pv, cve)) +- cves_unpatched.append(cve) ++ cve_update(d, cve_data, cve, {"abbrev-status": "Unpatched", "status": "version-in-range"}) + break + product_cursor.close() + + if not vulnerable: + bb.note("%s-%s is not vulnerable to %s" % (pn, real_pv, cve)) +- patched_cves.add(cve) ++ cve_update(d, cve_data, cve, {"abbrev-status": "Patched", "status": "version-not-in-range"}) + cve_cursor.close() + + if not cves_in_product: +@@ -420,123 +418,38 @@ def check_cves(d, patched_cves): + cves_status.append([product, False]) + + conn.close() +- diff_ignore = list(set(cve_ignore) - set(cves_ignored)) +- if diff_ignore: +- oe.qa.handle_error("cve_status_not_in_db", "Found CVE (%s) with CVE_STATUS set that are not found in database for this component" % " ".join(diff_ignore), d) + + if not cves_in_recipe: + bb.note("No CVE records for products in recipe %s" % (pn)) + +- return (list(cves_ignored), list(patched_cves), cves_unpatched, cves_status) ++ return (cve_data, cves_status) + +-def get_cve_info(d, cves): ++def get_cve_info(d, cve_data): + """ + Get CVE information from the database. + """ + + import sqlite3 + +- cve_data = {} + db_file = d.expand("file:${CVE_CHECK_DB_FILE}?mode=ro") + conn = sqlite3.connect(db_file, uri=True) + +- for cve in cves: ++ for cve in cve_data: + cursor = conn.execute("SELECT * FROM NVD WHERE ID IS ?", (cve,)) + for row in cursor: +- cve_data[row[0]] = {} +- cve_data[row[0]]["summary"] = row[1] +- cve_data[row[0]]["scorev2"] = row[2] +- cve_data[row[0]]["scorev3"] = row[3] +- cve_data[row[0]]["modified"] = row[4] +- cve_data[row[0]]["vector"] = row[5] +- cve_data[row[0]]["vectorString"] = row[6] ++ # The CVE itdelf has been added already ++ if row[0] not in cve_data: ++ bb.note("CVE record %s not present" % row[0]) ++ continue ++ #cve_data[row[0]] = {} ++ cve_data[row[0]]["NVD-summary"] = row[1] ++ cve_data[row[0]]["NVD-scorev2"] = row[2] ++ cve_data[row[0]]["NVD-scorev3"] = row[3] ++ cve_data[row[0]]["NVD-modified"] = row[4] ++ cve_data[row[0]]["NVD-vector"] = row[5] ++ cve_data[row[0]]["NVD-vectorString"] = row[6] + cursor.close() + conn.close() +- return cve_data +- +-def cve_write_data_text(d, patched, unpatched, ignored, cve_data): +- """ +- Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and +- CVE manifest if enabled. +- """ +- +- from oe.cve_check import decode_cve_status +- +- cve_file = d.getVar("CVE_CHECK_LOG") +- fdir_name = d.getVar("FILE_DIRNAME") +- layer = fdir_name.split("/")[-3] +- +- include_layers = d.getVar("CVE_CHECK_LAYER_INCLUDELIST").split() +- exclude_layers = d.getVar("CVE_CHECK_LAYER_EXCLUDELIST").split() +- +- report_all = d.getVar("CVE_CHECK_REPORT_PATCHED") == "1" +- +- if exclude_layers and layer in exclude_layers: +- return +- +- if include_layers and layer not in include_layers: +- return +- +- # Early exit, the text format does not report packages without CVEs +- if not patched+unpatched+ignored: +- return +- +- nvd_link = "https://nvd.nist.gov/vuln/detail/" +- write_string = "" +- unpatched_cves = [] +- bb.utils.mkdirhier(os.path.dirname(cve_file)) +- +- for cve in sorted(cve_data): +- is_patched = cve in patched +- is_ignored = cve in ignored +- +- status = "Unpatched" +- if (is_patched or is_ignored) and not report_all: +- continue +- if is_ignored: +- status = "Ignored" +- elif is_patched: +- status = "Patched" +- else: +- # default value of status is Unpatched +- unpatched_cves.append(cve) +- +- write_string += "LAYER: %s\n" % layer +- write_string += "PACKAGE NAME: %s\n" % d.getVar("PN") +- write_string += "PACKAGE VERSION: %s%s\n" % (d.getVar("EXTENDPE"), d.getVar("PV")) +- write_string += "CVE: %s\n" % cve +- write_string += "CVE STATUS: %s\n" % status +- _, detail, description = decode_cve_status(d, cve) +- if detail: +- write_string += "CVE DETAIL: %s\n" % detail +- if description: +- write_string += "CVE DESCRIPTION: %s\n" % description +- write_string += "CVE SUMMARY: %s\n" % cve_data[cve]["summary"] +- write_string += "CVSS v2 BASE SCORE: %s\n" % cve_data[cve]["scorev2"] +- write_string += "CVSS v3 BASE SCORE: %s\n" % cve_data[cve]["scorev3"] +- write_string += "VECTOR: %s\n" % cve_data[cve]["vector"] +- write_string += "VECTORSTRING: %s\n" % cve_data[cve]["vectorString"] +- write_string += "MORE INFORMATION: %s%s\n\n" % (nvd_link, cve) +- +- if unpatched_cves and d.getVar("CVE_CHECK_SHOW_WARNINGS") == "1": +- bb.warn("Found unpatched CVE (%s), for more information check %s" % (" ".join(unpatched_cves),cve_file)) +- +- with open(cve_file, "w") as f: +- bb.note("Writing file %s with CVE information" % cve_file) +- f.write(write_string) +- +- if d.getVar("CVE_CHECK_COPY_FILES") == "1": +- deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE") +- bb.utils.mkdirhier(os.path.dirname(deploy_file)) +- with open(deploy_file, "w") as f: +- f.write(write_string) +- +- if d.getVar("CVE_CHECK_CREATE_MANIFEST") == "1": +- cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR") +- bb.utils.mkdirhier(cvelogpath) +- +- with open(d.getVar("CVE_CHECK_TMP_FILE"), "a") as f: +- f.write("%s" % write_string) + + def cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file): + """ +@@ -568,13 +481,11 @@ def cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_fi + with open(index_path, "a+") as f: + f.write("%s\n" % fragment_path) + +-def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status): ++def cve_write_data_json(d, cve_data, cve_status): + """ + Prepare CVE data for the JSON format, then write it. + """ + +- from oe.cve_check import decode_cve_status +- + output = {"version":"1", "package": []} + nvd_link = "https://nvd.nist.gov/vuln/detail/" + +@@ -592,8 +503,6 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status): + if include_layers and layer not in include_layers: + return + +- unpatched_cves = [] +- + product_data = [] + for s in cve_status: + p = {"product": s[0], "cvesInRecord": "Yes"} +@@ -608,39 +517,31 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status): + "version" : package_version, + "products": product_data + } ++ + cve_list = [] + + for cve in sorted(cve_data): +- is_patched = cve in patched +- is_ignored = cve in ignored +- status = "Unpatched" +- if (is_patched or is_ignored) and not report_all: ++ if not report_all and (cve_data[cve]["abbrev-status"] == "Patched" or cve_data[cve]["abbrev-status"] == "Ignored"): + continue +- if is_ignored: +- status = "Ignored" +- elif is_patched: +- status = "Patched" +- else: +- # default value of status is Unpatched +- unpatched_cves.append(cve) +- + issue_link = "%s%s" % (nvd_link, cve) + + cve_item = { + "id" : cve, +- "summary" : cve_data[cve]["summary"], +- "scorev2" : cve_data[cve]["scorev2"], +- "scorev3" : cve_data[cve]["scorev3"], +- "vector" : cve_data[cve]["vector"], +- "vectorString" : cve_data[cve]["vectorString"], +- "status" : status, +- "link": issue_link ++ "status" : cve_data[cve]["abbrev-status"], ++ "link": issue_link, + } +- _, detail, description = decode_cve_status(d, cve) +- if detail: +- cve_item["detail"] = detail +- if description: +- cve_item["description"] = description ++ if 'NVD-summary' in cve_data[cve]: ++ cve_item["summary"] = cve_data[cve]["NVD-summary"] ++ cve_item["scorev2"] = cve_data[cve]["NVD-scorev2"] ++ cve_item["scorev3"] = cve_data[cve]["NVD-scorev3"] ++ cve_item["vector"] = cve_data[cve]["NVD-vector"] ++ cve_item["vectorString"] = cve_data[cve]["NVD-vectorString"] ++ if 'status' in cve_data[cve]: ++ cve_item["detail"] = cve_data[cve]["status"] ++ if 'justification' in cve_data[cve]: ++ cve_item["description"] = cve_data[cve]["justification"] ++ if 'resource' in cve_data[cve]: ++ cve_item["patch-file"] = cve_data[cve]["resource"] + cve_list.append(cve_item) + + package_data["issue"] = cve_list +@@ -652,12 +553,10 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status): + + cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file) + +-def cve_write_data(d, patched, unpatched, ignored, cve_data, status): ++def cve_write_data(d, cve_data, status): + """ + Write CVE data in each enabled format. + """ + +- if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1": +- cve_write_data_text(d, patched, unpatched, ignored, cve_data) + if d.getVar("CVE_CHECK_FORMAT_JSON") == "1": +- cve_write_data_json(d, patched, unpatched, ignored, cve_data, status) ++ cve_write_data_json(d, cve_data, status) +diff --git a/meta/classes/vex.bbclass b/meta/classes/vex.bbclass +new file mode 100644 +index 0000000000..bb16e2a529 +--- /dev/null ++++ b/meta/classes/vex.bbclass +@@ -0,0 +1,310 @@ ++# ++# Copyright OpenEmbedded Contributors ++# ++# SPDX-License-Identifier: MIT ++# ++ ++# This class is used to generate metadata needed by external ++# tools to check for vulnerabilities, for example CVEs. ++# ++# In order to use this class just inherit the class in the ++# local.conf file and it will add the generate_vex task for ++# every recipe. If an image is build it will generate a report ++# in DEPLOY_DIR_IMAGE for all the packages used, it will also ++# generate a file for all recipes used in the build. ++# ++# Variables use CVE_CHECK prefix to keep compatibility with ++# the cve-check class ++# ++# Example: ++# bitbake -c generate_vex openssl ++# bitbake core-image-sato ++# bitbake -k -c generate_vex universe ++# ++# The product name that the CVE database uses defaults to BPN, but may need to ++# be overriden per recipe (for example tiff.bb sets CVE_PRODUCT=libtiff). ++CVE_PRODUCT ??= "${BPN}" ++CVE_VERSION ??= "${PV}" ++ ++CVE_CHECK_SUMMARY_DIR ?= "${LOG_DIR}/cve" ++ ++CVE_CHECK_SUMMARY_FILE_NAME_JSON = "cve-summary.json" ++CVE_CHECK_SUMMARY_INDEX_PATH = "${CVE_CHECK_SUMMARY_DIR}/cve-summary-index.txt" ++ ++CVE_CHECK_DIR ??= "${DEPLOY_DIR}/cve" ++CVE_CHECK_RECIPE_FILE_JSON ?= "${CVE_CHECK_DIR}/${PN}_cve.json" ++CVE_CHECK_MANIFEST_JSON ?= "${IMGDEPLOYDIR}/${IMAGE_NAME}.json" ++ ++# Skip CVE Check for packages (PN) ++CVE_CHECK_SKIP_RECIPE ?= "" ++ ++# Replace NVD DB check status for a given CVE. Each of CVE has to be mentioned ++# separately with optional detail and description for this status. ++# ++# CVE_STATUS[CVE-1234-0001] = "not-applicable-platform: Issue only applies on Windows" ++# CVE_STATUS[CVE-1234-0002] = "fixed-version: Fixed externally" ++# ++# Settings the same status and reason for multiple CVEs is possible ++# via CVE_STATUS_GROUPS variable. ++# ++# CVE_STATUS_GROUPS = "CVE_STATUS_WIN CVE_STATUS_PATCHED" ++# ++# CVE_STATUS_WIN = "CVE-1234-0001 CVE-1234-0003" ++# CVE_STATUS_WIN[status] = "not-applicable-platform: Issue only applies on Windows" ++# CVE_STATUS_PATCHED = "CVE-1234-0002 CVE-1234-0004" ++# CVE_STATUS_PATCHED[status] = "fixed-version: Fixed externally" ++# ++# All possible CVE statuses could be found in cve-check-map.conf ++# CVE_CHECK_STATUSMAP[not-applicable-platform] = "Ignored" ++# CVE_CHECK_STATUSMAP[fixed-version] = "Patched" ++# ++# CVE_CHECK_IGNORE is deprecated and CVE_STATUS has to be used instead. ++# Keep CVE_CHECK_IGNORE until other layers migrate to new variables ++CVE_CHECK_IGNORE ?= "" ++ ++# Layers to be excluded ++CVE_CHECK_LAYER_EXCLUDELIST ??= "" ++ ++# Layers to be included ++CVE_CHECK_LAYER_INCLUDELIST ??= "" ++ ++ ++# set to "alphabetical" for version using single alphabetical character as increment release ++CVE_VERSION_SUFFIX ??= "" ++ ++python () { ++ if bb.data.inherits_class("cve-check", d): ++ raise bb.parse.SkipRecipe("Skipping recipe: found incompatible combination of cve-check and vex enabled at the same time.") ++ ++ # Fallback all CVEs from CVE_CHECK_IGNORE to CVE_STATUS ++ cve_check_ignore = d.getVar("CVE_CHECK_IGNORE") ++ if cve_check_ignore: ++ bb.warn("CVE_CHECK_IGNORE is deprecated in favor of CVE_STATUS") ++ for cve in (d.getVar("CVE_CHECK_IGNORE") or "").split(): ++ d.setVarFlag("CVE_STATUS", cve, "ignored") ++ ++ # Process CVE_STATUS_GROUPS to set multiple statuses and optional detail or description at once ++ for cve_status_group in (d.getVar("CVE_STATUS_GROUPS") or "").split(): ++ cve_group = d.getVar(cve_status_group) ++ if cve_group is not None: ++ for cve in cve_group.split(): ++ d.setVarFlag("CVE_STATUS", cve, d.getVarFlag(cve_status_group, "status")) ++ else: ++ bb.warn("CVE_STATUS_GROUPS contains undefined variable %s" % cve_status_group) ++} ++ ++def generate_json_report(d, out_path, link_path): ++ if os.path.exists(d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")): ++ import json ++ from oe.cve_check import cve_check_merge_jsons, update_symlinks ++ ++ bb.note("Generating JSON CVE summary") ++ index_file = d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH") ++ summary = {"version":"1", "package": []} ++ with open(index_file) as f: ++ filename = f.readline() ++ while filename: ++ with open(filename.rstrip()) as j: ++ data = json.load(j) ++ cve_check_merge_jsons(summary, data) ++ filename = f.readline() ++ ++ summary["package"].sort(key=lambda d: d['name']) ++ ++ with open(out_path, "w") as f: ++ json.dump(summary, f, indent=2) ++ ++ update_symlinks(out_path, link_path) ++ ++python vex_save_summary_handler () { ++ import shutil ++ import datetime ++ from oe.cve_check import update_symlinks ++ ++ cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR") ++ ++ bb.utils.mkdirhier(cvelogpath) ++ timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S') ++ ++ json_summary_link_name = os.path.join(cvelogpath, d.getVar("CVE_CHECK_SUMMARY_FILE_NAME_JSON")) ++ json_summary_name = os.path.join(cvelogpath, "cve-summary-%s.json" % (timestamp)) ++ generate_json_report(d, json_summary_name, json_summary_link_name) ++ bb.plain("Complete CVE JSON report summary created at: %s" % json_summary_link_name) ++} ++ ++addhandler vex_save_summary_handler ++vex_save_summary_handler[eventmask] = "bb.event.BuildCompleted" ++ ++python do_generate_vex () { ++ """ ++ Generate metadata needed for vulnerability checking for ++ the current recipe ++ """ ++ from oe.cve_check import get_patched_cves ++ ++ try: ++ patched_cves = get_patched_cves(d) ++ cves_status = [] ++ products = d.getVar("CVE_PRODUCT").split() ++ for product in products: ++ if ":" in product: ++ _, product = product.split(":", 1) ++ cves_status.append([product, False]) ++ ++ except FileNotFoundError: ++ bb.fatal("Failure in searching patches") ++ ++ cve_write_data_json(d, patched_cves, cves_status) ++} ++ ++addtask generate_vex before do_build ++ ++python vex_cleanup () { ++ """ ++ Delete the file used to gather all the CVE information. ++ """ ++ bb.utils.remove(e.data.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")) ++} ++ ++addhandler vex_cleanup ++vex_cleanup[eventmask] = "bb.event.BuildCompleted" ++ ++python vex_write_rootfs_manifest () { ++ """ ++ Create VEX/CVE manifest when building an image ++ """ ++ ++ import json ++ from oe.rootfs import image_list_installed_packages ++ from oe.cve_check import cve_check_merge_jsons, update_symlinks ++ ++ deploy_file_json = d.getVar("CVE_CHECK_RECIPE_FILE_JSON") ++ if os.path.exists(deploy_file_json): ++ bb.utils.remove(deploy_file_json) ++ ++ # Create a list of relevant recipies ++ recipies = set() ++ for pkg in list(image_list_installed_packages(d)): ++ pkg_info = os.path.join(d.getVar('PKGDATA_DIR'), ++ 'runtime-reverse', pkg) ++ pkg_data = oe.packagedata.read_pkgdatafile(pkg_info) ++ recipies.add(pkg_data["PN"]) ++ ++ bb.note("Writing rootfs VEX manifest") ++ deploy_dir = d.getVar("IMGDEPLOYDIR") ++ link_name = d.getVar("IMAGE_LINK_NAME") ++ ++ json_data = {"version":"1", "package": []} ++ text_data = "" ++ ++ save_pn = d.getVar("PN") ++ ++ for pkg in recipies: ++ # To be able to use the CVE_CHECK_RECIPE_FILE_JSON variable we have to evaluate ++ # it with the different PN names set each time. ++ d.setVar("PN", pkg) ++ ++ pkgfilepath = d.getVar("CVE_CHECK_RECIPE_FILE_JSON") ++ if os.path.exists(pkgfilepath): ++ with open(pkgfilepath) as j: ++ data = json.load(j) ++ cve_check_merge_jsons(json_data, data) ++ ++ d.setVar("PN", save_pn) ++ ++ link_path = os.path.join(deploy_dir, "%s.json" % link_name) ++ manifest_name = d.getVar("CVE_CHECK_MANIFEST_JSON") ++ ++ with open(manifest_name, "w") as f: ++ json.dump(json_data, f, indent=2) ++ ++ update_symlinks(manifest_name, link_path) ++ bb.plain("Image VEX JSON report stored in: %s" % manifest_name) ++} ++ ++ROOTFS_POSTPROCESS_COMMAND:prepend = "vex_write_rootfs_manifest; " ++do_rootfs[recrdeptask] += "do_generate_vex " ++do_populate_sdk[recrdeptask] += "do_generate_vex " ++ ++def cve_write_data_json(d, cve_data, cve_status): ++ """ ++ Prepare CVE data for the JSON format, then write it. ++ Done for each recipe. ++ """ ++ ++ from oe.cve_check import get_cpe_ids ++ import json ++ ++ output = {"version":"1", "package": []} ++ nvd_link = "https://nvd.nist.gov/vuln/detail/" ++ ++ fdir_name = d.getVar("FILE_DIRNAME") ++ layer = fdir_name.split("/")[-3] ++ ++ include_layers = d.getVar("CVE_CHECK_LAYER_INCLUDELIST").split() ++ exclude_layers = d.getVar("CVE_CHECK_LAYER_EXCLUDELIST").split() ++ ++ if exclude_layers and layer in exclude_layers: ++ return ++ ++ if include_layers and layer not in include_layers: ++ return ++ ++ product_data = [] ++ for s in cve_status: ++ p = {"product": s[0], "cvesInRecord": "Yes"} ++ if s[1] == False: ++ p["cvesInRecord"] = "No" ++ product_data.append(p) ++ product_data = list({p['product']:p for p in product_data}.values()) ++ ++ package_version = "%s%s" % (d.getVar("EXTENDPE"), d.getVar("PV")) ++ cpes = get_cpe_ids(d.getVar("CVE_PRODUCT"), d.getVar("CVE_VERSION")) ++ package_data = { ++ "name" : d.getVar("PN"), ++ "layer" : layer, ++ "version" : package_version, ++ "products": product_data, ++ "cpes": cpes ++ } ++ ++ cve_list = [] ++ ++ for cve in sorted(cve_data): ++ issue_link = "%s%s" % (nvd_link, cve) ++ ++ cve_item = { ++ "id" : cve, ++ "status" : cve_data[cve]["abbrev-status"], ++ "link": issue_link, ++ } ++ if 'NVD-summary' in cve_data[cve]: ++ cve_item["summary"] = cve_data[cve]["NVD-summary"] ++ cve_item["scorev2"] = cve_data[cve]["NVD-scorev2"] ++ cve_item["scorev3"] = cve_data[cve]["NVD-scorev3"] ++ cve_item["vector"] = cve_data[cve]["NVD-vector"] ++ cve_item["vectorString"] = cve_data[cve]["NVD-vectorString"] ++ if 'status' in cve_data[cve]: ++ cve_item["detail"] = cve_data[cve]["status"] ++ if 'justification' in cve_data[cve]: ++ cve_item["description"] = cve_data[cve]["justification"] ++ if 'resource' in cve_data[cve]: ++ cve_item["patch-file"] = cve_data[cve]["resource"] ++ cve_list.append(cve_item) ++ ++ package_data["issue"] = cve_list ++ output["package"].append(package_data) ++ ++ deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE_JSON") ++ ++ write_string = json.dumps(output, indent=2) ++ ++ cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR") ++ index_path = d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH") ++ bb.utils.mkdirhier(cvelogpath) ++ fragment_file = os.path.basename(deploy_file) ++ fragment_path = os.path.join(cvelogpath, fragment_file) ++ with open(fragment_path, "w") as f: ++ f.write(write_string) ++ with open(index_path, "a+") as f: ++ f.write("%s\n" % fragment_path) +diff --git a/meta/lib/oe/cve_check.py b/meta/lib/oe/cve_check.py +index ed5c714cb8..487f30dc25 100644 +--- a/meta/lib/oe/cve_check.py ++++ b/meta/lib/oe/cve_check.py +@@ -88,7 +88,7 @@ def get_patched_cves(d): + # (cve_match regular expression) + cve_file_name_match = re.compile(r".*(CVE-\d{4}-\d+)", re.IGNORECASE) + +- patched_cves = set() ++ patched_cves = {} + patches = oe.patch.src_patches(d) + bb.debug(2, "Scanning %d patches for CVEs" % len(patches)) + for url in patches: +@@ -98,7 +98,7 @@ def get_patched_cves(d): + fname_match = cve_file_name_match.search(patch_file) + if fname_match: + cve = fname_match.group(1).upper() +- patched_cves.add(cve) ++ patched_cves[cve] = {"abbrev-status": "Patched", "status": "fix-file-included", "resource": patch_file} + bb.debug(2, "Found %s from patch file name %s" % (cve, patch_file)) + + # Remote patches won't be present and compressed patches won't be +@@ -124,7 +124,7 @@ def get_patched_cves(d): + cves = patch_text[match.start()+5:match.end()] + for cve in cves.split(): + bb.debug(2, "Patch %s solves %s" % (patch_file, cve)) +- patched_cves.add(cve) ++ patched_cves[cve] = {"abbrev-status": "Patched", "status": "fix-file-included", "resource": patch_file} + text_match = True + + if not fname_match and not text_match: +@@ -132,10 +132,16 @@ def get_patched_cves(d): + + # Search for additional patched CVEs + for cve in (d.getVarFlags("CVE_STATUS") or {}): +- decoded_status, _, _ = decode_cve_status(d, cve) +- if decoded_status == "Patched": +- bb.debug(2, "CVE %s is additionally patched" % cve) +- patched_cves.add(cve) ++ decoded_status = decode_cve_status(d, cve) ++ products = d.getVar("CVE_PRODUCT") ++ if has_cve_product_match(decoded_status, products) == True: ++ patched_cves[cve] = { ++ "abbrev-status": decoded_status["mapping"], ++ "status": decoded_status["detail"], ++ "justification": decoded_status["description"], ++ "affected-vendor": decoded_status["vendor"], ++ "affected-product": decoded_status["product"] ++ } + + return patched_cves + +@@ -227,19 +233,57 @@ def convert_cve_version(version): + + def decode_cve_status(d, cve): + """ +- Convert CVE_STATUS into status, detail and description. ++ Convert CVE_STATUS into status, vendor, product, detail and description. + """ + status = d.getVarFlag("CVE_STATUS", cve) + if not status: +- return ("", "", "") ++ return {} ++ ++ status_split = status.split(':', 5) ++ status_out = {} ++ status_out["detail"] = status_split[0] ++ product = "*" ++ vendor = "*" ++ description = "" ++ if len(status_split) >= 4 and status_split[1].strip() == "cpe": ++ # Both vendor and product are mandatory if cpe: present, the syntax is then: ++ # detail: cpe:vendor:product:description ++ vendor = status_split[2].strip() ++ product = status_split[3].strip() ++ description = status_split[4].strip() ++ elif len(status_split) >= 2 and status_split[1].strip() == "cpe": ++ # Malformed CPE ++ bb.warn('Invalid CPE information for CVE_STATUS[%s] = "%s", not setting CPE' % (detail, cve, status)) ++ else: ++ # Other case: no CPE, the syntax is then: ++ # detail: description ++ description = status_split[len(status_split)-1].strip() if (len(status_split) > 1) else "" + +- status_split = status.split(':', 1) +- detail = status_split[0] +- description = status_split[1].strip() if (len(status_split) > 1) else "" ++ status_out["vendor"] = vendor ++ status_out["product"] = product ++ status_out["description"] = description + +- status_mapping = d.getVarFlag("CVE_CHECK_STATUSMAP", detail) ++ status_mapping = d.getVarFlag("CVE_CHECK_STATUSMAP", status_out['detail']) + if status_mapping is None: + bb.warn('Invalid detail "%s" for CVE_STATUS[%s] = "%s", fallback to Unpatched' % (detail, cve, status)) + status_mapping = "Unpatched" ++ status_out["mapping"] = status_mapping ++ ++ return status_out ++ ++def has_cve_product_match(detailed_status, products): ++ """ ++ Check product/vendor match between detailed_status from decode_cve_status and a string of ++ products (like from CVE_PRODUCT) ++ """ ++ for product in products.split(): ++ vendor = "*" ++ if ":" in product: ++ vendor, product = product.split(":", 1) ++ ++ if (vendor == detailed_status["vendor"] or detailed_status["vendor"] == "*") and \ ++ (product == detailed_status["product"] or detailed_status["product"] == "*"): ++ return True + +- return (status_mapping, detail, description) ++ #if no match, return False ++ return False +-- +2.45.1 + diff --git a/configs/repositories.conf b/configs/repositories.conf deleted file mode 100644 index d269188..0000000 --- a/configs/repositories.conf +++ /dev/null @@ -1,6 +0,0 @@ -# " -repositories/meta-enapter https://github.com/enapter/meta-enapter release/3.0 -repositories/meta-intel git://git.yoctoproject.org/meta-intel.git cfb0e237cf3f25ffb128bd55e3e72eb0fb2ca096 -repositories/meta-openembedded git://git.openembedded.org/meta-openembedded 1235dd4ed4a57e67683c045ad76b6a0f9e896b45 -repositories/meta-virtualization git://git.yoctoproject.org/meta-virtualization 54b806b1985f3989722ee308e1073530fe3328c1 -repositories/poky git://git.yoctoproject.org/poky.git yocto-5.0.3 diff --git a/configs/versions.env b/configs/versions.env index 36ab794..3f501cb 100644 --- a/configs/versions.env +++ b/configs/versions.env @@ -1 +1 @@ -ENAPTER_LINUX_BASE_VERSION="3.0.0" +ENAPTER_LINUX_BASE_VERSION="v3.0.0" diff --git a/docker-compose-buildagent.yml b/docker-compose-buildagent.yml index 08e2b03..6a253e4 100644 --- a/docker-compose-buildagent.yml +++ b/docker-compose-buildagent.yml @@ -1,26 +1,39 @@ version: "3" services: intel-x86-64-build: - image: enapter/build-yocto:v0.3 + image: enapter/build-yocto:v0.8 network_mode: host + privileged: true environment: - - SKIP_SSTATE_SAVE - - SKIP_SSTATE_LOAD + - DISTRO + - DISTRO_VERSION + - IMG_ARTIFACT_NAME + - IMG_FILE_ARTIFACT_NAME + - INITRAMFS_SPDX_ARTIFACT_NAME + - MODSIGN_SIGNING_CERT + - MODSIGN_SIGNING_CERT_BASE64 + - MODSIGN_SIGNING_KEY + - MODSIGN_SIGNING_KEY_BASE64 + - NVDCVE_API_KEY + - RAUC_CERT + - RAUC_CERT_BASE64 + - RAUC_KEY + - RAUC_KEYRING + - RAUC_KEYRING_BASE64 + - RAUC_KEY_BASE64 + - RAUC_UPDATE_ARTIFACT_NAME + - ROOTFS_SPDX_ARTIFACT_NAME - SECURE_BOOT_SIGNING_CERT - SECURE_BOOT_SIGNING_CERT_BASE64 - - SECURE_BOOT_SIGNING_KEY - - SECURE_BOOT_SIGNING_KEY_BASE64 - SECURE_BOOT_SIGNING_CERT_DER - SECURE_BOOT_SIGNING_CERT_DER_BASE64 - - BB_ENV_PASSTHROUGH_ADDITIONS - - DISTRO_VERSION - - IMG_ARTIFACT_NAME - - IMG_FILE_ARTIFACT_NAME + - SECURE_BOOT_SIGNING_KEY + - SECURE_BOOT_SIGNING_KEY_BASE64 + - SKIP_SSTATE_LOAD + - SKIP_SSTATE_SAVE - UPDATE_ARTIFACT_NAME + - VEX_ARTIFACT_NAME - VMDK_ARTIFACT_NAME - - SSTATE_DIR - - DL_DIR - - TMPDIR volumes: - ${CI_PROJECT_DIR}:/home/build/enapter-linux-build-source - ${CI_PROJECT_DIR}/docker/bin:/home/build/bin diff --git a/docker/bin/build-intel-x86-64.sh b/docker/bin/build-intel-x86-64.sh index 4d5cca5..962654c 100755 --- a/docker/bin/build-intel-x86-64.sh +++ b/docker/bin/build-intel-x86-64.sh @@ -4,15 +4,12 @@ set -ex -/home/build/bin/prepare-sign-keys.sh /home/build/bin/sync-sources.sh +/home/build/bin/prepare-sign-keys.sh /home/build/bin/load-sstate.sh -cd "/home/build/enapter-linux-build/repositories/poky" -source oe-init-build-env ../../machine/ -cd .. - -bitbake enapter-industrial-linux-image +cd "/home/build/enapter-linux-build" +kas build configs/enapter-industrial-linux.yml /home/build/bin/save-sstate.sh -/home/build/bin/prepare-artifacts.sh +kas shell configs/enapter-industrial-linux.yml -c /home/build/bin/prepare-artifacts.sh diff --git a/docker/bin/debug-intel-x86-64.sh b/docker/bin/debug-intel-x86-64.sh index 04333c5..8cdd2af 100755 --- a/docker/bin/debug-intel-x86-64.sh +++ b/docker/bin/debug-intel-x86-64.sh @@ -1,15 +1,11 @@ # SPDX-FileCopyrightText: 2024 Enapter # SPDX-License-Identifier: Apache-2.0 -/home/build/bin/prepare-sign-keys.sh /home/build/bin/sync-sources.sh +/home/build/bin/prepare-sign-keys.sh /home/build/bin/load-sstate.sh -cd "/home/build/enapter-linux-build/repositories/poky" -source oe-init-build-env ../../machine/ -cd .. - -# pip3 install -r /home/build/enapter-linux-build/repositories/poky/bitbake/toaster-requirements.txt -# source toaster start webport=0.0.0.0:8080 +# ./oe-init-build-env ~/enapter-linux-build/build/ -echo "/home/build/bin/sync-sources.sh && bitbake -k enapter-industrial-linux-image" +cd "/home/build/enapter-linux-build" +echo "/home/build/bin/sync-sources.sh && kas build configs/enapter-industrial-linux.yml" diff --git a/docker/bin/prepare-artifacts.sh b/docker/bin/prepare-artifacts.sh index 5803bec..605b810 100755 --- a/docker/bin/prepare-artifacts.sh +++ b/docker/bin/prepare-artifacts.sh @@ -4,44 +4,169 @@ set -ex +efi_boot_dir="EFI/BOOT" +efi_enapter_dir="EFI/enapter" +initramfs_spdx="enapter-industrial-linux-initramfs-intel-corei7-64.spdx.tar.zst" +install_bundle_name="install.raucb" +rauc_manifest="manifest.raucm" +root_ext4_file="enapter-industrial-linux-rootfs-intel-corei7-64.rootfs.ext4" +rootfs_spdx="enapter-industrial-linux-rootfs-intel-corei7-64.rootfs.spdx.tar.zst" +vex_file="cve-summary.json" wic_file="enapter-industrial-linux-image-intel-corei7-64.rootfs.wic" -vmdk_file="enapter-industrial-linux-image-intel-corei7-64.rootfs.wic.vmdk" -update_file="enapter-industrial-linux-update.zip" - -update_dir=/home/build/update -images_dir=/home/build/images -deploy_dir=/home/build/tmp-glibc/deploy/images/intel-corei7-64 - -mv "$deploy_dir/$wic_file" "$deploy_dir/$IMG_FILE_ARTIFACT_NAME" +boot_files="grubx64.efi grub.cfg" +cve_dir="/home/build/tmp-glibc/log/cve" +deploy_dir="/home/build/tmp-glibc/deploy/images/intel-corei7-64" +enapter_files="rootfs.img bzImage initrd version.txt" +images_dir="/home/build/images" img_path="$deploy_dir/$IMG_FILE_ARTIFACT_NAME" -vmdk_path="$deploy_dir/$vmdk_file" +rauc_boot_files="bootx64.efi mmx64.efi grubx64.efi" +rauc_bootloader_update="bootloader.vfat" +rauc_bootloader_update_dir="bootloader" +rauc_conf="system.conf" +rauc_enapter_cert_file="Enapter.cer" +rauc_kernel_files="initrd bzImage" +rauc_kernel_update="kernel.tar.gz" +rauc_kernel_update_dir="kernel" +rauc_rootfs_update="rootfs.ext4" +rauc_update_dir="/home/build/rauc-update" +update_dir="/home/build/update" +# Cleanup from previous run rm -rf "$update_dir" -rm -f "enapter-industrial-linux-*" +rm -rf "$rauc_update_dir" +rm -vf "$images_dir/enapter-industrial-linux-"* + +# dummy config for rauc extract to work +# we specifying all required settings and +# check-purpose=any is only important for us +cat << EOF > "$deploy_dir/$rauc_conf" +[system] +compatible=Enapter Linux +bootloader=grub +bundle-formats=-plain + +[keyring] +check-purpose=any +EOF + +# Copying industrial Linux img file with another name +# to make this script easier to test +cp "$deploy_dir/$wic_file" "$img_path" + +# Temporary dirs for industrial Linux update +mkdir -p "$rauc_update_dir" +mkdir -p "$rauc_update_dir/$rauc_bootloader_update_dir/EFI/BOOT" +mkdir -p "$rauc_update_dir/$rauc_kernel_update_dir" + +# Copy build rootfs ext4 image to update folder +cp "$deploy_dir/$root_ext4_file" "$rauc_update_dir/$rauc_rootfs_update" + +# Copy grub stuff into bootloader image temp folder +for f in $rauc_boot_files; do + wic cp "$img_path:1/$efi_boot_dir/$f" "$rauc_update_dir/$rauc_bootloader_update_dir/EFI/BOOT" +done + +# Enapter.cer file will be part of bootloader image +wic cp "$img_path:1/$rauc_enapter_cert_file" "$rauc_update_dir/$rauc_bootloader_update_dir/$rauc_enapter_cert_file" + +# Copying bzImage and initrd to kernel update image temp folder +for f in $rauc_kernel_files; do + wic cp "$img_path:1/$efi_enapter_dir/$f" "$rauc_update_dir/$rauc_kernel_update_dir" +done + +# We are using special grub config for system when it is installed on disk +wic cp "$img_path:1/$efi_boot_dir/grub.cfg.install" "$rauc_update_dir/$rauc_bootloader_update_dir/EFI/BOOT/grub.cfg" + +# Creating vfat image for bootloader +mkfs.vfat -n "enp-boot" -C "$rauc_update_dir/$rauc_bootloader_update" 16384 # 16Mb +mcopy -i "$rauc_update_dir/$rauc_bootloader_update" -s "$rauc_update_dir/$rauc_bootloader_update_dir"/* :: +# Creating kernel update image +tar -czf "$rauc_update_dir/$rauc_kernel_update" -C "$rauc_update_dir/$rauc_kernel_update_dir" --strip-components 1 . + +cat << EOF > "$rauc_update_dir/$rauc_manifest" +[update] +compatible=Enapter Linux +version=$DISTRO_VERSION + +[bundle] +format=verity + +[image.rootfs.$DISTRO] +filename=$rauc_rootfs_update + +[image.bootloader.$DISTRO] +filename=$rauc_bootloader_update + +[image.kernel.$DISTRO] +filename=$rauc_kernel_update +EOF + +echo "Rauc Manifest:" +cat "$rauc_update_dir/$rauc_manifest" + +# Remove temp dir from update bundle folder +rm -rf "${rauc_update_dir:?}/$rauc_kernel_update_dir" +rm -rf "${rauc_update_dir:?}/$rauc_bootloader_update_dir" + +# Creating update bundle in output folder +rauc bundle --keyring="$RAUC_KEYRING" -c "$deploy_dir/$rauc_conf" \ + --cert="$RAUC_CERT" --key="$RAUC_KEY" \ + "$rauc_update_dir/" "$images_dir/$RAUC_UPDATE_ARTIFACT_NAME" + +# Put update bundle as install bundle file inside disk image +wic cp "$images_dir/$RAUC_UPDATE_ARTIFACT_NAME" "$img_path:1/$install_bundle_name" + +# Creating temp dir for building legacy update bundle mkdir -p "$update_dir" -enapter_files="rootfs.img bzImage initrd version.txt" -boot_files="grubx64.efi grub.cfg" +# Extracting system images from .img into temp folder for f in $enapter_files; do - wic cp "$img_path:1/EFI/enapter/$f" "$update_dir/" + wic cp "$img_path:1/$efi_enapter_dir/$f" "$update_dir/" done +# Extracting bootloader files and grub.cfg from .img into temp folder for f in $boot_files; do - wic cp "$img_path:1/EFI/BOOT/$f" "$update_dir/" + wic cp "$img_path:1/$efi_boot_dir/$f" "$update_dir/" done +# Legacy update bundle checksums cd "$update_dir" # shellcheck disable=SC2086 sha256sum $enapter_files $boot_files > SHA256SUMS +# Build legacy update bundle (simple zip archive) into output dir # shellcheck disable=SC2086 -zip -0 "$update_file" $enapter_files $boot_files SHA256SUMS +zip -0 "$images_dir/$UPDATE_ARTIFACT_NAME" $enapter_files $boot_files SHA256SUMS +# Calculate .img file checksum hashsum=$(sha256sum "$img_path" | awk '{ print $1 }') echo "$hashsum $IMG_FILE_ARTIFACT_NAME" > SHA256SUMS +# Create zip file with .img and hashsum zip -rj "/tmp/$IMG_ARTIFACT_NAME" "$img_path" SHA256SUMS -cp "$update_file" "$images_dir/$UPDATE_ARTIFACT_NAME" +# Copy VEX if exists +if [ -e "$cve_dir/$vex_file" ]; then + cp "$cve_dir/$vex_file" "$images_dir/$VEX_ARTIFACT_NAME" +fi + +# Copy SPDX archives if exists +if [ -e "$deploy_dir/$rootfs_spdx" ]; then + mkdir "$deploy_dir/$rootfs_spdx".tmp + cd "$deploy_dir/$rootfs_spdx".tmp + tar -xf "$deploy_dir/$rootfs_spdx" -C "$deploy_dir/$rootfs_spdx".tmp + zip -r "$images_dir/$ROOTFS_SPDX_ARTIFACT_NAME" ./ + rm -rf "$deploy_dir/$rootfs_spdx".tmp +fi + +if [ -e "$deploy_dir/$initramfs_spdx" ]; then + mkdir "$deploy_dir/$initramfs_spdx".tmp + cd "$deploy_dir/$initramfs_spdx".tmp + tar -xf "$deploy_dir/$initramfs_spdx" -C "$deploy_dir/$initramfs_spdx".tmp + zip -r "$images_dir/$INITRAMFS_SPDX_ARTIFACT_NAME" ./ + rm -rf "$deploy_dir/$initramfs_spdx".tmp +fi + +# Place .img and .vmdk images cp "/tmp/$IMG_ARTIFACT_NAME" "$images_dir/$IMG_ARTIFACT_NAME" -cp "$vmdk_path" "$images_dir/$VMDK_ARTIFACT_NAME" +qemu-img convert -O vmdk "$img_path" "$images_dir/$VMDK_ARTIFACT_NAME" diff --git a/docker/bin/prepare-sign-keys.sh b/docker/bin/prepare-sign-keys.sh index 6a9e30e..9a04229 100755 --- a/docker/bin/prepare-sign-keys.sh +++ b/docker/bin/prepare-sign-keys.sh @@ -7,3 +7,14 @@ mkdir "$(dirname "$SECURE_BOOT_SIGNING_CERT")" echo "$SECURE_BOOT_SIGNING_CERT_BASE64" | base64 -d > "$SECURE_BOOT_SIGNING_CERT" echo "$SECURE_BOOT_SIGNING_KEY_BASE64" | base64 -d > "$SECURE_BOOT_SIGNING_KEY" echo "$SECURE_BOOT_SIGNING_CERT_DER_BASE64" | base64 -d > "$SECURE_BOOT_SIGNING_CERT_DER" + +mkdir "$(dirname "$MODSIGN_SIGNING_CERT")" + +echo "$MODSIGN_SIGNING_CERT_BASE64" | base64 -d > "$MODSIGN_SIGNING_CERT" +echo "$MODSIGN_SIGNING_KEY_BASE64" | base64 -d > "$MODSIGN_SIGNING_KEY" + +mkdir "$(dirname "$RAUC_CERT")" + +echo "$RAUC_CERT_BASE64" | base64 -d > "$RAUC_CERT" +echo "$RAUC_KEY_BASE64" | base64 -d > "$RAUC_KEY" +echo "$RAUC_KEYRING_BASE64" | base64 -d > "$RAUC_KEYRING" diff --git a/docker/bin/save-sstate.sh b/docker/bin/save-sstate.sh index 7475f34..b8d22c4 100755 --- a/docker/bin/save-sstate.sh +++ b/docker/bin/save-sstate.sh @@ -5,7 +5,7 @@ set -ex if [[ -z "${SKIP_SSTATE_SAVE}" ]]; then - /home/build/enapter-linux-build/repositories/poky/scripts/sstate-cache-management.py --stamps-dir=/home/build/tmp-glibc/stamps -y --cache-dir=/home/build/sstate-cache + /home/build/enapter-linux-build/poky/scripts/sstate-cache-management.py --stamps-dir=/home/build/tmp-glibc/stamps -y --cache-dir=/home/build/sstate-cache sudo rsync -za --delete-after /home/build/sstate-cache/ /home/build/sstate-cache-source else echo "Skipping save-sstate..." diff --git a/docker/bin/sync-sources.sh b/docker/bin/sync-sources.sh index 3dca41e..5b141b4 100755 --- a/docker/bin/sync-sources.sh +++ b/docker/bin/sync-sources.sh @@ -5,7 +5,9 @@ set -ex if [[ -z "${SKIP_SOURCES_SYNC}" ]]; then - rsync --exclude='.git/' --delete-after -za /home/build/enapter-linux-build-source/ /home/build/enapter-linux-build + rsync --delete-after -za /home/build/enapter-linux-build-source/ /home/build/enapter-linux-build + cd /home/build/enapter-linux-build + KAS_CLONE_DEPTH=1 kas checkout --force-checkout configs/enapter-industrial-linux.yml else echo "Skipping sync-sources..." fi diff --git a/machine/conf/bblayers.conf b/machine/conf/bblayers.conf deleted file mode 100644 index cb09ea6..0000000 --- a/machine/conf/bblayers.conf +++ /dev/null @@ -1,22 +0,0 @@ -POKY_BBLAYERS_CONF_VERSION = "2" - -BBPATH = "${TOPDIR}" -BSPDIR := "${@os.path.abspath(os.path.dirname(d.getVar('FILE', True)) + '/../..')}" -BBFILES ?= "" - -BBLAYERS ?= " \ - ${BSPDIR}/meta-enapter-linux \ - ${BSPDIR}/repositories/meta-enapter/meta-enapter-core \ - ${BSPDIR}/repositories/meta-intel \ - ${BSPDIR}/repositories/meta-openembedded/meta-filesystems \ - ${BSPDIR}/repositories/meta-openembedded/meta-initramfs \ - ${BSPDIR}/repositories/meta-openembedded/meta-networking \ - ${BSPDIR}/repositories/meta-openembedded/meta-oe \ - ${BSPDIR}/repositories/meta-openembedded/meta-perl \ - ${BSPDIR}/repositories/meta-openembedded/meta-python \ - ${BSPDIR}/repositories/meta-openembedded/meta-webserver \ - ${BSPDIR}/repositories/meta-virtualization \ - ${BSPDIR}/repositories/poky/meta \ - ${BSPDIR}/repositories/poky/meta-poky \ - ${BSPDIR}/repositories/poky/meta-yocto-bsp \ - " diff --git a/machine/conf/local.conf b/machine/conf/local.conf deleted file mode 100644 index f4fb438..0000000 --- a/machine/conf/local.conf +++ /dev/null @@ -1,8 +0,0 @@ -MACHINE ?= "intel-corei7-64" -DISTRO ?= "enapter-industrial-linux" - -IMAGE_FSTYPES = "wic wic.vmdk" - -SKIP_META_VIRT_SANITY_CHECK = "1" - -INHERIT += "rm_work" diff --git a/machine/conf/distro/enapter-industrial-linux.conf b/meta-enapter-linux/conf/distro/enapter-industrial-linux.conf similarity index 95% rename from machine/conf/distro/enapter-industrial-linux.conf rename to meta-enapter-linux/conf/distro/enapter-industrial-linux.conf index c761666..aa55b3e 100644 --- a/machine/conf/distro/enapter-industrial-linux.conf +++ b/meta-enapter-linux/conf/distro/enapter-industrial-linux.conf @@ -1,4 +1,3 @@ -DISTRO = "enapter-industrial-linux" DISTRO_NAME = "Enapter Industrial Linux" DISTRO_CODENAME = "genapter" PACKAGE_CLASSES = "package_rpm" @@ -32,17 +31,21 @@ IMAGE_FS_TOOLS = "\ squashfs-tools \ util-linux \ " +IMAGE_INITRD_KERNEL_DEPS = "\ + lshw \ + pciutils \ + usbutils \ +" IMAGE_KERNEL_DEPS = "\ + ${IMAGE_INITRD_KERNEL_DEPS} \ + iucode-tool \ kernel-modules \ linux-firmware-i915 \ linux-firmware-ibt \ linux-firmware-iwlwifi \ - lshw \ module-init-tools \ packagegroup-base-wifi \ - pciutils \ - usbutils \ " IMAGE_ROOTFS_INSTALL_BASE_UTILS = "\ @@ -130,13 +133,12 @@ IMAGE_ROOTFS_INSTALL_ENAPTER_OSS_SOFTWARE = "\ enapter-podman-prepare \ enapter-scripts \ enapter-setup-machine-id \ - enapter-update-manager \ - enapter-update-manager-timer \ enapter-user \ enapter-wifi-watchdog \ enapter-wifi-watchdog-timer \ monit \ nginx \ + rauc \ " IMAGE_ROOTFS_INSTALL_EXTENDED_UTILS = "\ @@ -221,9 +223,11 @@ hostname_pn-base-files = "" PACKAGECONFIG:remove:pn-systemd = "timesyncd networkd resolved nss-resolve userdb" PACKAGECONFIG:append:pn-systemd = " pcre2 coredump elfutils" +PACKAGECONFIG:append:pn-rauc = " gpt service network streaming" + SYSTEMD_AUTO_ENABLE:sshd = "disable" -DISTRO_FEATURES = "acl ipv4 ipv6 largefile pam usbhost wifi xattr pci vfat seccomp modsign" +DISTRO_FEATURES = "acl ipv4 ipv6 largefile pam usbhost wifi xattr pci vfat seccomp modsign rauc" MACHINE_FEATURES:remove = " \ alsa \ @@ -234,3 +238,5 @@ MACHINE_FEATURES:remove = " \ touchscreen \ usbgadget \ " + +require conf/distro/include/security_flags.inc diff --git a/meta-enapter-linux/recipes-core/images/enapter-industrial-linux-image.bb b/meta-enapter-linux/recipes-core/images/enapter-industrial-linux-image.bb index af74661..57a1d44 100644 --- a/meta-enapter-linux/recipes-core/images/enapter-industrial-linux-image.bb +++ b/meta-enapter-linux/recipes-core/images/enapter-industrial-linux-image.bb @@ -9,20 +9,42 @@ IMAGE_ROOTFS_EXTRA_SPACE = "300000" inherit core-image -do_rootfs[depends] += "enapter-industrial-linux-rootfs:do_image_complete enapter-industrial-linux-initramfs:do_image_complete virtual/kernel:do_deploy sbsigntool-native:do_populate_sysroot" +do_rootfs[depends] += "enapter-industrial-linux-rootfs:do_image_complete enapter-industrial-linux-initramfs:do_image_complete virtual/kernel:do_deploy sbsigntool-native:do_populate_sysroot intel-microcode:do_deploy" copy_files_to_boot () { mkdir -p ${IMAGE_ROOTFS}/boot/EFI/enapter - KERNEL_FILE="${IMAGE_ROOTFS}/boot/EFI/enapter/bzImage" - GRUB_EFI_FILE="${IMAGE_ROOTFS}/boot/EFI/BOOT/grubx64.efi" + GRUB_EFI_SOURCE="${IMAGE_ROOTFS}/boot/EFI/BOOT/grub-efi-bootx64.efi" + INITRD_SOURCE="${DEPLOY_DIR_IMAGE}/enapter-industrial-linux-initramfs-${MACHINE}.cpio" + KERNEL_SOURCE="${DEPLOY_DIR_IMAGE}/bzImage" + MICROCODE_SOURCE="${DEPLOY_DIR_IMAGE}/microcode.cpio" + ROOTFS_SOURCE="${DEPLOY_DIR_IMAGE}/enapter-industrial-linux-rootfs-${MACHINE}.rootfs.squashfs-zst" + + GRUB_EFI_TARGET="${IMAGE_ROOTFS}/boot/EFI/BOOT/grubx64.efi" + INITRD_TARGET="${IMAGE_ROOTFS}/boot/EFI/enapter/initrd" + KERNEL_TARGET="${IMAGE_ROOTFS}/boot/EFI/enapter/bzImage" + MICROCODE_TARGET="${IMAGE_ROOTFS}/boot/EFI/enapter/microcode.cpio" + ROOTFS_TARGET="${IMAGE_ROOTFS}/boot/EFI/enapter/rootfs.img" + + if ! sbverify --cert "${SECURE_BOOT_SIGNING_CERT}" "${GRUB_EFI_SOURCE}"; then + bbfatal "grubx64.efi sbverify failed" + else + bbnote "grubx64.efi signatures: $(sbverify --list --cert "${SECURE_BOOT_SIGNING_CERT}" "${GRUB_EFI_SOURCE}")" + fi + + if ! sbverify --cert "${SECURE_BOOT_SIGNING_CERT}" "${KERNEL_SOURCE}"; then + bbfatal "bzImage sbverify failed" + else + bbnote "bzImage signatures: $(sbverify --list --cert "${SECURE_BOOT_SIGNING_CERT}" "${KERNEL_SOURCE}")" + fi echo "${DISTRO_VERSION}" > ${IMAGE_ROOTFS}/boot/EFI/enapter/version.txt - cp ${DEPLOY_DIR_IMAGE}/enapter-industrial-linux-rootfs-${MACHINE}.rootfs.squashfs-zst ${IMAGE_ROOTFS}/boot/EFI/enapter/rootfs.img - cp ${DEPLOY_DIR_IMAGE}/enapter-industrial-linux-initramfs-${MACHINE}.cpio.gz ${IMAGE_ROOTFS}/boot/EFI/enapter/initrd - cp ${DEPLOY_DIR_IMAGE}/bzImage ${KERNEL_FILE} - mv ${IMAGE_ROOTFS}/boot/EFI/BOOT/grub-efi-bootx64.efi ${GRUB_EFI_FILE} + cat "${MICROCODE_SOURCE}" "${INITRD_SOURCE}" | gzip > "${INITRD_TARGET}" + + cp ${KERNEL_SOURCE} ${KERNEL_TARGET} + cp ${ROOTFS_SOURCE} ${ROOTFS_TARGET} + mv ${GRUB_EFI_SOURCE} ${GRUB_EFI_TARGET} if [ -f ${SECURE_BOOT_SIGNING_CERT_DER} ]; then cp ${SECURE_BOOT_SIGNING_CERT_DER} "${IMAGE_ROOTFS}/boot/Enapter.cer" diff --git a/meta-enapter-linux/recipes-core/images/enapter-industrial-linux-initramfs.bb b/meta-enapter-linux/recipes-core/images/enapter-industrial-linux-initramfs.bb index 500232a..b449c0f 100644 --- a/meta-enapter-linux/recipes-core/images/enapter-industrial-linux-initramfs.bb +++ b/meta-enapter-linux/recipes-core/images/enapter-industrial-linux-initramfs.bb @@ -6,7 +6,7 @@ INITRAMFS_SCRIPTS ?= "\ PACKAGE_INSTALL = "\ ${IMAGE_FS_TOOLS} \ - ${IMAGE_KERNEL_DEPS} \ + ${IMAGE_INITRD_KERNEL_DEPS} \ ${INITRAMFS_SCRIPTS} \ ${VIRTUAL-RUNTIME_base-utils} \ ${VIRTUAL-RUNTIME_dev_manager} \ @@ -14,6 +14,11 @@ PACKAGE_INSTALL = "\ coreutils \ " +BAD_RECOMMENDATIONS += "\ + kernel-modules \ + tzdata \ + " + IMAGE_FEATURES = "" export IMAGE_BASENAME = "enapter-industrial-linux-initramfs" IMAGE_NAME_SUFFIX ?= "" @@ -21,7 +26,7 @@ IMAGE_LINGUAS = "" IMAGE_INSTALL = "" LICENSE = "MIT" -IMAGE_FSTYPES = "${INITRAMFS_FSTYPES}" +IMAGE_FSTYPES = "cpio" inherit core-image diff --git a/meta-enapter-linux/recipes-core/images/enapter-industrial-linux-rootfs.bb b/meta-enapter-linux/recipes-core/images/enapter-industrial-linux-rootfs.bb index 41a85bb..5ef3872 100644 --- a/meta-enapter-linux/recipes-core/images/enapter-industrial-linux-rootfs.bb +++ b/meta-enapter-linux/recipes-core/images/enapter-industrial-linux-rootfs.bb @@ -3,9 +3,11 @@ IMAGE_INSTALL = "\ ${IMAGE_ROOTFS_INSTALL} \ " +IMAGE_OVERHEAD_FACTOR = "1.3" + DEPENDS:remove = "grub-efi" IMAGE_FEATURES = "" -IMAGE_FSTYPES = "squashfs-zst" +IMAGE_FSTYPES = "squashfs-zst ext4" KERNELDEPMODDEPEND = "" LICENSE = "MIT" LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302" diff --git a/meta-enapter-linux/recipes-core/rauc/rauc-conf.bbappend b/meta-enapter-linux/recipes-core/rauc/rauc-conf.bbappend new file mode 100644 index 0000000..62f5499 --- /dev/null +++ b/meta-enapter-linux/recipes-core/rauc/rauc-conf.bbappend @@ -0,0 +1,3 @@ +do_install:prepend() { + cp ${RAUC_KEYRING} ${WORKDIR}/${RAUC_KEYRING_FILE} +}