From 98c82b78a2f63d4a76c27cdd087f74581ac935b7 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Mon, 20 Oct 2025 14:14:39 +0000 Subject: [PATCH 1/5] Add script to generate and enrich CycloneDX SBOM for Rust driver --- .evergreen/generate-sbom.sh | 85 +++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100755 .evergreen/generate-sbom.sh diff --git a/.evergreen/generate-sbom.sh b/.evergreen/generate-sbom.sh new file mode 100755 index 000000000..1153bc0a2 --- /dev/null +++ b/.evergreen/generate-sbom.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -euo pipefail + +# generate-sbom.sh +# Purpose: Produce a CycloneDX SBOM for the Rust driver via cdxgen and enrich it with Parlay. +# Usage: bash .evergreen/generate-sbom.sh [--quiet] + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +QUIET=0 +for arg in "$@"; do + case "$arg" in + --quiet) QUIET=1 ;; + esac +done + +log() { [ "$QUIET" -eq 1 ] || echo "$*" >&2; } +err() { echo "ERROR: $*" >&2; } + +require_cmd() { + local c="$1"; shift || true + if ! command -v "$c" >/dev/null 2>&1; then + err "Missing required command: $c. $*"; exit 1; + fi +} + +log "Checking prerequisites" +require_cmd node "Install Node.js >= 20 (https://nodejs.org/)" +require_cmd npm "Install Node.js/npm" +require_cmd wget "Install wget (e.g., sudo apt-get install -y wget)" +require_cmd jq "Install jq (e.g., sudo apt-get install -y jq)" +if ! npx --no-install cdxgen --version >/dev/null 2>&1; then + log "Installing @cyclonedx/cdxgen" + npm install @cyclonedx/cdxgen >/dev/null 2>&1 || { err "Failed to install cdxgen"; exit 1; } +fi + +# Install parlay if missing +if ! command -v parlay >/dev/null 2>&1; then + log "Installing Parlay" + ARCH="$(uname -m)"; OS="$(uname -s)" + # Map architecture names + case "$ARCH" in + x86_64|amd64) ARCH_DL="x86_64" ;; + aarch64|arm64) ARCH_DL="aarch64" ;; + *) err "Unsupported architecture for parlay: $ARCH"; exit 1;; + esac + if [ "$OS" != "Linux" ]; then + err "Parlay install script currently supports Linux only"; exit 1; + fi + PARLAY_TAR="parlay_Linux_${ARCH_DL}.tar.gz" + wget -q "https://github.com/snyk/parlay/releases/latest/download/${PARLAY_TAR}" || { err "Failed to download Parlay"; exit 1; } + tar -xzf "$PARLAY_TAR" + chmod +x parlay || true + mv parlay "$SCRIPT_DIR/parlay-bin" || true + export PATH="$SCRIPT_DIR:$PATH" +fi + +# Extract crate metadata from Cargo.toml +CRATE_NAME="$(grep -E '^name\s*=\s*"' Cargo.toml | head -1 | sed -E 's/name\s*=\s*"([^"]+)"/\1/')" +CRATE_VERSION="$(grep -E '^version\s*=\s*"' Cargo.toml | head -1 | sed -E 's/version\s*=\s*"([^"]+)"/\1/')" +CRATE_LICENSE="$(grep -E '^license\s*=\s*"' Cargo.toml | head -1 | sed -E 's/license\s*=\s*"([^"]+)"/\1/')" +if [ -z "$CRATE_NAME" ] || [ -z "$CRATE_VERSION" ]; then + err "Failed to parse crate name/version from Cargo.toml"; exit 1; +fi +log "Crate: $CRATE_NAME v$CRATE_VERSION (license: $CRATE_LICENSE)" + +SBOM_FILE="sbom.${CRATE_NAME}@v${CRATE_VERSION}.cdxgen.json" +ENRICHED_SBOM_FILE="sbom.${CRATE_NAME}@v${CRATE_VERSION}.cdxgen.parlay.json" + +log "Generating SBOM: $SBOM_FILE" +npx cdxgen --type cargo --output "$SBOM_FILE" >/dev/null || { err "cdxgen failed"; exit 1; } +mv "$SBOM_FILE" "$SBOM_FILE.raw" +jq . "$SBOM_FILE.raw" > "$SBOM_FILE" || { err "Failed to pretty-print SBOM"; exit 1; } +rm -f "$SBOM_FILE.raw" + +grep -q 'CycloneDX' "$SBOM_FILE" || { err "CycloneDX marker missing in $SBOM_FILE"; exit 1; } +test $(stat -c%s "$SBOM_FILE") -gt 1000 || { err "SBOM file too small (<1000 bytes)"; exit 1; } + +log "Enriching SBOM: $ENRICHED_SBOM_FILE" +parlay ecosystems enrich "$SBOM_FILE" > "$ENRICHED_SBOM_FILE.raw" || { err "Parlay enrichment failed"; exit 1; } +jq . "$ENRICHED_SBOM_FILE.raw" > "$ENRICHED_SBOM_FILE" || { err "Failed to pretty-print enriched SBOM"; exit 1; } +rm -f "$ENRICHED_SBOM_FILE.raw" + +log "Done" +echo "SBOM: $SBOM_FILE" +echo "SBOM (enriched): $ENRICHED_SBOM_FILE" From 06c895ed5e7e82e59216679b8744b306d74a3e6d Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Thu, 6 Nov 2025 16:39:40 +0000 Subject: [PATCH 2/5] Refactor SBOM generation script to use mise and cargo-cyclonedx --- .evergreen/generate-sbom.sh | 147 +++++++++++++++++++----------------- 1 file changed, 78 insertions(+), 69 deletions(-) diff --git a/.evergreen/generate-sbom.sh b/.evergreen/generate-sbom.sh index 1153bc0a2..a88799aa2 100755 --- a/.evergreen/generate-sbom.sh +++ b/.evergreen/generate-sbom.sh @@ -1,85 +1,94 @@ #!/usr/bin/env bash set -euo pipefail -# generate-sbom.sh -# Purpose: Produce a CycloneDX SBOM for the Rust driver via cdxgen and enrich it with Parlay. -# Usage: bash .evergreen/generate-sbom.sh [--quiet] +# Ephemeral SBOM generator (Rust) using mise + cargo-cyclonedx. +# Environment overrides: +# MISE_RUST_VERSION Rust version (default nightly) +# SBOM_OUT Output filename (default sbom.json) +# +# Usage: bash .evergreen/generate-sbom.sh -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -QUIET=0 -for arg in "$@"; do - case "$arg" in - --quiet) QUIET=1 ;; - esac -done +RUST_VERSION="${MISE_RUST_VERSION:-latest}" +JQ_VERSION="${JQ_VERSION:-latest}" +OUT_JSON="${SBOM_OUT:-sbom.json}" -log() { [ "$QUIET" -eq 1 ] || echo "$*" >&2; } -err() { echo "ERROR: $*" >&2; } +log() { printf '\n[sbom] %s\n' "$*"; } -require_cmd() { - local c="$1"; shift || true - if ! command -v "$c" >/dev/null 2>&1; then - err "Missing required command: $c. $*"; exit 1; +# Ensure mise is available (installed locally in $HOME) and PATH includes shims. + +ensure_mise() { + # Installer places binary in ~/.local/bin/mise by default. + if ! command -v mise >/dev/null 2>&1; then + log "Installing mise" + curl -fsSL https://mise.run | bash >/dev/null 2>&1 || { log "mise install script failed"; exit 1; } + fi + # Ensure ~/.local/bin precedes so 'mise' is found even if shims absent. + export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:$HOME/.local/share/mise/bin:$PATH" + if ! command -v mise >/dev/null 2>&1; then + log "mise not found on PATH after install"; ls -al "$HOME/.local/bin" || true; exit 1 fi } -log "Checking prerequisites" -require_cmd node "Install Node.js >= 20 (https://nodejs.org/)" -require_cmd npm "Install Node.js/npm" -require_cmd wget "Install wget (e.g., sudo apt-get install -y wget)" -require_cmd jq "Install jq (e.g., sudo apt-get install -y jq)" -if ! npx --no-install cdxgen --version >/dev/null 2>&1; then - log "Installing @cyclonedx/cdxgen" - npm install @cyclonedx/cdxgen >/dev/null 2>&1 || { err "Failed to install cdxgen"; exit 1; } -fi +## resolve_toolchain_flags +# Returns space-separated tool@version specs required for SBOM generation. +resolve_toolchain_flags() { + printf 'rust@%s jq@%s' "$RUST_VERSION" "$JQ_VERSION" +} + +## prepare_exec_prefix +# Builds the mise exec prefix for ephemeral command runs. +prepare_exec_prefix() { + local tools + tools="$(resolve_toolchain_flags)" + echo "mise exec $tools --" +} -# Install parlay if missing -if ! command -v parlay >/dev/null 2>&1; then - log "Installing Parlay" - ARCH="$(uname -m)"; OS="$(uname -s)" - # Map architecture names - case "$ARCH" in - x86_64|amd64) ARCH_DL="x86_64" ;; - aarch64|arm64) ARCH_DL="aarch64" ;; - *) err "Unsupported architecture for parlay: $ARCH"; exit 1;; - esac - if [ "$OS" != "Linux" ]; then - err "Parlay install script currently supports Linux only"; exit 1; +## ensure_cargo_cyclonedx +# Installs cargo-cyclonedx if not available. +ensure_cargo_cyclonedx() { + if ! mise exec rust@"$RUST_VERSION" -- cargo cyclonedx --version >/dev/null 2>&1; then + log "Installing cargo-cyclonedx" + mise exec rust@"$RUST_VERSION" -- cargo install cargo-cyclonedx || { log "Failed to install cargo-cyclonedx"; exit 1; } fi - PARLAY_TAR="parlay_Linux_${ARCH_DL}.tar.gz" - wget -q "https://github.com/snyk/parlay/releases/latest/download/${PARLAY_TAR}" || { err "Failed to download Parlay"; exit 1; } - tar -xzf "$PARLAY_TAR" - chmod +x parlay || true - mv parlay "$SCRIPT_DIR/parlay-bin" || true - export PATH="$SCRIPT_DIR:$PATH" -fi - -# Extract crate metadata from Cargo.toml -CRATE_NAME="$(grep -E '^name\s*=\s*"' Cargo.toml | head -1 | sed -E 's/name\s*=\s*"([^"]+)"/\1/')" -CRATE_VERSION="$(grep -E '^version\s*=\s*"' Cargo.toml | head -1 | sed -E 's/version\s*=\s*"([^"]+)"/\1/')" -CRATE_LICENSE="$(grep -E '^license\s*=\s*"' Cargo.toml | head -1 | sed -E 's/license\s*=\s*"([^"]+)"/\1/')" -if [ -z "$CRATE_NAME" ] || [ -z "$CRATE_VERSION" ]; then - err "Failed to parse crate name/version from Cargo.toml"; exit 1; -fi -log "Crate: $CRATE_NAME v$CRATE_VERSION (license: $CRATE_LICENSE)" +} -SBOM_FILE="sbom.${CRATE_NAME}@v${CRATE_VERSION}.cdxgen.json" -ENRICHED_SBOM_FILE="sbom.${CRATE_NAME}@v${CRATE_VERSION}.cdxgen.parlay.json" +## generate_sbom +# Executes cargo-cyclonedx to generate SBOM. +generate_sbom() { + log "Generating SBOM using cargo-cyclonedx" + local exec_prefix + exec_prefix="$(prepare_exec_prefix)" + $exec_prefix cargo cyclonedx -vv --format json --override-filename sbom || { + log "SBOM generation failed"; exit 1; } + log "SBOM generated" +} -log "Generating SBOM: $SBOM_FILE" -npx cdxgen --type cargo --output "$SBOM_FILE" >/dev/null || { err "cdxgen failed"; exit 1; } -mv "$SBOM_FILE" "$SBOM_FILE.raw" -jq . "$SBOM_FILE.raw" > "$SBOM_FILE" || { err "Failed to pretty-print SBOM"; exit 1; } -rm -f "$SBOM_FILE.raw" +## install_toolchains +# Installs required runtime versions into the local mise cache unconditionally. +# (mise skips download if already present.) +install_toolchains() { + local tools + tools="$(resolve_toolchain_flags)" + log "Installing toolchains: $tools" + mise install $tools >/dev/null +} -grep -q 'CycloneDX' "$SBOM_FILE" || { err "CycloneDX marker missing in $SBOM_FILE"; exit 1; } -test $(stat -c%s "$SBOM_FILE") -gt 1000 || { err "SBOM file too small (<1000 bytes)"; exit 1; } +## format_sbom +# Formats the SBOM JSON with jq (required). Exits non-zero if formatting fails. +format_sbom() { + log "Formatting SBOM via jq@$JQ_VERSION" + if ! mise exec jq@"$JQ_VERSION" -- jq . "$OUT_JSON" > "$OUT_JSON.tmp" 2>/dev/null; then + log "jq formatting failed"; return 1 + fi + mv "$OUT_JSON.tmp" "$OUT_JSON" +} -log "Enriching SBOM: $ENRICHED_SBOM_FILE" -parlay ecosystems enrich "$SBOM_FILE" > "$ENRICHED_SBOM_FILE.raw" || { err "Parlay enrichment failed"; exit 1; } -jq . "$ENRICHED_SBOM_FILE.raw" > "$ENRICHED_SBOM_FILE" || { err "Failed to pretty-print enriched SBOM"; exit 1; } -rm -f "$ENRICHED_SBOM_FILE.raw" +main() { + ensure_mise + install_toolchains + ensure_cargo_cyclonedx + generate_sbom + format_sbom +} -log "Done" -echo "SBOM: $SBOM_FILE" -echo "SBOM (enriched): $ENRICHED_SBOM_FILE" +main "$@" From c7db204743abf27d903f78b2a66279e5960e6ee5 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Thu, 6 Nov 2025 16:48:26 +0000 Subject: [PATCH 3/5] Add CycloneDX CLI download and verification to SBOM generation script --- .evergreen/generate-sbom.sh | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/.evergreen/generate-sbom.sh b/.evergreen/generate-sbom.sh index a88799aa2..77c63b2e7 100755 --- a/.evergreen/generate-sbom.sh +++ b/.evergreen/generate-sbom.sh @@ -52,6 +52,24 @@ ensure_cargo_cyclonedx() { fi } +## ensure_cyclonedx_cli +# Downloads CycloneDX CLI binary if not available. +ensure_cyclonedx_cli() { + if [ ! -f /tmp/cyclonedx ]; then + log "Downloading CycloneDX CLI" + local arch + arch="$(uname -m)" + case "$arch" in + x86_64) arch="x64" ;; + aarch64) arch="arm64" ;; + *) log "Unsupported architecture for CycloneDX CLI: $arch"; exit 1 ;; + esac + local url="https://github.com/CycloneDX/cyclonedx-cli/releases/latest/download/cyclonedx-linux-${arch}" + curl -L -s -o /tmp/cyclonedx "$url" || { log "Failed to download CycloneDX CLI"; exit 1; } + chmod +x /tmp/cyclonedx || { log "Failed to make CycloneDX CLI executable"; exit 1; } + fi +} + ## generate_sbom # Executes cargo-cyclonedx to generate SBOM. generate_sbom() { @@ -83,12 +101,29 @@ format_sbom() { mv "$OUT_JSON.tmp" "$OUT_JSON" } +## verify_sbom +# Verifies the SBOM is valid CycloneDX format using CycloneDX CLI. +verify_sbom() { + log "Verifying SBOM validity with CycloneDX CLI" + local size + size=$(stat -c%s "$OUT_JSON" 2>/dev/null || echo 0) + if [ "$size" -lt 1000 ]; then + log "SBOM file too small (<1000 bytes)"; exit 1 + fi + if ! /tmp/cyclonedx validate --input-file "$OUT_JSON" --fail-on-errors >/dev/null 2>&1; then + log "SBOM validation failed"; exit 1 + fi + log "SBOM verified successfully" +} + main() { ensure_mise install_toolchains ensure_cargo_cyclonedx + ensure_cyclonedx_cli generate_sbom format_sbom + verify_sbom } main "$@" From f3902c1e7213a82ef52e230226d2522bd457180a Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Tue, 11 Nov 2025 23:09:24 +0000 Subject: [PATCH 4/5] Added verification script to validate if sbom changes are needed --- .evergreen/verify-sbom.sh | 93 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 .evergreen/verify-sbom.sh diff --git a/.evergreen/verify-sbom.sh b/.evergreen/verify-sbom.sh new file mode 100644 index 000000000..fa0443cdf --- /dev/null +++ b/.evergreen/verify-sbom.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -euo pipefail +# verify-sbom.sh: Enforces policy that when dependency manifest files change, the SBOM file must also change. +# Generic so patterns can be repurposed for other artifact freshness checks. +# +# Configurable variables (override via env): +# MANIFEST_PATTERNS Space-delimited gitignore-style patterns to monitor. +# Default: Cargo.toml Cargo.lock **/Cargo.toml +# SBOM_FILE Path to canonical SBOM file (default sbom.json) +# EXIT_CODE_MISSING Exit code when manifests changed but SBOM did not (default 10) +# SKIP_SBOM_VERIFY If set to 1, skip verification (default unset) +# DIFF_BASE Explicit git ref/commit to diff against (optional). If unset, merge-base with origin/main or HEAD~1 fallback. +# VERBOSE If set to 1, prints expanded manifest list and full diff file list. + +MANIFEST_PATTERNS="${MANIFEST_PATTERNS:-*.toml *.lock **/*.toml **/*.lock}" +SBOM_FILE="${SBOM_FILE:-sbom.json}" +EXIT_CODE_MISSING="${EXIT_CODE_MISSING:-10}" +DIFF_BASE="${DIFF_BASE:-}" # optional user-provided base ref +VERBOSE="${VERBOSE:-0}" + +log() { printf '\n[verify-sbom] %s\n' "$*"; } + +if [[ "${SKIP_SBOM_VERIFY:-}" == "1" ]]; then + log "Skipping verification (SKIP_SBOM_VERIFY=1)"; exit 0; fi + +# Determine base for diff +if [[ -n "$DIFF_BASE" ]]; then + base_ref="$DIFF_BASE" +else + git fetch origin main >/dev/null 2>&1 || true + base_ref="$(git merge-base HEAD origin/main 2>/dev/null || echo '')" + if [[ -z "$base_ref" ]]; then + # Fallback to previous commit + base_ref="HEAD~1" + fi +fi + +range="$base_ref..HEAD" +log "Using diff range: $range" + +# Committed diff +committed_status="$(git diff --name-status $range || true)" +# Working tree (staged + unstaged) diff vs HEAD +wt_status="$(git diff --name-status HEAD || true)" +# Combine and extract filenames (second column); preserve uniqueness +changed_files=$(printf "%s\n%s\n" "$committed_status" "$wt_status" | awk '{print $2}' | grep -v '^$' | sort -u) + +if [[ $VERBOSE == 1 ]]; then + log "Changed files:"; echo "$changed_files" +fi + +# Expand manifest patterns to tracked files +expanded_manifests="" +for pattern in $MANIFEST_PATTERNS; do + # git ls-files supports pathspec; '**' requires extglob-like; rely on grep fallback + matches=$(git ls-files "$pattern" 2>/dev/null || true) + if [[ -z "$matches" && "$pattern" == *"**"* ]]; then + # Manual glob expansion for recursive pattern + matches=$(git ls-files | grep -E "$(echo "$pattern" | sed 's/**/.*'/ | sed 's/\./\\./g')" || true) + fi + expanded_manifests+="$matches\n" +done +expanded_manifests=$(echo -e "$expanded_manifests" | grep -v '^$' | sort -u) + +if [[ $VERBOSE == 1 ]]; then + log "Expanded manifests:"; echo "$expanded_manifests" +fi + +manifest_hit=0 +manifest_changed_list=() +while IFS= read -r mf; do + if echo "$changed_files" | grep -Fxq "$mf"; then + manifest_hit=1 + manifest_changed_list+=("$mf") + fi +done <<< "$expanded_manifests" + +if [[ $manifest_hit -eq 0 ]]; then + log "No manifest changes detected; passing." + exit 0 +fi + +if echo "$changed_files" | grep -Fxq "$SBOM_FILE"; then + log "SBOM file '$SBOM_FILE' updated alongside manifest changes; pass." + exit 0 +fi + +log "FAILURE: Manifest files changed but SBOM '$SBOM_FILE' was not modified." >&2 +log "Changed manifest(s):" >&2 +for mf in "${manifest_changed_list[@]}"; do echo " - $mf" >&2; done +log "Regenerate SBOM locally (e.g., bash .evergreen/generate-sbom.sh) and commit '$SBOM_FILE'." >&2 +log "This is a dry run and will always pass." +#exit "$EXIT_CODE_MISSING" From ef56c44f7e10223a39db8b3fde56349a0abe71f4 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Wed, 12 Nov 2025 18:41:52 +0000 Subject: [PATCH 5/5] Add verification task for SBOM changes in Evergreen configuration --- .evergreen/config.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index c7e2e5765..2652cc7d8 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -419,6 +419,19 @@ buildvariants: #- name: .8.0 #- name: .7.0 + - name: "verify-sbom" + display_name: "Verify SBOM Changes Needed" + run_on: ubuntu2204-small + # Only trigger when dependency manifests or the SBOM itself change. + paths: + - "*.toml" + - "*.lock" + - "**/*.toml" + - "**/*.lock" + - "sbom.json" + tasks: + - name: "verify-sbom-task" + ############### # Task Groups # ############### @@ -1246,6 +1259,20 @@ tasks: include_expansions_in_env: - PROJECT_DIRECTORY + - name: "verify-sbom-task" + allowed_requesters: ["patch", "github_pr"] + tags: [ "sbom" ] + commands: + - command: shell.exec + type: "test" + params: + working_dir: "src" + shell: "bash" + script: | + ${PREPARE_SHELL} + echo "Verifying SBOM files" + bash .evergreen/verify-sbom.sh + ############# # Functions # #############