diff --git a/.devcontainer/base/Dockerfile b/.devcontainer/base/Dockerfile index b56a5774..0baf0e5c 100644 --- a/.devcontainer/base/Dockerfile +++ b/.devcontainer/base/Dockerfile @@ -31,7 +31,8 @@ HEALTHCHECK NONE SHELL ["/bin/bash", "-Eeuo", "pipefail", "-c"] # hadolint ignore=DL3008 -RUN --mount=type=bind,source=.devcontainer/base/apt-requirements.json,target=/tmp/apt-requirements.json \ +RUN --mount=type=bind,source=.devcontainer/base/add-sbom-note.sh,target=/tmp/add-sbom-note.sh \ + --mount=type=bind,source=.devcontainer/base/apt-requirements.json,target=/tmp/apt-requirements.json \ --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ --mount=type=cache,target=/var/log,sharing=locked \ @@ -51,6 +52,14 @@ RUN --mount=type=bind,source=.devcontainer/base/apt-requirements.json,target=/tm cp -r /src/bats-support /usr/local/bats-support cp -r /src/bats-assert /usr/local/bats-assert + # Retain the bats-core manifest so the tool is captured in the SBOM + mkdir -p /usr/local/lib/bats-core + cp /src/bats-core/package.json /usr/local/lib/bats-core/package.json + + # Install the SBOM annotation helper so downstream flavors can annotate tools + # that Syft cannot detect on its own (see .devcontainer/base/add-sbom-note.sh) + install -m 0755 /tmp/add-sbom-note.sh /usr/local/bin/add-sbom-note + # Generate locale sed -i '/C.UTF-8/s/^# //' /etc/locale.gen locale-gen diff --git a/.devcontainer/base/add-sbom-note.sh b/.devcontainer/base/add-sbom-note.sh new file mode 100644 index 00000000..602020c3 --- /dev/null +++ b/.devcontainer/base/add-sbom-note.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Annotate ELF binaries with a systemd `.note.package` section so that Syft +# includes them in the generated SBOM (see https://systemd.io/ELF_PACKAGE_METADATA/). +# +# This is used for tools that are installed outside of a package manager (e.g. +# downloaded release binaries) and that Syft cannot otherwise detect. Binaries +# that do not exist are skipped; if none of the given binaries exist the script +# fails so that a renamed or dropped tool becomes a hard build error. +# +# Usage: add-sbom-note.sh ... +set -Eeuo pipefail + +name="${1:?tool name required}" +version="${2:?tool version required}" +purl="${3:?tool purl required}" +shift 3 + +note="$(mktemp)" +trap 'rm -f "${note}"' EXIT +jq -n --arg name "${name}" --arg version "${version}" --arg purl "${purl}" '{name: $name, version: $version, purl: $purl}' > "${note}" + +tagged=0 +for binary in "$@"; do + if [ -x "${binary}" ]; then + objcopy --add-section .note.package="${note}" \ + --set-section-flags .note.package=noload,readonly "${binary}" + tagged=1 + fi +done + +[ "${tagged}" -eq 1 ] || { echo "no binaries found to annotate for ${name}" >&2; exit 1; } diff --git a/.devcontainer/base/apt-requirements.json b/.devcontainer/base/apt-requirements.json index 8b945708..795a9105 100644 --- a/.devcontainer/base/apt-requirements.json +++ b/.devcontainer/base/apt-requirements.json @@ -1,5 +1,6 @@ { "bash-completion": "1:2.16.0-8build1", + "binutils": "2.46-3ubuntu2", "ca-certificates": "20260601~26.04.1", "git": "1:2.53.0-1ubuntu1", "gnupg2": "2.4.8-4ubuntu3", diff --git a/.devcontainer/base/tool-inventory.json b/.devcontainer/base/tool-inventory.json new file mode 100644 index 00000000..3f0f08e4 --- /dev/null +++ b/.devcontainer/base/tool-inventory.json @@ -0,0 +1,8 @@ +[ + "bats", + "bats-assert", + "bats-support", + "git", + "gnupg2", + "wget" +] diff --git a/.devcontainer/cpp/Dockerfile b/.devcontainer/cpp/Dockerfile index c3cc3706..1124e277 100644 --- a/.devcontainer/cpp/Dockerfile +++ b/.devcontainer/cpp/Dockerfile @@ -9,6 +9,7 @@ ARG CCACHE_VERSION=4.13.1 ARG XWIN_VERSION=0.8.0 # Whether or not to build the embedded flavor of this container ARG BUILD_EMBEDDED_FLAVOR=false +ARG ARM_GNU_TOOLCHAIN_VERSION=15.2.rel1 # Downloader stage for AMD64 architecture FROM scratch AS downloader-amd64 @@ -53,6 +54,7 @@ ARG CCACHE_MINISIGN_PUBKEY ARG CCACHE_VERSION ARG XWIN_VERSION ARG BUILD_EMBEDDED_FLAVOR +ARG ARM_GNU_TOOLCHAIN_VERSION SHELL ["/bin/bash", "-Eeuo", "pipefail", "-c"] @@ -65,7 +67,7 @@ RUN --mount=from=downloader,target=/dl \ apt-get update && apt-get install --no-install-recommends -y minisign if [[ "${BUILD_EMBEDDED_FLAVOR}" == "true" ]]; then - ARM_GNU_TOOLCHAIN_URL="https://developer.arm.com/-/media/Files/downloads/gnu/15.2.rel1/binrel/arm-gnu-toolchain-15.2.rel1-$(uname -m)-arm-none-eabi.tar.xz" + ARM_GNU_TOOLCHAIN_URL="https://developer.arm.com/-/media/Files/downloads/gnu/${ARM_GNU_TOOLCHAIN_VERSION}/binrel/arm-gnu-toolchain-${ARM_GNU_TOOLCHAIN_VERSION}-$(uname -m)-arm-none-eabi.tar.xz" ARM_GNU_TOOLCHAIN_TAR="/tmp/arm-gnu-toolchain.tar.xz" if [[ "$(uname -m)" == "x86_64" ]]; then @@ -95,6 +97,9 @@ ARG CLANG_VERSION=22 ARG CPM_VERSION=0.42.1 ARG INCLUDE_WHAT_YOU_USE_VERSION=0.26 ARG BUILD_EMBEDDED_FLAVOR +ARG CCACHE_VERSION +ARG XWIN_VERSION +ARG ARM_GNU_TOOLCHAIN_VERSION ARG DEBIAN_FRONTEND=noninteractive @@ -111,6 +116,7 @@ ENV CCACHE_DIR=/cache/.ccache \ PATH="$PATH:/usr/lib/llvm-${CLANG_VERSION}/bin:/opt/gcc-arm-none-eabi/bin" \ PYTHONPYCACHEPREFIX=/cache/.python +# hadolint ignore=DL3008 RUN --mount=type=bind,source=.devcontainer/cpp/apt-requirements-base.json,target=/tmp/apt-requirements-base.json \ --mount=type=bind,source=.devcontainer/cpp/apt-requirements-clang.json,target=/tmp/apt-requirements-clang.json \ --mount=type=bind,source=.devcontainer/cpp/requirements.txt,target=/tmp/requirements.txt \ @@ -148,40 +154,56 @@ RUN --mount=type=bind,source=.devcontainer/cpp/apt-requirements-base.json,target # Install arm-gcc toolchain cp -a /src/arm-gnu-toolchain-*-arm-none-eabi /opt/gcc-arm-none-eabi fi -EOF -# Install include-what-you-use (iwyu) from source -# hadolint ignore=DL3008 -RUN --mount=type=cache,target=/cache,sharing=locked \ - --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt,sharing=locked \ - apt-get update && apt-get install -y --no-install-recommends libclang-${CLANG_VERSION}-dev llvm-${CLANG_VERSION}-dev \ - && wget --no-hsts -qO - https://github.com/include-what-you-use/include-what-you-use/archive/refs/tags/${INCLUDE_WHAT_YOU_USE_VERSION}.tar.gz | tar xz -C /tmp \ - && CC=clang CXX=clang++ cmake -S /tmp/include-what-you-use-${INCLUDE_WHAT_YOU_USE_VERSION} -B /tmp/include-what-you-use-${INCLUDE_WHAT_YOU_USE_VERSION}/build \ - && cmake --build /tmp/include-what-you-use-${INCLUDE_WHAT_YOU_USE_VERSION}/build --target install \ - && rm -rf /tmp/include-what-you-use-${INCLUDE_WHAT_YOU_USE_VERSION} \ - && apt-get purge -y libclang-${CLANG_VERSION}-dev llvm-${CLANG_VERSION}-dev \ - && apt-get autoremove -y \ - && apt-get clean - -# Update all tool alternatives to the correct version -RUN --mount=type=cache,target=/var/log,sharing=locked \ - update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 10 \ + # Install include-what-you-use (iwyu) from source + apt-get update && apt-get install -y --no-install-recommends libclang-${CLANG_VERSION}-dev llvm-${CLANG_VERSION}-dev + wget --no-hsts -qO - https://github.com/include-what-you-use/include-what-you-use/archive/refs/tags/${INCLUDE_WHAT_YOU_USE_VERSION}.tar.gz | tar xz -C /tmp + CC=clang CXX=clang++ cmake -S /tmp/include-what-you-use-${INCLUDE_WHAT_YOU_USE_VERSION} -B /tmp/include-what-you-use-${INCLUDE_WHAT_YOU_USE_VERSION}/build + cmake --build /tmp/include-what-you-use-${INCLUDE_WHAT_YOU_USE_VERSION}/build --target install + rm -rf /tmp/include-what-you-use-${INCLUDE_WHAT_YOU_USE_VERSION} + apt-get purge -y libclang-${CLANG_VERSION}-dev llvm-${CLANG_VERSION}-dev + apt-get autoremove -y + apt-get clean + + # Update all tool alternatives to the correct version + update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 10 \ --slave /usr/bin/g++ g++ /usr/bin/g++-15 \ - --slave /usr/bin/gcov gcov /usr/bin/gcov-15 \ - && update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-15 10 \ - --slave /usr/bin/c++ c++ /usr/bin/g++-15 \ - && update-alternatives --install /usr/bin/iwyu iwyu /usr/local/bin/include-what-you-use 10 \ - && update-alternatives --install /usr/bin/mull-runner mull-runner /usr/bin/mull-runner-${CLANG_VERSION} 10 \ + --slave /usr/bin/gcov gcov /usr/bin/gcov-15 + update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-15 10 \ + --slave /usr/bin/c++ c++ /usr/bin/g++-15 + update-alternatives --install /usr/bin/iwyu iwyu /usr/local/bin/include-what-you-use 10 + update-alternatives --install /usr/bin/mull-runner mull-runner /usr/bin/mull-runner-${CLANG_VERSION} 10 \ --slave /usr/bin/mull-reporter mull-reporter /usr/bin/mull-reporter-${CLANG_VERSION} \ - --slave /usr/lib/mull-ir-frontend mull-ir-frontend /usr/lib/mull-ir-frontend-${CLANG_VERSION} \ - && update-alternatives --install /usr/bin/python python /usr/bin/python3 10 \ - && mkdir /root/.amp - -# Set up package managers CPM and Conan -# - Install CPM.cmake to the CMake module path -# - Configure a default profile for Conan and set the CMake generator to Ninja -RUN --mount=type=cache,target=/cache,sharing=locked \ - wget --no-hsts -qP /usr/local/lib/python*/dist-packages/cmake/data/share/cmake-*/Modules/ https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_VERSION}/CPM.cmake \ - && conan profile detect \ - && echo -e "\n[conf]\ntools.cmake.cmaketoolchain:generator=Ninja" >> "$(conan profile path default)" + --slave /usr/lib/mull-ir-frontend mull-ir-frontend /usr/lib/mull-ir-frontend-${CLANG_VERSION} + update-alternatives --install /usr/bin/python python /usr/bin/python3 10 + + mkdir /root/.amp + + # Set up package managers CPM and Conan + # - Install CPM.cmake to the CMake module path + # - Configure a default profile for Conan and set the CMake generator to Ninja + # + # NOTE: CPM.cmake is intentionally absent from the generated SBOM. It is a single + # CMake module (not an ELF binary), so it cannot carry a `.note.package` ELF + # section and Syft has no cataloger for standalone `.cmake` files. Its version is + # pinned and tracked via the CPM_VERSION build argument above. + wget --no-hsts -qP /usr/local/lib/python*/dist-packages/cmake/data/share/cmake-*/Modules/ https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_VERSION}/CPM.cmake + conan profile detect + echo -e "\n[conf]\ntools.cmake.cmaketoolchain:generator=Ninja" >> "$(conan profile path default)" + + add-sbom-note ccache "${CCACHE_VERSION}" "pkg:generic/ccache@${CCACHE_VERSION}" \ + /usr/local/bin/ccache + add-sbom-note xwin "${XWIN_VERSION}" "pkg:cargo/xwin@${XWIN_VERSION}" \ + /usr/local/bin/xwin + add-sbom-note include-what-you-use "${INCLUDE_WHAT_YOU_USE_VERSION}" "pkg:generic/include-what-you-use@${INCLUDE_WHAT_YOU_USE_VERSION}" \ + /usr/local/bin/include-what-you-use + + # Annotate tools that Syft cannot detect on its own with a `.note.package` ELF + # section (systemd ELF package metadata convention, see + # https://systemd.io/ELF_PACKAGE_METADATA/) so they are included in the SBOM + # generated for the image. `add-sbom-note` is provided by the base image. + if [[ "${BUILD_EMBEDDED_FLAVOR}" == "true" ]]; then + add-sbom-note arm-gnu-toolchain "${ARM_GNU_TOOLCHAIN_VERSION}" "pkg:generic/arm-gnu-toolchain@${ARM_GNU_TOOLCHAIN_VERSION}" \ + /opt/gcc-arm-none-eabi/bin/arm-none-eabi-{gcc,g++,ld,objcopy,size} + fi +EOF diff --git a/.devcontainer/cpp/tool-inventory.json b/.devcontainer/cpp/tool-inventory.json new file mode 100644 index 00000000..1048c70a --- /dev/null +++ b/.devcontainer/cpp/tool-inventory.json @@ -0,0 +1,19 @@ +[ + "ccache", + "clang-22", + "clang-format-22", + "clang-tidy-22", + "clang-tools-22", + "clangd-22", + "cmake", + "conan", + "g++-15", + "gcovr", + "gdb-multiarch", + "include-what-you-use", + "lld-22", + "llvm-22", + "mull-22", + "ninja-build", + "xwin" +] diff --git a/.devcontainer/docs/tool-inventory.json b/.devcontainer/docs/tool-inventory.json new file mode 100644 index 00000000..e6f98568 --- /dev/null +++ b/.devcontainer/docs/tool-inventory.json @@ -0,0 +1,5 @@ +[ + "graphviz", + "plantuml", + "sbdl" +] diff --git a/.devcontainer/embedded-cpp/tool-inventory.json b/.devcontainer/embedded-cpp/tool-inventory.json new file mode 100644 index 00000000..cc90c4d8 --- /dev/null +++ b/.devcontainer/embedded-cpp/tool-inventory.json @@ -0,0 +1,20 @@ +[ + "arm-gnu-toolchain", + "ccache", + "clang-22", + "clang-format-22", + "clang-tidy-22", + "clang-tools-22", + "clangd-22", + "cmake", + "conan", + "g++-15", + "gcovr", + "gdb-multiarch", + "include-what-you-use", + "lld-22", + "llvm-22", + "mull-22", + "ninja-build", + "xwin" +] diff --git a/.devcontainer/rust/Dockerfile b/.devcontainer/rust/Dockerfile index 387dd71d..f1565b32 100644 --- a/.devcontainer/rust/Dockerfile +++ b/.devcontainer/rust/Dockerfile @@ -4,6 +4,10 @@ ARG BASE_IMAGE=ghcr.io/philips-software/amp-devcontainer-base:edge FROM ${BASE_IMAGE} ARG CARGO_BINSTALL_VERSION=1.17.9 +ARG CARGO_BINUTILS_VERSION=0.3.6 +ARG CARGO_MUTANTS_VERSION=25.3.1 +ARG FLIP_LINK_VERSION=0.1.12 +ARG PROBE_RS_TOOLS_VERSION=0.30.0 ARG RUST_VERSION=1.94.1 ARG DEBIAN_FRONTEND=noninteractive @@ -12,6 +16,11 @@ HEALTHCHECK NONE SHELL ["/bin/bash", "-Eeuo", "pipefail", "-c"] +ENV BINSTALL_DISABLE_TELEMETRY=true \ + CARGO_HOME=/usr/local/cargo \ + RUSTUP_HOME=/usr/local/rustup \ + PATH=/usr/local/cargo/bin:"$PATH" + RUN --mount=type=bind,source=.devcontainer/rust/apt-requirements.json,target=/tmp/apt-requirements.json \ --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ @@ -23,20 +32,32 @@ RUN --mount=type=bind,source=.devcontainer/rust/apt-requirements.json,target=/tm update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-15 10 \ --slave /usr/bin/c++ c++ /usr/bin/g++-15 -EOF -# Install rust -ENV BINSTALL_DISABLE_TELEMETRY=true \ - CARGO_HOME=/usr/local/cargo \ - RUSTUP_HOME=/usr/local/rustup \ - PATH=/usr/local/cargo/bin:"$PATH" - -RUN rustup set profile minimal \ - && rustup default ${RUST_VERSION} \ - && rustup component add clippy llvm-tools rustfmt \ - && rustup target add thumbv7em-none-eabi \ - && rustup target add thumbv7em-none-eabihf - -# Install additional rust tools -RUN wget -qO - "https://github.com/cargo-bins/cargo-binstall/releases/download/v${CARGO_BINSTALL_VERSION}/cargo-binstall-$(uname -m)-unknown-linux-gnu.tgz" | tar xz -C "/usr/bin" \ - && cargo-binstall -y --locked cargo-binutils@0.3.6 cargo-mutants@25.3.1 flip-link@0.1.12 probe-rs-tools@0.30.0 + # Install rust + rustup set profile minimal + rustup default ${RUST_VERSION} + rustup component add clippy llvm-tools rustfmt + rustup target add thumbv7em-none-eabi + rustup target add thumbv7em-none-eabihf + + wget -qO - "https://github.com/cargo-bins/cargo-binstall/releases/download/v${CARGO_BINSTALL_VERSION}/cargo-binstall-$(uname -m)-unknown-linux-gnu.tgz" | tar xz -C "/usr/bin" + cargo-binstall -y --locked "cargo-binutils@${CARGO_BINUTILS_VERSION}" \ + "cargo-mutants@${CARGO_MUTANTS_VERSION}" \ + "flip-link@${FLIP_LINK_VERSION}" \ + "probe-rs-tools@${PROBE_RS_TOOLS_VERSION}" + + # Annotate tools that Syft cannot detect on its own with a `.note.package` ELF + # section (systemd ELF package metadata convention, see + # https://systemd.io/ELF_PACKAGE_METADATA/) so they are included in the SBOM + # generated for the image. `add-sbom-note` is provided by the base image. + add-sbom-note cargo-binstall "${CARGO_BINSTALL_VERSION}" "pkg:cargo/cargo-binstall@${CARGO_BINSTALL_VERSION}" \ + /usr/bin/cargo-binstall + add-sbom-note cargo-binutils "${CARGO_BINUTILS_VERSION}" "pkg:cargo/cargo-binutils@${CARGO_BINUTILS_VERSION}" \ + "${CARGO_HOME}"/bin/cargo-{cov,nm,objcopy,profdata,readobj,size,strip} + add-sbom-note cargo-mutants "${CARGO_MUTANTS_VERSION}" "pkg:cargo/cargo-mutants@${CARGO_MUTANTS_VERSION}" \ + "${CARGO_HOME}/bin/cargo-mutants" + add-sbom-note flip-link "${FLIP_LINK_VERSION}" "pkg:cargo/flip-link@${FLIP_LINK_VERSION}" \ + "${CARGO_HOME}/bin/flip-link" + add-sbom-note probe-rs-tools "${PROBE_RS_TOOLS_VERSION}" "pkg:cargo/probe-rs-tools@${PROBE_RS_TOOLS_VERSION}" \ + "${CARGO_HOME}"/bin/probe-rs "${CARGO_HOME}"/bin/cargo-embed "${CARGO_HOME}"/bin/cargo-flash +EOF diff --git a/.devcontainer/rust/tool-inventory.json b/.devcontainer/rust/tool-inventory.json new file mode 100644 index 00000000..7740357b --- /dev/null +++ b/.devcontainer/rust/tool-inventory.json @@ -0,0 +1,10 @@ +[ + "cargo-binstall", + "cargo-binutils", + "cargo-mutants", + "flip-link", + "g++-15", + "probe-rs-tools", + "rust", + "rustup" +] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b023d79d..ff254825 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,3 +14,4 @@ - [ ] I have added tests for new behavior, and have not broken any existing tests - [ ] I have added or updated relevant documentation - [ ] I have verified that all added components are accounted for in the SBOM +- [ ] I understand the image size delta and agree the functionality justifies it diff --git a/.github/RELEASE_TEMPLATE.md b/.github/RELEASE_TEMPLATE.md index 4f9cbc34..434dea05 100644 --- a/.github/RELEASE_TEMPLATE.md +++ b/.github/RELEASE_TEMPLATE.md @@ -16,3 +16,44 @@ | amp-devcontainer-embedded-cpp | ghcr.io/philips-software/amp-devcontainer-embedded-cpp:{{ amp-devcontainer-embedded-cpp-version }}@{{ amp-devcontainer-embedded-cpp-sha }} | | amp-devcontainer-rust | ghcr.io/philips-software/amp-devcontainer-rust:{{ amp-devcontainer-rust-version }}@{{ amp-devcontainer-rust-sha }} | +#### :hammer_and_wrench: Included tools + +The tables below list the most relevant tools per flavor and their versions, as +recorded in the image's Software Bill of Materials (SBOM). + +
+amp-devcontainer-base + +{{ amp-devcontainer-base-tools }} + +
+ +
+amp-devcontainer-cpp + +{{ amp-devcontainer-cpp-tools }} + +
+ +
+amp-devcontainer-docs + +{{ amp-devcontainer-docs-tools }} + +
+ +
+amp-devcontainer-embedded-cpp + +{{ amp-devcontainer-embedded-cpp-tools }} + +
+ +
+amp-devcontainer-rust + +{{ amp-devcontainer-rust-tools }} + +
+ + diff --git a/.github/scripts/generate-tool-inventory.sh b/.github/scripts/generate-tool-inventory.sh new file mode 100644 index 00000000..39f512ad --- /dev/null +++ b/.github/scripts/generate-tool-inventory.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Derive a curated, per-flavor tool inventory from a Syft SPDX SBOM. +# +# The inventory is a filtered view of the (complete) SBOM: it keeps only the +# packages whose name is listed in the flavor's allowlist and projects them to +# {name, version, purl} pairs. Because the versions come straight from the SBOM, +# the inventory can never disagree with it. +# +# The script fails if an allowlisted tool is absent from the SBOM. This turns a +# version bump that renames a package (e.g. clang-22 -> clang-23) or a tool that +# silently dropped out of the image into a hard, actionable CI error. +# +# Usage: generate-tool-inventory.sh +set -Eeuo pipefail + +SBOM="${1:?path to SPDX SBOM required}" +ALLOWLIST="${2:?path to allowlist JSON required}" +FLAVOR="${3:?flavor name required}" + +inventory="$(jq -n \ + --slurpfile sbom "${SBOM}" \ + --slurpfile allow "${ALLOWLIST}" \ + --arg flavor "${FLAVOR}" ' + ($allow[0] | map(ascii_downcase)) as $names + | [ $sbom[0].packages[] + | select(.name != null) + | . as $pkg + | select($names | index($pkg.name | ascii_downcase)) + | { name: $pkg.name, + version: $pkg.versionInfo, + purl: ([ $pkg.externalRefs[]? | select(.referenceType == "purl") | .referenceLocator ] | first) } ] + | unique_by(.name | ascii_downcase) + | sort_by(.name | ascii_downcase) + | { flavor: $flavor, tools: . }')" + +missing="$(jq -rn \ + --argjson inventory "${inventory}" \ + --slurpfile allow "${ALLOWLIST}" ' + ($inventory.tools | map(.name | ascii_downcase)) as $found + | $allow[0] + | map(select((ascii_downcase) as $name | ($found | index($name)) | not)) + | .[]')" + +if [ -n "${missing}" ]; then + echo "error: the following allowlisted tools were not found in the SBOM:" >&2 + echo "${missing}" | sed 's/^/ - /' >&2 + exit 1 +fi + +echo "${inventory}" diff --git a/.github/workflows/build-push-test.yml b/.github/workflows/build-push-test.yml index 18a73c95..c879a1bb 100644 --- a/.github/workflows/build-push-test.yml +++ b/.github/workflows/build-push-test.yml @@ -33,6 +33,7 @@ jobs: image-name: ${{ github.repository }}-base integration-test-file: test/base/integration-tests.bats integration-test-podman: true + tool-inventory-file: .devcontainer/base/tool-inventory.json build-push-test-flavors: name: ๐Ÿจ ${{ matrix.flavor }} @@ -43,13 +44,17 @@ jobs: - flavor: cpp dockerfile: .devcontainer/cpp/Dockerfile build-embedded-flavor: "false" + tool-inventory-file: .devcontainer/cpp/tool-inventory.json - flavor: docs dockerfile: .devcontainer/docs/Dockerfile + tool-inventory-file: .devcontainer/docs/tool-inventory.json - flavor: embedded-cpp dockerfile: .devcontainer/cpp/Dockerfile build-embedded-flavor: "true" + tool-inventory-file: .devcontainer/embedded-cpp/tool-inventory.json - flavor: rust dockerfile: .devcontainer/rust/Dockerfile + tool-inventory-file: .devcontainer/rust/tool-inventory.json uses: ./.github/workflows/wc-build-push-test.yml secrets: TEST_GITHUB_TOKEN: ${{ secrets.TEST_GITHUB_TOKEN }} @@ -69,3 +74,4 @@ jobs: image-name: ${{ github.repository }}-${{ matrix.flavor }} integration-test-file: test/${{ matrix.flavor }}/integration-tests.bats integration-test-podman: true + tool-inventory-file: ${{ matrix.tool-inventory-file }} diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 2fe54273..406300a1 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -66,6 +66,7 @@ jobs: permissions: # Please note that this is an overly broad scope, but GitHub does not # currently provide a more fine-grained permission for release modification. + actions: read # is needed by actions/download-artifact to fetch the tool inventory contents: write # is needed to modify a release needs: [build-push-test, apply-release-notes-template] env: @@ -90,24 +91,42 @@ jobs: set -Eeuo pipefail FORMATTED_DIGEST=${DIGEST//:/_} gh attestation verify --repo "${GH_REPO}" "oci://${REGISTRY}/${GH_REPO}-${CONTAINER_FLAVOR}@${DIGEST}" --format json --jq '.[] | .attestation.bundle.dsseEnvelope | select(.payloadType == "application/vnd.in-toto+json").payload' | base64 -d | jq . > "${REPOSITORY_OWNER}-${REPOSITORY_NAME}-${CONTAINER_FLAVOR}_${FORMATTED_DIGEST}.intoto.jsonl" - gh release upload "${REF_NAME}" ./*.intoto.jsonl + gh release upload --clobber "${REF_NAME}" ./*.intoto.jsonl env: DIGEST: ${{ steps.inspect-manifest.outputs.digest }} GH_REPO: ${{ github.repository }} GH_TOKEN: ${{ github.token }} REPOSITORY_OWNER: ${{ github.repository_owner }} REPOSITORY_NAME: ${{ github.event.repository.name }} + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: tool-inventory-${{ github.event.repository.name }}-${{ matrix.flavor }} + path: tool-inventory - name: Update package details in release run: | set -Eeuo pipefail + TOOLS_TABLE=$(jq -r '"| Tool | Version |\n| --- | --- |\n" + ([.tools[] | "| \(.name) | \(.version) |"] | join("\n"))' tool-inventory/tool-inventory.json) UPDATED_NOTES=$(gh release view "${REF_NAME}" --json body -q '.body') UPDATED_NOTES=${UPDATED_NOTES//"{{ amp-devcontainer-${CONTAINER_FLAVOR}-version }}"/"${REF_NAME}"} UPDATED_NOTES=${UPDATED_NOTES//"{{ amp-devcontainer-${CONTAINER_FLAVOR}-sha }}"/"${DIGEST}"} + UPDATED_NOTES=${UPDATED_NOTES//"{{ amp-devcontainer-${CONTAINER_FLAVOR}-tools }}"/"${TOOLS_TABLE}"} gh release edit "${REF_NAME}" --notes "${UPDATED_NOTES}" env: DIGEST: ${{ steps.inspect-manifest.outputs.digest }} GH_REPO: ${{ github.repository }} GH_TOKEN: ${{ github.token }} + - name: Upload tool inventory to release + run: | + set -Eeuo pipefail + FORMATTED_DIGEST=${DIGEST//:/_} + INVENTORY="${REPOSITORY_OWNER}-${REPOSITORY_NAME}-${CONTAINER_FLAVOR}_${FORMATTED_DIGEST}.tool-inventory.json" + cp tool-inventory/tool-inventory.json "${INVENTORY}" + gh release upload --clobber "${REF_NAME}" "${INVENTORY}" + env: + DIGEST: ${{ steps.inspect-manifest.outputs.digest }} + GH_TOKEN: ${{ github.token }} + REPOSITORY_OWNER: ${{ github.repository_owner }} + REPOSITORY_NAME: ${{ github.event.repository.name }} publish-devcontainer-templates: name: ๐Ÿ“ Publish templates diff --git a/.github/workflows/wc-build-push-test.yml b/.github/workflows/wc-build-push-test.yml index 7fa64260..e128336f 100644 --- a/.github/workflows/wc-build-push-test.yml +++ b/.github/workflows/wc-build-push-test.yml @@ -82,6 +82,15 @@ on: required: false type: string default: '["ubuntu-latest"]' + tool-inventory-file: + description: >- + Path to a JSON allowlist of package names used to derive a curated tool + inventory from the generated SBOM. When empty, no inventory is produced. + + Examples: + '.devcontainer//tool-inventory.json' + required: false + type: string outputs: digest: value: ${{ jobs.build-push.outputs.digest }} @@ -133,6 +142,7 @@ jobs: devcontainer-metadata-file: ${{ inputs.devcontainer-metadata-file }} runner-labels: ${{ inputs.runner-labels }} build-test-runner-labels: ${{ inputs.build-test-runner-labels }} + tool-inventory-file: ${{ inputs.tool-inventory-file }} integration-test-docker: name: ๐Ÿงช diff --git a/.github/workflows/wc-build-push.yml b/.github/workflows/wc-build-push.yml index 8d725810..babf4322 100644 --- a/.github/workflows/wc-build-push.yml +++ b/.github/workflows/wc-build-push.yml @@ -15,7 +15,7 @@ on: workflow_call: inputs: build-args: - required: false + required: true type: string build-test-runner-labels: required: true @@ -38,6 +38,9 @@ on: runner-labels: required: true type: string + tool-inventory-file: + required: true + type: string outputs: digest: value: ${{ jobs.merge-image.outputs.digest }} @@ -248,6 +251,32 @@ jobs: with: image: ${{ needs.sanitize-image-name.outputs.fully-qualified-image-name }}@${{ steps.inspect-manifest.outputs.digest }} dependency-snapshot: true + output-file: ${{ runner.temp }}/sbom.spdx.json + - name: Derive tool inventory from SBOM + if: ${{ inputs.tool-inventory-file }} + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + sparse-checkout: | + .github/scripts/generate-tool-inventory.sh + ${{ inputs.tool-inventory-file }} + sparse-checkout-cone-mode: false + - name: Generate tool inventory + if: ${{ inputs.tool-inventory-file }} + run: | + set -Eeuo pipefail + bash .github/scripts/generate-tool-inventory.sh \ + "${SBOM}" "${ALLOWLIST}" "${FLAVOR}" | tee tool-inventory.json + env: + SBOM: ${{ runner.temp }}/sbom.spdx.json + ALLOWLIST: ${{ inputs.tool-inventory-file }} + FLAVOR: ${{ needs.sanitize-image-name.outputs.image-basename }} + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: ${{ inputs.tool-inventory-file }} + with: + name: tool-inventory-${{ needs.sanitize-image-name.outputs.image-basename }} + path: tool-inventory.json + retention-days: 10 - uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1 with: subject-name: ${{ needs.sanitize-image-name.outputs.fully-qualified-image-name }} diff --git a/test/base/integration-tests.bats b/test/base/integration-tests.bats index 39b82d41..1442ce72 100644 --- a/test/base/integration-tests.bats +++ b/test/base/integration-tests.bats @@ -12,3 +12,49 @@ setup() { assert_success assert_output --partial "OK" } + +# bats test_tags=Sbom +@test "add-sbom-note tool is installed and executable" { + run command -v add-sbom-note + assert_success + + assert [ -x /usr/local/bin/add-sbom-note ] +} + +# bats test_tags=Sbom +@test "add-sbom-note fails when required arguments are missing" { + run add-sbom-note only-a-name + assert_failure +} + +# bats test_tags=Sbom +@test "add-sbom-note fails when none of the given binaries exist" { + run add-sbom-note example 1.2.3 pkg:generic/example@1.2.3 /nonexistent/binary + + assert_failure + assert_output --partial "no binaries found to annotate for example" +} + +# bats test_tags=Sbom +@test "add-sbom-note annotates an ELF binary with a .note.package section" { + BINARY=$(mktemp) + cp "$(command -v bash)" "$BINARY" + chmod +x "$BINARY" + # Ubuntu ships some binaries with a pre-existing .note.package section; start + # from a clean binary so the test exercises add-sbom-note's own annotation. + objcopy --remove-section .note.package "$BINARY" 2> /dev/null || true + + run add-sbom-note example 1.2.3 pkg:generic/example@1.2.3 "$BINARY" + assert_success + + SECTION=$(mktemp) + objcopy --dump-section .note.package="$SECTION" "$BINARY" /dev/null + + run cat "$SECTION" + assert_success + assert_output --partial '"name": "example"' + assert_output --partial '"version": "1.2.3"' + assert_output --partial '"purl": "pkg:generic/example@1.2.3"' + + rm -f "$BINARY" "$SECTION" +}