diff --git a/.github/workflows/sbom-builder.yml b/.github/workflows/sbom-builder.yml index b71618f..a545a7a 100644 --- a/.github/workflows/sbom-builder.yml +++ b/.github/workflows/sbom-builder.yml @@ -48,7 +48,6 @@ jobs: # Source-specific tools SOURCE=$(yq -r '.source.type' "apps/${{ inputs.app }}/config.yaml") case "$SOURCE" in - lockfile) npm install -g @cyclonedx/cdxgen ;; chainguard) curl -sLO "https://github.com/sigstore/cosign/releases/download/v2.2.2/cosign-linux-amd64" sudo install cosign-linux-amd64 /usr/local/bin/cosign @@ -64,25 +63,63 @@ jobs: echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "component_id=$(yq -r '.sbomify.component_id // ""' "$CONFIG")" >> $GITHUB_OUTPUT echo "component_name=$(yq -r '.sbomify.component_name // ""' "$CONFIG")" >> $GITHUB_OUTPUT + echo "source_type=$(yq -r '.source.type // ""' "$CONFIG")" >> $GITHUB_OUTPUT + echo "clone=$(yq -r '.source.clone // "false"' "$CONFIG")" >> $GITHUB_OUTPUT + LOCKFILE_PATH=$(yq -r '.source.lockfile // ""' "$CONFIG") + if [[ -n "$LOCKFILE_PATH" ]]; then + echo "lockfile=$(basename "$LOCKFILE_PATH")" >> $GITHUB_OUTPUT + # For cloned repos, lockfile is inside the repo/ directory + CLONE=$(yq -r '.source.clone // "false"' "$CONFIG") + if [[ "$CLONE" == "true" ]]; then + echo "lockfile_path=repo/$LOCKFILE_PATH" >> $GITHUB_OUTPUT + else + echo "lockfile_path=$(basename "$LOCKFILE_PATH")" >> $GITHUB_OUTPUT + fi + else + echo "lockfile=" >> $GITHUB_OUTPUT + echo "lockfile_path=" >> $GITHUB_OUTPUT + fi PRODUCT_ID=$(yq -r '.sbomify.product_id // ""' "$CONFIG") if [[ -n "$PRODUCT_ID" && -n "$VERSION" ]]; then echo "product_release=[\"${PRODUCT_ID}:${VERSION}\"]" >> $GITHUB_OUTPUT fi - - name: Fetch SBOM - run: | - ./scripts/fetch-sbom.sh "${{ inputs.app }}" - ls -la sbom.json + - name: Cache Maven dependencies + if: steps.config.outputs.source_type == 'lockfile' && steps.config.outputs.clone == 'true' + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: maven-${{ inputs.app }}-${{ steps.config.outputs.version }} + restore-keys: | + maven-${{ inputs.app }}- + maven- + + - name: Fetch SBOM or lockfile + run: ./scripts/fetch-sbom.sh "${{ inputs.app }}" - name: Upload input artifact - if: always() + if: always() && steps.config.outputs.source_type != 'lockfile' uses: actions/upload-artifact@v4 with: name: sbom-${{ inputs.app }}-${{ steps.config.outputs.version }} path: sbom.json - - name: Upload to sbomify - if: steps.config.outputs.component_id != '' + - name: Upload lockfile artifact + if: always() && steps.config.outputs.source_type == 'lockfile' && steps.config.outputs.clone != 'true' + uses: actions/upload-artifact@v4 + with: + name: lockfile-${{ inputs.app }}-${{ steps.config.outputs.version }} + path: ${{ steps.config.outputs.lockfile }} + + - name: Upload cloned repo artifact + if: always() && steps.config.outputs.source_type == 'lockfile' && steps.config.outputs.clone == 'true' + uses: actions/upload-artifact@v4 + with: + name: repo-${{ inputs.app }}-${{ steps.config.outputs.version }} + path: repo/ + + - name: Upload to sbomify (SBOM) + if: steps.config.outputs.component_id != '' && steps.config.outputs.source_type != 'lockfile' uses: sbomify/github-action@master env: TOKEN: ${{ secrets.SBOMIFY_TOKEN }} @@ -96,6 +133,21 @@ jobs: UPLOAD: ${{ !inputs.dry_run }} PRODUCT_RELEASE: ${{ steps.config.outputs.product_release }} + - name: Upload to sbomify (lockfile) + if: steps.config.outputs.component_id != '' && steps.config.outputs.source_type == 'lockfile' + uses: sbomify/github-action@master + env: + TOKEN: ${{ secrets.SBOMIFY_TOKEN }} + COMPONENT_ID: ${{ steps.config.outputs.component_id }} + COMPONENT_NAME: ${{ steps.config.outputs.component_name }} + COMPONENT_VERSION: ${{ steps.config.outputs.version }} + LOCK_FILE: ${{ steps.config.outputs.lockfile_path }} + OUTPUT_FILE: sbom-output.json + AUGMENT: true + ENRICH: true + UPLOAD: ${{ !inputs.dry_run }} + PRODUCT_RELEASE: ${{ steps.config.outputs.product_release }} + - name: Upload output artifact if: always() uses: actions/upload-artifact@v4 diff --git a/.github/workflows/sbom-keycloak-js.yml b/.github/workflows/sbom-keycloak-js.yml new file mode 100644 index 0000000..edfbc19 --- /dev/null +++ b/.github/workflows/sbom-keycloak-js.yml @@ -0,0 +1,37 @@ +# SBOM workflow for Keycloak JS +# +# Triggers when keycloak-js version or config is updated. +# Generates SBOM from pnpm-lock.yaml lockfile. +# +# https://github.com/keycloak/keycloak + +name: "SBOM: keycloak-js" + +on: + push: + branches: + - master + paths: + - 'apps/keycloak-js/config.yaml' + - '.github/workflows/sbom-keycloak-js.yml' + + workflow_dispatch: + inputs: + dry_run: + description: 'Run in dry-run mode (no upload)' + required: false + type: boolean + default: false + +jobs: + build: + uses: ./.github/workflows/sbom-builder.yml + with: + app: keycloak-js + dry_run: ${{ github.event.inputs.dry_run == 'true' }} + secrets: inherit + permissions: + id-token: write + contents: read + attestations: write + diff --git a/.github/workflows/sbom-keycloak.yml b/.github/workflows/sbom-keycloak.yml new file mode 100644 index 0000000..be312d6 --- /dev/null +++ b/.github/workflows/sbom-keycloak.yml @@ -0,0 +1,35 @@ +# Keycloak SBOM workflow +# +# Generates SBOM from the pom.xml lockfile of Keycloak + +name: "SBOM: keycloak" + +on: + push: + branches: + - master + paths: + - 'apps/keycloak/config.yaml' + - '.github/workflows/sbom-keycloak.yml' + + # Allow manual triggering + workflow_dispatch: + inputs: + dry_run: + description: 'Run in dry-run mode (no upload)' + required: false + type: boolean + default: false + +jobs: + build: + uses: ./.github/workflows/sbom-builder.yml + with: + app: keycloak + dry_run: ${{ github.event.inputs.dry_run == 'true' }} + secrets: inherit + permissions: + id-token: write + contents: read + attestations: write + diff --git a/README.md b/README.md index e5560bd..e218352 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This repository manages SBOM extraction from multiple sources: - **Docker OCI Attestations** - Extract SBOMs embedded in Docker images via BuildKit attestations - **Chainguard Images** - Download signed SBOM attestations from Chainguard images via cosign - **GitHub Releases** - Download SBOMs published as release assets -- **Lockfile Generation** - Generate SBOMs from project dependency lockfiles +- **Lockfile Sources** - Download lockfiles for SBOM generation by sbomify Each app has its own folder with version tracking. When you bump the `version` in `config.yaml`, only that app's SBOM is rebuilt and uploaded - not the entire repository. @@ -22,6 +22,8 @@ Each app has its own folder with version tracking. When you bump the `version` i | [Caddy](https://github.com/caddyserver/caddy) | Caddy | GitHub Release | [![SBOM](https://github.com/sbomify/library/actions/workflows/sbom-caddy.yml/badge.svg)](https://github.com/sbomify/library/actions/workflows/sbom-caddy.yml) | [![sbomify](https://sbomify.com/assets/images/logo/badge.svg)](https://library.sbomify.com/product/caddy/) | | [Dependency Track](https://github.com/DependencyTrack/dependency-track) | API Server | GitHub Release | [![SBOM](https://github.com/sbomify/library/actions/workflows/sbom-dependency-track.yml/badge.svg)](https://github.com/sbomify/library/actions/workflows/sbom-dependency-track.yml) | [![sbomify](https://sbomify.com/assets/images/logo/badge.svg)](https://library.sbomify.com/product/dependency-track/) | | [Dependency Track](https://github.com/DependencyTrack/frontend) | Frontend | GitHub Release | [![SBOM](https://github.com/sbomify/library/actions/workflows/sbom-dependency-track-frontend.yml/badge.svg)](https://github.com/sbomify/library/actions/workflows/sbom-dependency-track-frontend.yml) | [![sbomify](https://sbomify.com/assets/images/logo/badge.svg)](https://library.sbomify.com/product/dependency-track/) | +| [Keycloak](https://github.com/keycloak/keycloak) | Backend | Lockfile (pom.xml) | [![SBOM](https://github.com/sbomify/library/actions/workflows/sbom-keycloak.yml/badge.svg)](https://github.com/sbomify/library/actions/workflows/sbom-keycloak.yml) | [![sbomify](https://sbomify.com/assets/images/logo/badge.svg)](https://library.sbomify.com/product/keycloak/) | +| [Keycloak](https://github.com/keycloak/keycloak) | JS | Lockfile (pnpm) | [![SBOM](https://github.com/sbomify/library/actions/workflows/sbom-keycloak-js.yml/badge.svg)](https://github.com/sbomify/library/actions/workflows/sbom-keycloak-js.yml) | [![sbomify](https://sbomify.com/assets/images/logo/badge.svg)](https://library.sbomify.com/product/keycloak/) | | [OSV Scanner](https://github.com/google/osv-scanner) | OSV Scanner | Lockfile | [![SBOM](https://github.com/sbomify/library/actions/workflows/sbom-osv-scanner.yml/badge.svg)](https://github.com/sbomify/library/actions/workflows/sbom-osv-scanner.yml) | [![sbomify](https://sbomify.com/assets/images/logo/badge.svg)](https://library.sbomify.com/product/osv-scanner/) | ## Directory Structure @@ -43,7 +45,7 @@ Each app has its own folder with version tracking. When you bump the `version` i │ └── sources/ │ ├── docker-attestation.sh # Docker extraction │ ├── github-release.sh # GitHub release download -│ └── lockfile-generator.sh # Lockfile-based generation +│ └── lockfile-generator.sh # Lockfile download └── README.md ``` @@ -183,9 +185,9 @@ source: tag_prefix: "v" ``` -#### Lockfile Generation +#### Lockfile Sources -Generate SBOMs from project lockfiles: +Download lockfiles for SBOM generation by the sbomify GitHub Action: ```yaml source: @@ -193,11 +195,21 @@ source: repo: "owner/repo" # GitHub repository (required) lockfile: "package-lock.json" # Path to lockfile (required) tag_prefix: "v" # Tag prefix - generator: "auto" # cdxgen | syft | auto - extra_files: # Additional files to download - - "package.json" + clone: false # Shallow clone repo instead of downloading lockfile ``` +For projects with complex dependency structures (e.g., Maven multi-module projects), set `clone: true` to perform a shallow clone of the entire repository: + +```yaml +source: + type: lockfile + repo: "keycloak/keycloak" + lockfile: "pom.xml" + clone: true # Clone repo for full dependency resolution +``` + +Note: SBOM generation from lockfiles is handled automatically by the sbomify GitHub Action. + ## Local Development ### Prerequisites @@ -214,9 +226,8 @@ For Docker sources: For Chainguard sources: - **cosign** (from sigstore) -For lockfile generation: -- **cdxgen** (`npm install -g @cyclonedx/cdxgen`), or -- **syft** +For lockfile sources: +- No additional tools required (SBOM generation handled by sbomify GitHub Action) ### Running Locally diff --git a/apps/keycloak-js/config.yaml b/apps/keycloak-js/config.yaml new file mode 100644 index 0000000..291b47e --- /dev/null +++ b/apps/keycloak-js/config.yaml @@ -0,0 +1,25 @@ +# Keycloak JS SBOM Configuration +# +# Keycloak is an open source identity and access management solution. +# This config is for the JavaScript/frontend component. +# +# SBOM source: Generated from pnpm-lock.yaml lockfile +# https://github.com/keycloak/keycloak + +name: keycloak-js +version: "26.4.7" + +format: cyclonedx + +source: + type: lockfile + repo: "keycloak/keycloak" + lockfile: "js/pnpm-lock.yaml" + tag_prefix: "" + clone: true + +sbomify: + component_id: "SwIXpcPzBedP" + component_name: "Keycloak JS" + product_id: "DYmPnVgOSdyT" + diff --git a/apps/keycloak/config.yaml b/apps/keycloak/config.yaml new file mode 100644 index 0000000..2b46baf --- /dev/null +++ b/apps/keycloak/config.yaml @@ -0,0 +1,27 @@ +# Keycloak SBOM Configuration +# +# Keycloak is an Open Source Identity and Access Management solution +# for modern Applications and Services. +# +# SBOM source: Lockfile (pom.xml from GitHub) +# https://github.com/keycloak/keycloak + +name: keycloak +version: "26.4.7" + +format: cyclonedx + +source: + type: lockfile + repo: "keycloak/keycloak" + lockfile: "quarkus/runtime/pom.xml" + tag_prefix: "" + clone: true + post_clone_commands: + - "./mvnw clean install -DskipTestsuite -DskipExamples -DskipTests" + +sbomify: + component_id: "GYsnuXarUapb" + component_name: "Keycloak" + product_id: "DYmPnVgOSdyT" + diff --git a/scripts/fetch-sbom.sh b/scripts/fetch-sbom.sh index 7d002d0..a88f4c4 100755 --- a/scripts/fetch-sbom.sh +++ b/scripts/fetch-sbom.sh @@ -2,12 +2,15 @@ # fetch-sbom.sh - Fetch SBOM for an app # # Usage: ./fetch-sbom.sh +# shellcheck source-path=SCRIPTDIR +# shellcheck source=lib/common.sh set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SOURCES_DIR="${SCRIPT_DIR}/sources" +# shellcheck source=lib/common.sh source "${SCRIPT_DIR}/lib/common.sh" main() { @@ -37,7 +40,25 @@ main() { fi "$handler" "$app" - log_info "Done: sbom.json" + + # Log appropriate output based on source type + case "$source_type" in + lockfile) + local clone + clone=$(get_config "$app" ".source.clone" "false") + if [[ "$clone" == "true" ]]; then + log_info "Done: repo/" + else + local lockfile_path lockfile + lockfile_path=$(get_config "$app" ".source.lockfile") + lockfile=$(basename "$lockfile_path") + log_info "Done: $lockfile" + fi + ;; + *) + log_info "Done: sbom.json" + ;; + esac } main "$@" diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh index 24814d3..a9a9e34 100755 --- a/scripts/lib/common.sh +++ b/scripts/lib/common.sh @@ -84,7 +84,7 @@ die() { require_cmd() { local cmd="$1" local install_hint="${2:-}" - + if ! command -v "$cmd" &> /dev/null; then if [[ -n "$install_hint" ]]; then die "Required command '$cmd' not found. $install_hint" @@ -105,15 +105,15 @@ check_required_tools() { validate_app_dir() { local app="$1" local app_dir="${APPS_DIR}/${app}" - + if [[ ! -d "$app_dir" ]]; then die "App directory not found: $app_dir" fi - + if [[ ! -f "${app_dir}/config.yaml" ]]; then die "config.yaml not found in: $app_dir" fi - + log_debug "Validated app directory: $app_dir" } @@ -125,15 +125,15 @@ validate_app_dir() { # Accepts: X.Y.Z, X.Y.Z-prerelease, X.Y.Z+build, X.Y.Z-prerelease+build validate_semver() { local version="$1" - + # Semver regex pattern # Matches: MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD] local semver_regex='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' - + if [[ ! "$version" =~ $semver_regex ]]; then return 1 fi - + return 0 } @@ -141,29 +141,29 @@ validate_semver() { get_latest_version() { local app="$1" local config_file="${APPS_DIR}/${app}/config.yaml" - + if [[ ! -f "$config_file" ]]; then die "Config file not found: $config_file" fi - + # Ensure yq is available before attempting to read the config if ! command -v yq >/dev/null 2>&1; then die "'yq' command not found. It is required to read: $config_file. Please install 'yq' and try again." fi - + # Read version from config.yaml local version version="$(yq -r '.version // ""' "$config_file" | tr -d '[:space:]')" - + if [[ -z "$version" ]]; then die "Version not specified in: $config_file" fi - + # Validate semver format if ! validate_semver "$version"; then die "Invalid version '$version' in $config_file. Must be valid semver (e.g., 1.2.3, 1.2.3-rc1, 1.2.3+build)" fi - + echo "$version" } @@ -223,6 +223,21 @@ get_sbomify_component_id() { get_config "$app" ".sbomify.component_id" } +# Get array values from app config.yaml +# Usage: get_config_array +# Outputs each array element on a separate line +get_config_array() { + local app="$1" + local path="$2" + local config_file="${APPS_DIR}/${app}/config.yaml" + + if [[ ! -f "$config_file" ]]; then + return 0 # No config file, return empty + fi + + yq -r "(${path} // [])[]" "$config_file" 2>/dev/null || true +} + # ============================================================================= # Utility Functions # ============================================================================= @@ -249,10 +264,10 @@ create_temp_dir() { local prefix="${1:-sbom}" local temp_dir temp_dir="$(mktemp -d -t "${prefix}.XXXXXX")" - + # Add to cleanup list _SBOM_TEMP_DIRS+=("$temp_dir") - + echo "$temp_dir" } @@ -274,11 +289,11 @@ run_cmd() { # Validate JSON output validate_json() { local input="$1" - + if ! echo "$input" | jq empty 2>/dev/null; then die "Invalid JSON output" fi - + log_debug "JSON validation passed" } @@ -286,10 +301,10 @@ validate_json() { validate_sbom() { local sbom="$1" local format="$2" - + # Check it's valid JSON first validate_json "$sbom" - + case "$format" in cyclonedx) # Check for bomFormat field @@ -307,7 +322,7 @@ validate_sbom() { log_warn "Unknown SBOM format: $format, skipping validation" ;; esac - + log_debug "SBOM validation passed for format: $format" } @@ -315,7 +330,7 @@ validate_sbom() { print_usage() { local script_name="$1" local description="$2" - + cat >&2 < [options] diff --git a/scripts/sources/chainguard.sh b/scripts/sources/chainguard.sh index cb68e3a..0d30a93 100755 --- a/scripts/sources/chainguard.sh +++ b/scripts/sources/chainguard.sh @@ -1,8 +1,12 @@ #!/usr/bin/env bash # chainguard.sh - Download SBOM from Chainguard image via cosign +# shellcheck source-path=SCRIPTDIR +# shellcheck source=../lib/common.sh + set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../lib/common.sh source "${SCRIPT_DIR}/../lib/common.sh" app="$1" diff --git a/scripts/sources/docker-attestation.sh b/scripts/sources/docker-attestation.sh index fb2bf28..0344171 100755 --- a/scripts/sources/docker-attestation.sh +++ b/scripts/sources/docker-attestation.sh @@ -1,8 +1,12 @@ #!/usr/bin/env bash # docker-attestation.sh - Extract SBOM from Docker image OCI attestation +# shellcheck source-path=SCRIPTDIR +# shellcheck source=../lib/common.sh + set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../lib/common.sh source "${SCRIPT_DIR}/../lib/common.sh" app="$1" diff --git a/scripts/sources/github-release.sh b/scripts/sources/github-release.sh index 280df6f..8467b58 100755 --- a/scripts/sources/github-release.sh +++ b/scripts/sources/github-release.sh @@ -1,8 +1,12 @@ #!/usr/bin/env bash # github-release.sh - Download SBOM from GitHub release +# shellcheck source-path=SCRIPTDIR +# shellcheck source=../lib/common.sh + set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../lib/common.sh source "${SCRIPT_DIR}/../lib/common.sh" app="$1" diff --git a/scripts/sources/lockfile-generator.sh b/scripts/sources/lockfile-generator.sh index 3c053ca..d5e27e1 100755 --- a/scripts/sources/lockfile-generator.sh +++ b/scripts/sources/lockfile-generator.sh @@ -1,8 +1,15 @@ #!/usr/bin/env bash -# lockfile-generator.sh - Generate SBOM from lockfile +# lockfile-generator.sh - Download lockfile or clone repo for SBOM generation +# +# This script downloads lockfiles or clones repos from GitHub. +# SBOM generation is handled by the sbomify GitHub Action. +# shellcheck source-path=SCRIPTDIR +# shellcheck source=../lib/common.sh + set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../lib/common.sh source "${SCRIPT_DIR}/../lib/common.sh" app="$1" @@ -11,36 +18,28 @@ repo=$(get_config "$app" ".source.repo") lockfile=$(get_config "$app" ".source.lockfile") tag_prefix=$(get_config "$app" ".source.tag_prefix" "") tag_suffix=$(get_config "$app" ".source.tag_suffix" "") +clone=$(get_config "$app" ".source.clone" "false") tag="${tag_prefix}${version}${tag_suffix}" -url="https://raw.githubusercontent.com/${repo}/${tag}/${lockfile}" -log_info "Downloading lockfile: $url" - -work_dir=$(mktemp -d) -cleanup() { rm -rf "$work_dir"; } -trap cleanup EXIT - -lockfile_name=$(basename "$lockfile") -curl -fsSL -o "${work_dir}/${lockfile_name}" "$url" - -# Try to get package.json for JS projects -case "$lockfile_name" in - package-lock.json|yarn.lock|pnpm-lock.yaml) - pkg_dir=$(dirname "$lockfile") - [[ "$pkg_dir" == "." ]] && pkg_dir="" - curl -fsSL -o "${work_dir}/package.json" \ - "https://raw.githubusercontent.com/${repo}/${tag}/${pkg_dir}package.json" 2>/dev/null || true - ;; -esac +if [[ "$clone" == "true" ]]; then + # Shallow clone the repository + repo_url="https://github.com/${repo}.git" + log_info "Shallow cloning: $repo_url (tag: $tag)" + git -c advice.detachedHead=false clone --depth 1 --branch "$tag" "$repo_url" repo + log_info "Cloned: repo/" -# Generate with cdxgen or syft -if command -v cdxgen &> /dev/null; then - log_info "Generating with cdxgen..." - (cd "$work_dir" && cdxgen -o sbom.json .) - mv "${work_dir}/sbom.json" sbom.json -elif command -v syft &> /dev/null; then - log_info "Generating with syft..." - syft "dir:${work_dir}" -o cyclonedx-json=sbom.json + # Run post-clone commands if configured + while IFS= read -r cmd; do + if [[ -n "$cmd" ]]; then + log_info "Running post-clone command: $cmd" + (cd repo && bash -c "$cmd") + fi + done < <(get_config_array "$app" ".source.post_clone_commands") else - die "No SBOM generator found. Install cdxgen or syft." + # Download just the lockfile + url="https://raw.githubusercontent.com/${repo}/${tag}/${lockfile}" + log_info "Downloading lockfile: $url" + lockfile_name=$(basename "$lockfile") + curl -fsSL -o "${lockfile_name}" "$url" + log_info "Downloaded: ${lockfile_name}" fi