ops/staging/generate-sbom.sh

58e6347eeb72 · 3 KB

#!/usr/bin/env bash
set -euo pipefail

readonly SYFT_IMAGE="anchore/syft@sha256:678bfa565b60f747aac0f8e964fe5588a24445b8d0a480e91f6efd70020dfbb0"

if [[ $# -ne 2 ]]; then
  echo "usage: $0 <image-reference> <output.cdx.json>" >&2
  exit 64
fi

readonly image_reference="$1"
readonly output_path="$2"
readonly output_directory="$(dirname -- "$output_path")"

if [[ -n "$(git status --porcelain --untracked-files=all)" ]]; then
  echo "refusing to generate release evidence from a dirty worktree" >&2
  exit 66
fi

readonly source_commit="$(git rev-parse HEAD)"

case "$image_reference" in
  *@sha256:????????????????????????????????????????????????????????????????)
    readonly image_digest="$image_reference"
    ;;
  *)
    readonly image_digest="$(
      docker image inspect "$image_reference" \
        --format '{{index .RepoDigests 0}}' 2>/dev/null
    )"
    ;;
esac

if [[ -z "$image_digest" || "$image_digest" == "<no value>" ]]; then
  echo "image must be present locally with a resolved repository digest: $image_reference" >&2
  exit 65
fi

readonly image_revision="$(
  docker image inspect "$image_reference" \
    --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' 2>/dev/null
)"

if [[ "$image_revision" != "$source_commit" ]]; then
  echo "image revision does not match HEAD: image=$image_revision head=$source_commit" >&2
  exit 67
fi

mkdir -p -- "$output_directory"

readonly temporary_sbom="$(mktemp "${output_path}.tmp.XXXXXX")"
readonly temporary_receipt="$(mktemp "${output_path}.receipt.tmp.XXXXXX")"
readonly scan_directory="$(mktemp -d "${TMPDIR:-/tmp}/openagents-sbom.XXXXXX")"
container_id=""

cleanup() {
  if [[ -n "$container_id" ]]; then
    docker rm "$container_id" >/dev/null 2>&1 || true
  fi

  unlink "$temporary_sbom" 2>/dev/null || true
  unlink "$temporary_receipt" 2>/dev/null || true
  find "$scan_directory" -depth -delete 2>/dev/null || true
}
trap cleanup EXIT

readonly rootfs_archive="$scan_directory/rootfs.tar"
readonly rootfs_directory="$scan_directory/rootfs"

mkdir "$rootfs_directory"
container_id="$(docker create "$image_digest")"
docker export "$container_id" --output "$rootfs_archive"
docker rm "$container_id" >/dev/null
container_id=""

tar --extract \
  --file "$rootfs_archive" \
  --directory "$rootfs_directory" \
  --no-same-owner
unlink "$rootfs_archive"

docker run --rm \
  --volume "$rootfs_directory:/scan:ro" \
  "$SYFT_IMAGE" \
  dir:/scan \
  --quiet \
  --select-catalogers '+erlang-otp-application-cataloger' \
  --source-name "$image_digest" \
  --source-version "$source_commit" \
  --output cyclonedx-json >"$temporary_sbom"

jq -e '
  .bomFormat == "CycloneDX" and
  (.specVersion | type == "string") and
  (.components | type == "array" and length > 0) and
  any(.components[]; (.purl // "") | startswith("pkg:deb/")) and
  any(.components[]; (.purl // "") | startswith("pkg:otp/"))
' "$temporary_sbom" >/dev/null

readonly sbom_sha256="$(sha256sum "$temporary_sbom" | cut -d ' ' -f 1)"

{
  echo "source_commit=$source_commit"
  echo "image_digest=$image_digest"
  echo "image_revision=$image_revision"
  echo "syft_image=$SYFT_IMAGE"
  echo "format=cyclonedx-json"
  echo "generated_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
  echo "sbom_sha256=$sbom_sha256"
} >"$temporary_receipt"

mv -- "$temporary_sbom" "$output_path"
mv -- "$temporary_receipt" "${output_path}.receipt"
trap - EXIT

echo "wrote $output_path"
echo "wrote ${output_path}.receipt"