Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .evergreen/.evg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,20 @@ tasks:
args:
- ".evergreen/static-checks.sh"

- 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

- name: "test-bson-and-crypt-task"
commands:
- func: "run-tests"
Expand Down Expand Up @@ -2231,6 +2245,20 @@ buildvariants:
tasks:
- name: "static-analysis-task"

- name: "verify-sbom"
display_name: "Verify SBOM freshness"
run_on: ubuntu2204-small
# Only trigger when dependency manifests or the SBOM itself change.
paths:
- "gradle/libs.versions.toml"
- "gradle.properties"
- "settings.gradle.kts"
- "build.gradle.kts"
- "**/build.gradle.kts"
- "sbom.json"
tasks:
- name: "verify-sbom-task"

- name: "perf"
display_name: "Performance Tests"
tags: [ "perf-variant" ]
Expand Down
127 changes: 127 additions & 0 deletions .evergreen/generate-sbom.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
set -euo pipefail

# Ephemeral SBOM generator (Gradle/Java) using mise + CycloneDX Gradle plugin.
# Environment overrides:
# MISE_JAVA_VERSION Java (Temurin) major version (default from Gradle sourceCompatibility or 21)
# SBOM_OUT Output filename (default sbom.json)
#
# Usage: bash .evergreen/generate-sbom.sh

## resolve_java_version
# Determines the required Java version by finding the maximum sourceCompatibility in Gradle files.
resolve_java_version() {
local max_version
max_version=$(find . -name "*.gradle.kts" -exec grep -h 'sourceCompatibility = JavaVersion.VERSION_' {} \; | sed 's/.*VERSION_\([0-9]*\).*/\1/' | sort -n | tail -1)
echo "${max_version:-21}"
}

JAVA_VERSION="${MISE_JAVA_VERSION:-$(resolve_java_version)}"
JQ_VERSION="${JQ_VERSION:-latest}" # jq version or 'latest'
OUT_JSON="${SBOM_OUT:-sbom.json}"

log() { printf '\n[sbom] %s\n' "$*"; }

# 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
}

## resolve_toolchain_flags
# Returns space-separated tool@version specs required for SBOM generation.
resolve_toolchain_flags() {
printf 'java@temurin-%s jq@%s' "$JAVA_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 --"
}

## generate_sbom
# Executes Gradle CycloneDX plugin to generate SBOM.
generate_sbom() {
log "Generating SBOM using Gradle CycloneDX plugin"
local exec_prefix
exec_prefix="$(prepare_exec_prefix)"
$exec_prefix ./gradlew cyclonedxBom || {
log "SBOM generation failed"; exit 1; }
log "SBOM generated"
}

## 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
}

## 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"
}

## 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
}

## 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
generate_sbom
format_sbom
ensure_cyclonedx_cli
verify_sbom
}

main "$@"
103 changes: 103 additions & 0 deletions .evergreen/verify-sbom.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/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: gradle/libs.versions.toml gradle.properties settings.gradle.kts **/build.gradle.kts
# SBOM_FILE Path to canonical SBOM file (default sbom.cdx.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.
#
# Behavior:
# 1. Determine diff range (patch vs mainline) by attempting merge-base with origin/main.
# 2. Capture changed files including both committed changes and staged/uncommitted (by combining git diff --name-status for range and working tree).
# 3. Expand manifest patterns via git ls-files.
# 4. If any manifest changed but SBOM_FILE not changed, fail with guidance.
# 5. If SBOM_FILE changed, pass.
# 6. If no manifest changed, pass.
#
# Local usage: run from repo root after making changes; exit codes surface policy status.

MANIFEST_PATTERNS="${MANIFEST_PATTERNS:-build.gradle.kts gradle/libs.versions.toml gradle.properties settings.gradle.kts **/build.gradle.kts}"
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"
40 changes: 40 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
* limitations under the License.
*/
import java.time.Duration
import org.cyclonedx.model.*

plugins {
id("eclipse")
id("idea")
alias(libs.plugins.nexus.publish)
id("org.cyclonedx.bom") version "2.3.1"
}

val nexusUsername: Provider<String> = providers.gradleProperty("nexusUsername")
Expand Down Expand Up @@ -47,3 +49,41 @@ nexusPublishing {
delayBetween.set(Duration.ofSeconds(10))
}
}

tasks.cyclonedxBom {
setGroup("org.mongodb")

// includeConfigs is the list of configuration names to include when generating the BOM (leave empty to include every configuration), regex is supported
setIncludeConfigs(listOf("runtimeClasspath","baseline"))
// skipConfigs is a list of configuration names to exclude when generating the BOM, regex is supported
//setSkipConfigs(listOf("(?i)(.*(compile|test|checkstyle|codenarc|spotbugs|detekt|analysis|zinc|dokka|commonizer|implementation|annotation).*)"))
// skipProjects is a list of project names to exclude when generating the BOM
setSkipProjects(listOf(rootProject.name, "bom"))
// Specified the type of project being built. Defaults to 'library'
setProjectType("library")
// Specified the version of the CycloneDX specification to use. Defaults to '1.5'
setSchemaVersion("1.5")
// Boms destination directory. Defaults to 'build/reports'
setDestination(project.file("./"))
// The file name for the generated BOMs (before the file format suffix). Defaults to 'bom'
setOutputName("sbom")
// The file format generated, can be xml, json or all for generating both. Defaults to 'all'
setOutputFormat("json")

// declaration of the Object from OrganizationalContact
var organizationalContact1 = OrganizationalContact()

// setting the Name[String], Email[String] and Phone[String] of the Object
organizationalContact1.setName("MongoDB, Inc.")

// passing data to the plugin
setOrganizationalEntity { oe ->
oe.name = "MongoDB, Inc."
oe.urls = listOf("www.mongodb.com")
oe.addContact(organizationalContact1)
}

setVCSGit { ref ->
ref.url = "https://github.com/mongodb/mongo-java-driver"
}
}