diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0534d82..4c6e670 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,8 +6,13 @@ on: publish: description: 'Publish images to registries' required: false - default: true + default: false type: boolean + fail_on_severity: + description: 'Comma-separated list of severities that fail the build if post-patch CVEs remain (e.g. CRITICAL,HIGH). Valid values: CRITICAL, HIGH, MEDIUM, LOW. Use NONE to disable the gate entirely.' + required: false + default: 'CRITICAL,HIGH' + type: string push: tags: - 'v*.*' @@ -16,6 +21,9 @@ on: env: IMAGE_NAME: pimcore/pimcore + COPA_VERSION: "0.14.1" + BUILDKIT_VERSION: "0.30.0" + TRIVY_DB_REPOSITORY: "ghcr.io/aquasecurity/trivy-db:2" jobs: build-php: @@ -55,16 +63,66 @@ jobs: - name: Login to GitHub Container Registry run: echo ${{ secrets.IMAGES_REPO_TOKEN }} | docker login ghcr.io -u ${{ secrets.IMAGES_REPO_USERNAME }} --password-stdin + - name: Install Copa and Trivy + run: | + set -eux + # Install Trivy + sudo apt-get update + sudo apt-get install -y wget curl apt-transport-https gnupg lsb-release jq + wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null + echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | sudo tee /etc/apt/sources.list.d/trivy.list + sudo apt-get update + sudo apt-get install -y trivy + + # Install Copa + COPA_ARCH="$(dpkg --print-architecture)" + curl -fsSL -o copa.tar.gz "https://github.com/project-copacetic/copacetic/releases/download/v${COPA_VERSION}/copa_${COPA_VERSION}_linux_${COPA_ARCH}.tar.gz" + curl -fsSL -o copacetic_checksums.txt "https://github.com/project-copacetic/copacetic/releases/download/v${COPA_VERSION}/copacetic_checksums.txt" + # Verify checksum before extracting + EXPECTED_SHA=$(grep "copa_${COPA_VERSION}_linux_${COPA_ARCH}.tar.gz" copacetic_checksums.txt | awk '{print $1}') + ACTUAL_SHA=$(sha256sum copa.tar.gz | awk '{print $1}') + if [ "$EXPECTED_SHA" != "$ACTUAL_SHA" ]; then + echo "::error::Copa checksum mismatch! Expected ${EXPECTED_SHA}, got ${ACTUAL_SHA}" + exit 1 + fi + tar -xzf copa.tar.gz copa + sudo mv copa /usr/local/bin/copa + rm copa.tar.gz copacetic_checksums.txt + + - name: Start buildkit daemon + run: | + docker run --detach --rm --privileged \ + -p 127.0.0.1:8888:8888/tcp \ + --name buildkitd \ + --entrypoint buildkitd \ + moby/buildkit:v${{ env.BUILDKIT_VERSION }} \ + --addr tcp://0.0.0.0:8888 + + # Wait for buildkit to be ready + for i in $(seq 1 60); do + if docker exec buildkitd buildctl --addr tcp://127.0.0.1:8888 debug workers >/dev/null 2>&1; then + echo "BuildKit is ready" + break + fi + if [ "$i" -eq 60 ]; then + echo "::error::BuildKit failed to start within 60 seconds" + exit 1 + fi + sleep 1 + done + - name: Configure and build images id: vars env: VERSION_OVERRIDE: "${{ matrix.build.version-override }}" ARCH_TAG: ${{ contains(matrix.runner, 'arm') && 'arm64' || 'amd64' }} PUSH: ${{ github.event_name != 'workflow_dispatch' || inputs.publish }} + FAIL_ON_SEVERITY: ${{ inputs.fail_on_severity || 'CRITICAL,HIGH' }} + TRIVY_DB_REPOSITORY: ${{ env.TRIVY_DB_REPOSITORY }} run: | set -eux; - sudo apt-get update + mkdir -p trivy-reports echo ${{ matrix.runner}} if [[ "${{ matrix.build.tag }}" =~ ^v?1.[0-9x]+$ ]]; then @@ -121,13 +179,94 @@ jobs: TAGS="$TAGS --tag $GHCR_TAG_MAJOR" fi - docker build --output "type=image,push=$PUSH" \ + # Build and load image locally + docker build --load \ --provenance=false \ --platform "linux/${ARCH_TAG}" \ --target="pimcore_php_$imageVariant" \ --build-arg PHP_VERSION="${PHP_VERSION}" \ --build-arg DEBIAN_VERSION="${DEBIAN_VERSION}" \ - ${TAGS} . + --tag "${IMAGE_NAME}:${TAG}" . + + # Patch OS-level vulnerabilities with Copa + echo "Scanning and patching image ${IMAGE_NAME}:${TAG}" + trivy image --pkg-types os --ignore-unfixed --format json \ + -o /tmp/trivy-report.json "${IMAGE_NAME}:${TAG}" + + if [ -s /tmp/trivy-report.json ] && jq -e '.Results[]? | select(.Vulnerabilities != null and (.Vulnerabilities | length > 0))' /tmp/trivy-report.json > /dev/null 2>&1; then + copa patch -i "${IMAGE_NAME}:${TAG}" \ + -r /tmp/trivy-report.json \ + -t "${TAG}-patched" \ + -a tcp://127.0.0.1:8888 + + # Verify the patched image exists + if ! docker image inspect "${IMAGE_NAME}:${TAG}-patched" > /dev/null 2>&1; then + echo "::error::Patched image not found for ${IMAGE_NAME}:${TAG}" + exit 1 + fi + + docker rmi "${IMAGE_NAME}:${TAG}" + docker tag "${IMAGE_NAME}:${TAG}-patched" "${IMAGE_NAME}:${TAG}" + docker rmi "${IMAGE_NAME}:${TAG}-patched" + echo "Successfully patched ${IMAGE_NAME}:${TAG}" + else + echo "No fixable OS vulnerabilities found, skipping Copa patch" + fi + rm -f /tmp/trivy-report.json + + # Post-patch vulnerability gate + FAIL_SEVERITY="$FAIL_ON_SEVERITY" + if [ "$FAIL_SEVERITY" != "NONE" ]; then + echo "Running post-patch scan (fail on ${FAIL_SEVERITY}+)" + + # Get the image hash for report naming + IMAGE_HASH=$(docker image inspect "${IMAGE_NAME}:${TAG}" --format '{{.Id}}' | sed 's/sha256://' | head -c 12) + + trivy image --pkg-types os --ignore-unfixed \ + --severity "$FAIL_SEVERITY" \ + --format table \ + -o /tmp/trivy-os-${TAG}.txt \ + "${IMAGE_NAME}:${TAG}" || true + + # Save report with image hash for artifact upload + trivy image --pkg-types os --ignore-unfixed \ + --severity "$FAIL_SEVERITY" \ + --format json \ + -o "trivy-reports/${TAG}_${IMAGE_HASH}.json" \ + "${IMAGE_NAME}:${TAG}" || true + cp /tmp/trivy-os-${TAG}.txt "trivy-reports/${TAG}_${IMAGE_HASH}.txt" 2>/dev/null || true + + trivy image --pkg-types os --ignore-unfixed \ + --exit-code 1 \ + --severity "$FAIL_SEVERITY" \ + "${IMAGE_NAME}:${TAG}" + + # Attach scan results to GitHub Actions job summary + { + echo "## Trivy Scan: ${IMAGE_NAME}:${TAG}" + echo "" + echo "### OS Vulnerabilities (${FAIL_SEVERITY}+)" + echo '```' + cat /tmp/trivy-os-${TAG}.txt 2>/dev/null || echo "No results" + echo '```' + echo "" + } >> "$GITHUB_STEP_SUMMARY" + rm -f /tmp/trivy-os-${TAG}.txt + fi + + # Apply all tags to the (patched) image + CLEAN_TAGS_FOR_TAGGING="${TAGS//--tag /}" + read -r -a ALL_TAGS <<< "$CLEAN_TAGS_FOR_TAGGING" + for additional_tag in "${ALL_TAGS[@]}"; do + if [ "$additional_tag" != "${IMAGE_NAME}:${TAG}" ]; then + docker tag "${IMAGE_NAME}:${TAG}" "$additional_tag" + fi + done + + # Push if publishing (parallel for speed) + if [[ "$PUSH" == "true" ]]; then + printf '%s\n' "${ALL_TAGS[@]}" | xargs -P 4 -I {} docker push "{}" + fi docker inspect ${IMAGE_NAME}:${TAG} || true; @@ -145,8 +284,26 @@ jobs: done fi + # Clean up to save disk space + docker rmi "${IMAGE_NAME}:${TAG}" || true + for additional_tag in "${ALL_TAGS[@]}"; do + docker rmi "$additional_tag" 2>/dev/null || true + done + done + - name: Stop buildkit daemon + if: always() + run: docker stop buildkitd || true + + - name: Upload trivy reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: trivy-reports_${{ matrix.runner }}_${{ matrix.build.tag }}_${{ matrix.build.php }}_${{ matrix.build.distro }}_${{ matrix.build.version-override }}_${{ matrix.build.latest-tag }} + path: trivy-reports/ + if-no-files-found: ignore + - name: Upload aggregated tags if: github.event_name != 'workflow_dispatch' || inputs.publish uses: actions/upload-artifact@v7