From 244b269b6746c739b8d970fd4b9682c2d4e34ee0 Mon Sep 17 00:00:00 2001 From: Robert Sturla Date: Sun, 21 Apr 2024 20:46:21 +0100 Subject: [PATCH] feat(ci): add image scanning workflow (#1161) --- .github/syft.yml | 5 ++ .github/workflows/build-38-bluefin.yml | 14 +++- .github/workflows/build-39-bluefin.yml | 12 +++- .github/workflows/build-40-bluefin.yml | 12 +++- .github/workflows/reusable-build.yml | 81 ++++++++++++++++++++--- .github/workflows/reusable-image-scan.yml | 73 ++++++++++++++++++++ 6 files changed, 182 insertions(+), 15 deletions(-) create mode 100644 .github/syft.yml create mode 100644 .github/workflows/reusable-image-scan.yml diff --git a/.github/syft.yml b/.github/syft.yml new file mode 100644 index 00000000000..e156977819c --- /dev/null +++ b/.github/syft.yml @@ -0,0 +1,5 @@ +# Exclude some files Syft will never get anything useful from +exclude: + - '/sysroot/ostree/repo/objects/**' + - '/usr/share/icons/**' + - '/usr/share/doc/**' diff --git a/.github/workflows/build-38-bluefin.yml b/.github/workflows/build-38-bluefin.yml index b2e43483c94..d8161b96b3a 100644 --- a/.github/workflows/build-38-bluefin.yml +++ b/.github/workflows/build-38-bluefin.yml @@ -9,7 +9,7 @@ on: - '**.md' - 'system_files/kinoite/**' schedule: - - cron: '42 16 * * *' # 16:42 UTC everyday + - cron: '42 16 * * *' # 16:42 UTC everyday workflow_dispatch: jobs: @@ -19,4 +19,14 @@ jobs: secrets: inherit with: brand_name: bluefin - fedora_version: 38 \ No newline at end of file + fedora_version: 38 + + scan: + # Scan can still be ran when some builds fail since only successfully built + # images will be stored in the output + if: github.event_name != 'pull_request' && always() + uses: ./.github/workflows/reusable-image-scan.yml + needs: build + secrets: inherit + with: + images: ${{ needs.build.outputs.images }} diff --git a/.github/workflows/build-39-bluefin.yml b/.github/workflows/build-39-bluefin.yml index c07ae86766e..3112be3fe2a 100644 --- a/.github/workflows/build-39-bluefin.yml +++ b/.github/workflows/build-39-bluefin.yml @@ -19,4 +19,14 @@ jobs: secrets: inherit with: brand_name: bluefin - fedora_version: 39 \ No newline at end of file + fedora_version: 39 + + scan: + # Scan can still be ran when some builds fail since only successfully built + # images will be stored in the output + if: github.event_name != 'pull_request' && always() + uses: ./.github/workflows/reusable-image-scan.yml + needs: build + secrets: inherit + with: + images: ${{ needs.build.outputs.images }} diff --git a/.github/workflows/build-40-bluefin.yml b/.github/workflows/build-40-bluefin.yml index 10efcc196e0..a187c9de3ee 100644 --- a/.github/workflows/build-40-bluefin.yml +++ b/.github/workflows/build-40-bluefin.yml @@ -19,4 +19,14 @@ jobs: secrets: inherit with: brand_name: bluefin - fedora_version: 40 \ No newline at end of file + fedora_version: 40 + + scan: + # Scan can still be ran when some builds fail since only successfully built + # images will be stored in the output + if: github.event_name != 'pull_request' && always() + uses: ./.github/workflows/reusable-image-scan.yml + needs: build + secrets: inherit + with: + images: ${{ needs.build.outputs.images }} diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index 6e52de12f4a..094a53a378f 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -10,6 +10,10 @@ on: description: "'aurora' or 'bluefin'" required: true type: string + outputs: + images: + description: 'An array of images built and pushed to the registry' + value: ${{ jobs.check.outputs.images }} env: IMAGE_REGISTRY: ghcr.io/${{ github.repository_owner }} @@ -22,6 +26,8 @@ jobs: name: image runs-on: ubuntu-latest continue-on-error: false + outputs: + image_full: ${{ steps.generate-outputs.outputs.image }} strategy: fail-fast: false matrix: @@ -262,24 +268,77 @@ jobs: COSIGN_EXPERIMENTAL: false COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }} - - name: Echo outputs + - name: Generate file containing outputs if: github.event_name != 'pull_request' - run: | - echo "${{ toJSON(steps.push.outputs) }}" + env: + DIGEST: ${{ steps.push.outputs.digest }} + IMAGE_REGISTRY: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }} + IMAGE_NAME: ${{ env.IMAGE_NAME }} + IMAGE_FLAVOR: ${{ matrix.image_flavor }} + FEDORA_VERSION: ${{ matrix.fedora_version }} + run: + echo "${IMAGE_REGISTRY}@${DIGEST}" > "${IMAGE_NAME}-${IMAGE_FLAVOR}-${FEDORA_VERSION}.txt" + + - name: Upload artifact + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: image-${{ env.IMAGE_NAME }}-${{ matrix.image_flavor }}-${{ matrix.fedora_version }} + retention-days: 1 + if-no-files-found: error + path: | + ${{ env.IMAGE_NAME }}-${{ matrix.image_flavor }}-${{ matrix.fedora_version }}.txt check: name: Check all ${{ inputs.brand_name }} ${{ inputs.fedora_version }} builds successful - if: ${{ !cancelled() }} + if: always() runs-on: ubuntu-latest needs: [build_container] + outputs: + images: ${{ steps.generate-outputs.outputs.images }} steps: - - name: Exit on failure - if: ${{ needs.build_container.result == 'failure' }} - shell: bash - run: exit 1 - - name: Exit - shell: bash - run: exit 0 + - name: Download artifacts + if: github.event_name != 'pull_request' + id: download-artifacts + uses: actions/download-artifact@v4 + with: + pattern: image-* + merge-multiple: true + + - name: Create output + if: github.event_name != 'pull_request' + id: generate-outputs + env: + JOBS: ${{ toJson(needs) }} + ARTIFACT_PATH: ${{ steps.download-artifacts.outputs.download-path }} + run: | + # Initialize the array + images=() + + # Populate the array with each line from each file in the artifacts directory + for file in $ARTIFACT_PATH/*; do + while IFS= read -r line; do + images+=("$line") + done < "$file" + done + + # Create the GITHUB_OUTPUT in the format '["image1", "image2", ...]' + echo "images=$(printf '%s\n' "${images[@]}" | jq -R -s -c 'split("\n") | .[:-1]')" >> $GITHUB_OUTPUT + + - name: Check Jobs + env: + JOBS: ${{ toJson(needs) }} + run: | + echo "Job status:" + echo $JOBS | jq -r 'to_entries[] | " - \(.key): \(.value.result)"' + + for i in $(echo $JOBS | jq -r 'to_entries[] | .value.result'); do + if [ "$i" != "success" ] && [ "$i" != "skipped" ]; then + echo "" + echo "Status check not okay!" + exit 1 + fi + done build_iso: name: iso diff --git a/.github/workflows/reusable-image-scan.yml b/.github/workflows/reusable-image-scan.yml new file mode 100644 index 00000000000..a720ca94142 --- /dev/null +++ b/.github/workflows/reusable-image-scan.yml @@ -0,0 +1,73 @@ +name: Scan Image +on: + workflow_call: + inputs: + images: + description: "A comma-separated list of images to scan. E.G. '[\"docker.io/library/alpine:3.14.0\", \"docker.io/library/alpine:3.13.6\"]'" + required: true + type: string + +jobs: + generate-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Set matrix + id: set-matrix + env: + IMAGES: ${{ inputs.images }} + run: | + echo "matrix=$IMAGES" >> $GITHUB_OUTPUT + + scan-image: + needs: generate-matrix + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + image: ${{fromJson(needs.generate-matrix.outputs.matrix)}} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Maximize build space + uses: ublue-os/remove-unwanted-software@v6 + + - name: Install Syft + shell: bash + run: | + curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin + syft version + + - name: Generate SBOM + env: + IMAGE: ${{ matrix.image }} + run: | + syft ${IMAGE} \ + --output cyclonedx-json=sbom.json \ + --config ./.github/syft.yml + + - name: Scan SBOM + id: scan + uses: anchore/scan-action@3343887d815d7b07465f6fdcd395bd66508d486a # v3 + with: + sbom: sbom.json + output-format: json + fail-build: false + + - name: Generate artifact name + id: artifact-name + env: + IMAGE: ${{ matrix.image }} + run: | + echo "name=$(echo ${IMAGE} | awk -F'/' '{print $NF}' | sed 's/:/-/g')" >> $GITHUB_OUTPUT + + - name: Upload scan results + uses: actions/upload-artifact@v4 + with: + name: security-${{ steps.artifact-name.outputs.name }} + if-no-files-found: error + path: | + sbom.json + ${{ steps.scan.outputs.json }}