Inventory BEAM applications in release SBOMs

eeacfd196ea7 · Christopher David · · parent e340d7287380

Inventory BEAM applications in release SBOMs

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified docs/dependencies-and-licenses.md
  • modified ops/staging/generate-sbom.sh

Diff

2 files changed, +62 -14

docs/dependencies-and-licenses.md modified +6 -2

@@ -61,8 +61,12 @@ ops/staging/generate-sbom.sh <image-reference> <evidence-directory>/sbom.cdx.jso

61 61
```
62 62
63 63
The script uses digest-pinned Syft `v1.51.0`, resolves the scanned image digest,
64
writes CycloneDX JSON atomically, and writes a receipt with the source commit and
65
SBOM checksum. Retain both files with the staging evidence.
64
and refuses a dirty worktree or an image whose OCI revision label differs from
65
`HEAD`. It exports that exact image into a disposable directory so Syft can
66
inventory both Debian packages and the OTP applications actually present in the
67
release. It writes CycloneDX JSON atomically, verifies both package families are
68
present, and writes a receipt with the source commit, image revision, and SBOM
69
checksum. Retain both files with the staging evidence.
66 70
67 71
## Vendored asset notices
68 72
ops/staging/generate-sbom.sh modified +56 -12

@@ -12,46 +12,90 @@ readonly image_reference="$1"

12 12
readonly output_path="$2"
13 13
readonly output_directory="$(dirname -- "$output_path")"
14 14
15
if [[ -n "$(git status --porcelain --untracked-files=all)" ]]; then
16
  echo "refusing to generate release evidence from a dirty worktree" >&2
17
  exit 66
18
fi
19
20
readonly source_commit="$(git rev-parse HEAD)"
21
22
readonly image_digest="$(
23
  docker image inspect "$image_reference" \
24
    --format '{{index .RepoDigests 0}}' 2>/dev/null
25
)"
26
27
if [[ -z "$image_digest" || "$image_digest" == "<no value>" ]]; then
28
  echo "image must be present locally with a resolved repository digest: $image_reference" >&2
29
  exit 65
30
fi
31
32
readonly image_revision="$(
33
  docker image inspect "$image_reference" \
34
    --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' 2>/dev/null
35
)"
36
37
if [[ "$image_revision" != "$source_commit" ]]; then
38
  echo "image revision does not match HEAD: image=$image_revision head=$source_commit" >&2
39
  exit 67
40
fi
41
15 42
mkdir -p -- "$output_directory"
16 43
17 44
readonly temporary_sbom="$(mktemp "${output_path}.tmp.XXXXXX")"
18 45
readonly temporary_receipt="$(mktemp "${output_path}.receipt.tmp.XXXXXX")"
46
readonly scan_directory="$(mktemp -d "${TMPDIR:-/tmp}/openagents-sbom.XXXXXX")"
47
container_id=""
19 48
20 49
cleanup() {
50
  if [[ -n "$container_id" ]]; then
51
    docker rm "$container_id" >/dev/null 2>&1 || true
52
  fi
53
21 54
  unlink "$temporary_sbom" 2>/dev/null || true
22 55
  unlink "$temporary_receipt" 2>/dev/null || true
56
  find "$scan_directory" -depth -delete 2>/dev/null || true
23 57
}
24 58
trap cleanup EXIT
25 59
26
readonly image_digest="$(
27
  docker image inspect "$image_reference" \
28
    --format '{{index .RepoDigests 0}}' 2>/dev/null
29
)"
60
readonly rootfs_archive="$scan_directory/rootfs.tar"
61
readonly rootfs_directory="$scan_directory/rootfs"
30 62
31
if [[ -z "$image_digest" || "$image_digest" == "<no value>" ]]; then
32
  echo "image must be present locally with a resolved repository digest: $image_reference" >&2
33
  exit 65
34
fi
63
mkdir "$rootfs_directory"
64
container_id="$(docker create "$image_digest")"
65
docker export "$container_id" --output "$rootfs_archive"
66
docker rm "$container_id" >/dev/null
67
container_id=""
68
69
tar --extract \
70
  --file "$rootfs_archive" \
71
  --directory "$rootfs_directory" \
72
  --no-same-owner
73
unlink "$rootfs_archive"
35 74
36 75
docker run --rm \
37
  --volume /var/run/docker.sock:/var/run/docker.sock \
76
  --volume "$rootfs_directory:/scan:ro" \
38 77
  "$SYFT_IMAGE" \
39
  "docker:$image_digest" \
78
  dir:/scan \
40 79
  --quiet \
80
  --select-catalogers '+erlang-otp-application-cataloger' \
81
  --source-name "$image_digest" \
82
  --source-version "$source_commit" \
41 83
  --output cyclonedx-json >"$temporary_sbom"
42 84
43 85
jq -e '
44 86
  .bomFormat == "CycloneDX" and
45 87
  (.specVersion | type == "string") and
46
  (.components | type == "array" and length > 0)
88
  (.components | type == "array" and length > 0) and
89
  any(.components[]; (.purl // "") | startswith("pkg:deb/")) and
90
  any(.components[]; (.purl // "") | startswith("pkg:otp/"))
47 91
' "$temporary_sbom" >/dev/null
48 92
49
readonly source_commit="$(git rev-parse HEAD)"
50 93
readonly sbom_sha256="$(sha256sum "$temporary_sbom" | cut -d ' ' -f 1)"
51 94
52 95
{
53 96
  echo "source_commit=$source_commit"
54 97
  echo "image_digest=$image_digest"
98
  echo "image_revision=$image_revision"
55 99
  echo "syft_image=$SYFT_IMAGE"
56 100
  echo "format=cyclonedx-json"
57 101
  echo "generated_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"

This page updates live while a promote is in flight · changelog