diff --git a/.github/actions/build-linux-image/action.yml b/.github/actions/build-linux-image/action.yml index 5ed50528..428859e1 100644 --- a/.github/actions/build-linux-image/action.yml +++ b/.github/actions/build-linux-image/action.yml @@ -42,7 +42,7 @@ runs: declare -a outputs=(); if test "${push}" = true; then - outputs+=(--output "type=image,compression=zstd,force-compression=true,oci-mediatypes=true,push=true,push-by-digest=true,name=${repo}"); + outputs+=(--output "type=image,compression=zstd,force-compression=true,oci-mediatypes=true,load=true,name=${repo}"); # HACK: remove the `-t` arg from the `docker buildx build` command generated by `devcontainer build` sed -i 's/,t.map(v=>l.push("-t",v))//g' "$(npm list -g | head -n1)"/node_modules/@devcontainers/cli/dist/spec-node/devContainersSpecCLI.js; fi @@ -66,8 +66,53 @@ runs: fi done + echo "base_image=${repo,,}:${tag}" >> "$GITHUB_OUTPUT" + + - id: embed-sbom + name: Embed SBOM into ${{ inputs.repo }}:${{ inputs.tag }}-${{ inputs.arch }} + shell: bash + env: + arch: "${{ inputs.arch }}" + base_image: "${{ steps.build.outputs.base_image }}" + push: "${{ inputs.push }}" + repo: "${{ inputs.repo }}" + tag: "${{ inputs.tag }}" + run: | + set -euo pipefail + + if test -z "${base_image}"; then + echo "Base image tag missing" + exit 1 + fi + + action_dir="${GITHUB_ACTION_PATH:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" + sbom_dockerfile="${action_dir}/sbom.Dockerfile" + sbom_log="${RUNNER_TEMP}/sbom-${arch}.log" + + output="type=image,compression=zstd,force-compression=true,oci-mediatypes=true,name=${repo,,}" if test "${push}" = true; then - echo "hash_${arch}=$(grep 'exporting manifest sha256:' "${{ runner.temp }}/${arch}.log" | tail -n1 | grep -oP 'sha256:\w+')" >> "$GITHUB_OUTPUT"; + output+="\,push=true,push-by-digest=true" else - echo "hash_${arch}=" >> "$GITHUB_OUTPUT"; + output+="\,load=true" fi + + docker buildx build \ + --platform "linux/${arch}" \ + --tag "${repo,,}:${tag}" \ + --build-context base="docker-image://${base_image}" \ + --build-arg SYFT_VERSION="1.32.0" \ + --build-arg SOURCE_IMAGE_NAME="${base_image}" \ + --output "${output}" \ + --file "${sbom_dockerfile}" \ + "${action_dir}" 2>&1 | tee "${sbom_log}" + + digest="" + if test "${push}" = true; then + digest="$(grep 'exporting manifest sha256:' "${sbom_log}" | tail -n1 | grep -oP 'sha256:\w+')" + if test -z "${digest}"; then + echo "Failed to determine pushed digest" + exit 1 + fi + fi + + echo "hash_${arch}=${digest}" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/build-linux-image/sbom.Dockerfile b/.github/actions/build-linux-image/sbom.Dockerfile new file mode 100644 index 00000000..c5c06ee8 --- /dev/null +++ b/.github/actions/build-linux-image/sbom.Dockerfile @@ -0,0 +1,31 @@ +# syntax=docker/dockerfile:1.6 +ARG SYFT_VERSION +ARG SOURCE_IMAGE_NAME + +FROM --platform=$BUILDPLATFORM alpine:3.20 AS syft-base +ARG BUILDPLATFORM +ARG SYFT_VERSION +RUN apk add --no-cache curl tar ca-certificates \ + && case "$BUILDPLATFORM" in \ + linux/amd64) SYFT_ARCH="linux_amd64" ;; \ + linux/arm64) SYFT_ARCH="linux_arm64" ;; \ + *) echo "Unsupported BUILDPLATFORM: ${BUILDPLATFORM}" >&2 && exit 1 ;; \ + esac \ + && curl -sSfL "https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}/syft_${SYFT_VERSION}_${SYFT_ARCH}.tar.gz" \ + | tar -xz -C /usr/local/bin syft \ + && chmod +x /usr/local/bin/syft + +FROM base AS devcontainer-base + +FROM syft-base AS sbom +RUN --mount=type=bind,from=devcontainer-base,source=/,target=/rootfs,ro \ + mkdir -p /out && \ + syft scan \ + --source-name "${SOURCE_IMAGE_NAME}" \ + --scope all-layers \ + --output cyclonedx-json@1.6=/out/sbom.json \ + dir:/rootfs + +FROM devcontainer-base +RUN mkdir -p /sbom +COPY --from=sbom /out/sbom.json /sbom/sbom.json diff --git a/windows/build-windows-image.ps1 b/windows/build-windows-image.ps1 index 42599823..38f48925 100644 --- a/windows/build-windows-image.ps1 +++ b/windows/build-windows-image.ps1 @@ -67,3 +67,50 @@ catch { finally { Pop-Location } + +$syftVersion = "1.32.0" +$arch = switch ($env:PROCESSOR_ARCHITECTURE.ToLower()) { + "amd64" { "windows_amd64" } + "arm64" { "windows_arm64" } + default { throw "Unsupported PROCESSOR_ARCHITECTURE '$env:PROCESSOR_ARCHITECTURE'" } +} +$syftZipName = "syft_${syftVersion}_${arch}.zip" +$syftDownload = "https://github.com/anchore/syft/releases/download/v$syftVersion/$syftZipName" +$tempRoot = Join-Path $env:TEMP ("sbom-" + [guid]::NewGuid()) +$syftArchive = Join-Path $tempRoot $syftZipName +$sbomJson = Join-Path $tempRoot "sbom.json" +$contextDir = Join-Path $tempRoot "context" +New-Item -ItemType Directory -Path $tempRoot, $contextDir | Out-Null + +try { + Invoke-WebRequest ` + -Uri $syftDownload ` + -OutFile $syftArchive ` + -UseBasicParsing + Expand-Archive -Path $syftArchive -DestinationPath $tempRoot -Force + + $syftExe = Get-ChildItem -Path $tempRoot -Filter syft.exe -Recurse | + Select-Object -First 1 | + ForEach-Object FullName + if (-not $syftExe) { + throw "syft.exe not found after extracting $syftZipName" + } + + & $syftExe ` + "docker:$ENV:IMAGE_NAME" ` + --scope all-layers ` + --source-name "$ENV:IMAGE_NAME" ` + "--output" "cyclonedx-json@1.6=$sbomJson" + + Copy-Item -Path $sbomJson -Destination (Join-Path $contextDir "sbom.json") + Copy-Item -Path (Join-Path $PSScriptRoot "sbom.Dockerfile") -Destination (Join-Path $contextDir "Dockerfile") + + docker build ` + --file (Join-Path $contextDir "Dockerfile") ` + --build-arg BASE_IMAGE=$ENV:IMAGE_NAME ` + --tag $ENV:IMAGE_NAME ` + $contextDir +} +finally { + Remove-Item -Path $tempRoot -Recurse -Force +} diff --git a/windows/sbom.Dockerfile b/windows/sbom.Dockerfile new file mode 100644 index 00000000..288ceb45 --- /dev/null +++ b/windows/sbom.Dockerfile @@ -0,0 +1,9 @@ +# escape=` +ARG BASE_IMAGE + +FROM ${BASE_IMAGE} + +SHELL ["powershell.exe", "-Command"] + +RUN New-Item -ItemType Directory -Path 'C:\sbom' -Force | Out-Null +COPY ["sbom.json", "C:\\sbom\\sbom.json"]