Skip to content

Commit fe0b20e

Browse files
Added SBOM generation and verification scripts for Java
1 parent feea1ad commit fe0b20e

File tree

4 files changed

+296
-0
lines changed

4 files changed

+296
-0
lines changed

.evergreen/.evg.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,19 @@ tasks:
855855
args:
856856
- ".evergreen/static-checks.sh"
857857

858+
- name: "verify-sbom-task"
859+
tags: [ "sbom" ]
860+
commands:
861+
- command: shell.exec
862+
type: "test"
863+
params:
864+
working_dir: "src"
865+
shell: "bash"
866+
script: |
867+
${PREPARE_SHELL}
868+
echo "Verifying SBOM files"
869+
bash .evergreen/verify-sbom.sh
870+
858871
- name: "test-bson-and-crypt-task"
859872
commands:
860873
- func: "run-tests"
@@ -2231,6 +2244,20 @@ buildvariants:
22312244
tasks:
22322245
- name: "static-analysis-task"
22332246

2247+
- name: "verify-sbom"
2248+
display_name: "Verify SBOM freshness"
2249+
run_on: ubuntu2204-small
2250+
# Only trigger when dependency manifests or the SBOM itself change.
2251+
paths:
2252+
- "gradle/libs.versions.toml"
2253+
- "gradle.properties"
2254+
- "settings.gradle.kts"
2255+
- "build.gradle.kts"
2256+
- "**/build.gradle.kts"
2257+
- "sbom.json"
2258+
tasks:
2259+
- name: "verify-sbom-task"
2260+
22342261
- name: "perf"
22352262
display_name: "Performance Tests"
22362263
tags: [ "perf-variant" ]

.evergreen/generate-sbom.sh

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Ephemeral SBOM generator (Gradle/Java) using mise + CycloneDX Gradle plugin.
5+
# Environment overrides:
6+
# MISE_JAVA_VERSION Java (Temurin) major version (default from Gradle sourceCompatibility or 21)
7+
# SBOM_OUT Output filename (default sbom.json)
8+
#
9+
# Usage: bash .evergreen/generate-sbom.sh
10+
11+
## resolve_java_version
12+
# Determines the required Java version by finding the maximum sourceCompatibility in Gradle files.
13+
resolve_java_version() {
14+
local max_version
15+
max_version=$(find . -name "*.gradle.kts" -exec grep -h 'sourceCompatibility = JavaVersion.VERSION_' {} \; | sed 's/.*VERSION_\([0-9]*\).*/\1/' | sort -n | tail -1)
16+
echo "${max_version:-21}"
17+
}
18+
19+
JAVA_VERSION="${MISE_JAVA_VERSION:-$(resolve_java_version)}"
20+
JQ_VERSION="${JQ_VERSION:-latest}" # jq version or 'latest'
21+
OUT_JSON="${SBOM_OUT:-sbom.json}"
22+
23+
log() { printf '\n[sbom] %s\n' "$*"; }
24+
25+
# Ensure mise is available (installed locally in $HOME) and PATH includes shims.
26+
27+
ensure_mise() {
28+
# Installer places binary in ~/.local/bin/mise by default.
29+
if ! command -v mise >/dev/null 2>&1; then
30+
log "Installing mise"
31+
curl -fsSL https://mise.run | bash >/dev/null 2>&1 || { log "mise install script failed"; exit 1; }
32+
fi
33+
# Ensure ~/.local/bin precedes so 'mise' is found even if shims absent.
34+
export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:$HOME/.local/share/mise/bin:$PATH"
35+
if ! command -v mise >/dev/null 2>&1; then
36+
log "mise not found on PATH after install"; ls -al "$HOME/.local/bin" || true; exit 1
37+
fi
38+
}
39+
40+
## resolve_toolchain_flags
41+
# Returns space-separated tool@version specs required for SBOM generation.
42+
resolve_toolchain_flags() {
43+
printf 'java@temurin-%s jq@%s' "$JAVA_VERSION" "$JQ_VERSION"
44+
}
45+
46+
## prepare_exec_prefix
47+
# Builds the mise exec prefix for ephemeral command runs.
48+
prepare_exec_prefix() {
49+
local tools
50+
tools="$(resolve_toolchain_flags)"
51+
echo "mise exec $tools --"
52+
}
53+
54+
## generate_sbom
55+
# Executes Gradle CycloneDX plugin to generate SBOM.
56+
generate_sbom() {
57+
log "Generating SBOM using Gradle CycloneDX plugin"
58+
local exec_prefix
59+
exec_prefix="$(prepare_exec_prefix)"
60+
$exec_prefix ./gradlew cyclonedxBom || {
61+
log "SBOM generation failed"; exit 1; }
62+
log "SBOM generated"
63+
}
64+
65+
## install_toolchains
66+
# Installs required runtime versions into the local mise cache unconditionally.
67+
# (mise skips download if already present.)
68+
install_toolchains() {
69+
local tools
70+
tools="$(resolve_toolchain_flags)"
71+
log "Installing toolchains: $tools"
72+
mise install $tools >/dev/null
73+
}
74+
75+
## format_sbom
76+
# Formats the SBOM JSON with jq (required). Exits non-zero if formatting fails.
77+
format_sbom() {
78+
log "Formatting SBOM via jq@$JQ_VERSION"
79+
if ! mise exec jq@"$JQ_VERSION" -- jq . "$OUT_JSON" > "$OUT_JSON.tmp" 2>/dev/null; then
80+
log "jq formatting failed"; return 1
81+
fi
82+
mv "$OUT_JSON.tmp" "$OUT_JSON"
83+
}
84+
85+
## ensure_cyclonedx_cli
86+
# Downloads CycloneDX CLI binary if not available.
87+
ensure_cyclonedx_cli() {
88+
if [ ! -f /tmp/cyclonedx ]; then
89+
log "Downloading CycloneDX CLI"
90+
local arch
91+
arch="$(uname -m)"
92+
case "$arch" in
93+
x86_64) arch="x64" ;;
94+
aarch64) arch="arm64" ;;
95+
*) log "Unsupported architecture for CycloneDX CLI: $arch"; exit 1 ;;
96+
esac
97+
local url="https://github.com/CycloneDX/cyclonedx-cli/releases/latest/download/cyclonedx-linux-${arch}"
98+
curl -L -s -o /tmp/cyclonedx "$url" || { log "Failed to download CycloneDX CLI"; exit 1; }
99+
chmod +x /tmp/cyclonedx || { log "Failed to make CycloneDX CLI executable"; exit 1; }
100+
fi
101+
}
102+
103+
## verify_sbom
104+
# Verifies the SBOM is valid CycloneDX format using CycloneDX CLI.
105+
verify_sbom() {
106+
log "Verifying SBOM validity with CycloneDX CLI"
107+
local size
108+
size=$(stat -c%s "$OUT_JSON" 2>/dev/null || echo 0)
109+
if [ "$size" -lt 1000 ]; then
110+
log "SBOM file too small (<1000 bytes)"; exit 1
111+
fi
112+
if ! /tmp/cyclonedx validate --input-file "$OUT_JSON" --fail-on-errors >/dev/null 2>&1; then
113+
log "SBOM validation failed"; exit 1
114+
fi
115+
log "SBOM verified successfully"
116+
}
117+
118+
main() {
119+
ensure_mise
120+
install_toolchains
121+
generate_sbom
122+
format_sbom
123+
ensure_cyclonedx_cli
124+
verify_sbom
125+
}
126+
127+
main "$@"

.evergreen/verify-sbom.sh

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
# verify-sbom.sh: Enforces policy that when dependency manifest files change, the SBOM file must also change.
4+
# Generic so patterns can be repurposed for other artifact freshness checks.
5+
#
6+
# Configurable variables (override via env):
7+
# MANIFEST_PATTERNS Space-delimited gitignore-style patterns to monitor.
8+
# Default: gradle/libs.versions.toml gradle.properties settings.gradle.kts **/build.gradle.kts
9+
# SBOM_FILE Path to canonical SBOM file (default sbom.cdx.json)
10+
# EXIT_CODE_MISSING Exit code when manifests changed but SBOM did not (default 10)
11+
# SKIP_SBOM_VERIFY If set to 1, skip verification (default unset)
12+
# DIFF_BASE Explicit git ref/commit to diff against (optional). If unset, merge-base with origin/main or HEAD~1 fallback.
13+
# VERBOSE If set to 1, prints expanded manifest list and full diff file list.
14+
#
15+
# Behavior:
16+
# 1. Determine diff range (patch vs mainline) by attempting merge-base with origin/main.
17+
# 2. Capture changed files including both committed changes and staged/uncommitted (by combining git diff --name-status for range and working tree).
18+
# 3. Expand manifest patterns via git ls-files.
19+
# 4. If any manifest changed but SBOM_FILE not changed, fail with guidance.
20+
# 5. If SBOM_FILE changed, pass.
21+
# 6. If no manifest changed, pass.
22+
#
23+
# Local usage: run from repo root after making changes; exit codes surface policy status.
24+
25+
MANIFEST_PATTERNS="${MANIFEST_PATTERNS:-build.gradle.kts gradle/libs.versions.toml gradle.properties settings.gradle.kts **/build.gradle.kts}"
26+
SBOM_FILE="${SBOM_FILE:-sbom.json}"
27+
EXIT_CODE_MISSING="${EXIT_CODE_MISSING:-10}"
28+
DIFF_BASE="${DIFF_BASE:-}" # optional user-provided base ref
29+
VERBOSE="${VERBOSE:-0}"
30+
31+
log() { printf '\n[verify-sbom] %s\n' "$*"; }
32+
33+
if [[ "${SKIP_SBOM_VERIFY:-}" == "1" ]]; then
34+
log "Skipping verification (SKIP_SBOM_VERIFY=1)"; exit 0; fi
35+
36+
# Determine base for diff
37+
if [[ -n "$DIFF_BASE" ]]; then
38+
base_ref="$DIFF_BASE"
39+
else
40+
git fetch origin main >/dev/null 2>&1 || true
41+
base_ref="$(git merge-base HEAD origin/main 2>/dev/null || echo '')"
42+
if [[ -z "$base_ref" ]]; then
43+
# Fallback to previous commit
44+
base_ref="HEAD~1"
45+
fi
46+
fi
47+
48+
range="$base_ref..HEAD"
49+
log "Using diff range: $range"
50+
51+
# Committed diff
52+
committed_status="$(git diff --name-status $range || true)"
53+
# Working tree (staged + unstaged) diff vs HEAD
54+
wt_status="$(git diff --name-status HEAD || true)"
55+
# Combine and extract filenames (second column); preserve uniqueness
56+
changed_files=$(printf "%s\n%s\n" "$committed_status" "$wt_status" | awk '{print $2}' | grep -v '^$' | sort -u)
57+
58+
if [[ $VERBOSE == 1 ]]; then
59+
log "Changed files:"; echo "$changed_files"
60+
fi
61+
62+
# Expand manifest patterns to tracked files
63+
expanded_manifests=""
64+
for pattern in $MANIFEST_PATTERNS; do
65+
# git ls-files supports pathspec; '**' requires extglob-like; rely on grep fallback
66+
matches=$(git ls-files "$pattern" 2>/dev/null || true)
67+
if [[ -z "$matches" && "$pattern" == *"**"* ]]; then
68+
# Manual glob expansion for recursive pattern
69+
matches=$(git ls-files | grep -E "$(echo "$pattern" | sed 's/**/.*'/ | sed 's/\./\\./g')" || true)
70+
fi
71+
expanded_manifests+="$matches\n"
72+
done
73+
expanded_manifests=$(echo -e "$expanded_manifests" | grep -v '^$' | sort -u)
74+
75+
if [[ $VERBOSE == 1 ]]; then
76+
log "Expanded manifests:"; echo "$expanded_manifests"
77+
fi
78+
79+
manifest_hit=0
80+
manifest_changed_list=()
81+
while IFS= read -r mf; do
82+
if echo "$changed_files" | grep -Fxq "$mf"; then
83+
manifest_hit=1
84+
manifest_changed_list+=("$mf")
85+
fi
86+
done <<< "$expanded_manifests"
87+
88+
if [[ $manifest_hit -eq 0 ]]; then
89+
log "No manifest changes detected; passing."
90+
exit 0
91+
fi
92+
93+
if echo "$changed_files" | grep -Fxq "$SBOM_FILE"; then
94+
log "SBOM file '$SBOM_FILE' updated alongside manifest changes; pass."
95+
exit 0
96+
fi
97+
98+
log "FAILURE: Manifest files changed but SBOM '$SBOM_FILE' was not modified." >&2
99+
log "Changed manifest(s):" >&2
100+
for mf in "${manifest_changed_list[@]}"; do echo " - $mf" >&2; done
101+
log "Regenerate SBOM locally (e.g., bash .evergreen/generate-sbom.sh) and commit '$SBOM_FILE'." >&2
102+
exit "$EXIT_CODE_MISSING"

build.gradle.kts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
* limitations under the License.
1515
*/
1616
import java.time.Duration
17+
import org.cyclonedx.model.*
1718

1819
plugins {
1920
id("eclipse")
2021
id("idea")
2122
alias(libs.plugins.nexus.publish)
23+
id("org.cyclonedx.bom") version "2.3.1"
2224
}
2325

2426
val nexusUsername: Provider<String> = providers.gradleProperty("nexusUsername")
@@ -47,3 +49,41 @@ nexusPublishing {
4749
delayBetween.set(Duration.ofSeconds(10))
4850
}
4951
}
52+
53+
tasks.cyclonedxBom {
54+
setGroup("org.mongodb")
55+
56+
// includeConfigs is the list of configuration names to include when generating the BOM (leave empty to include every configuration), regex is supported
57+
setIncludeConfigs(listOf("runtimeClasspath","baseline"))
58+
// skipConfigs is a list of configuration names to exclude when generating the BOM, regex is supported
59+
//setSkipConfigs(listOf("(?i)(.*(compile|test|checkstyle|codenarc|spotbugs|detekt|analysis|zinc|dokka|commonizer|implementation|annotation).*)"))
60+
// skipProjects is a list of project names to exclude when generating the BOM
61+
setSkipProjects(listOf(rootProject.name, "bom"))
62+
// Specified the type of project being built. Defaults to 'library'
63+
setProjectType("library")
64+
// Specified the version of the CycloneDX specification to use. Defaults to '1.5'
65+
setSchemaVersion("1.5")
66+
// Boms destination directory. Defaults to 'build/reports'
67+
setDestination(project.file("./"))
68+
// The file name for the generated BOMs (before the file format suffix). Defaults to 'bom'
69+
setOutputName("sbom")
70+
// The file format generated, can be xml, json or all for generating both. Defaults to 'all'
71+
setOutputFormat("json")
72+
73+
// declaration of the Object from OrganizationalContact
74+
var organizationalContact1 = OrganizationalContact()
75+
76+
// setting the Name[String], Email[String] and Phone[String] of the Object
77+
organizationalContact1.setName("MongoDB, Inc.")
78+
79+
// passing data to the plugin
80+
setOrganizationalEntity { oe ->
81+
oe.name = "MongoDB, Inc."
82+
oe.urls = listOf("www.mongodb.com")
83+
oe.addContact(organizationalContact1)
84+
}
85+
86+
setVCSGit { ref ->
87+
ref.url = "https://github.com/mongodb/mongo-java-driver"
88+
}
89+
}

0 commit comments

Comments
 (0)