From 172991dfb19d31acfc9ba5f55978ad107358e83f Mon Sep 17 00:00:00 2001 From: Sascha Schwarze Date: Fri, 13 Dec 2024 21:53:13 +0100 Subject: [PATCH] Improve Report release vulnerabilities action Signed-off-by: Sascha Schwarze --- .github/download-latest-release.sh | 29 ++++++ .github/report-release-vulnerabilities.sh | 91 +++++++++++++++---- .../report-release-vulnerabilities.yaml | 30 +++++- 3 files changed, 131 insertions(+), 19 deletions(-) create mode 100755 .github/download-latest-release.sh diff --git a/.github/download-latest-release.sh b/.github/download-latest-release.sh new file mode 100755 index 000000000..70c224b9b --- /dev/null +++ b/.github/download-latest-release.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Copyright The Shipwright Contributors +# +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +# determine the tag of the latest release +releaseTag="$(gh release view --json tagName --jq .tagName)" +echo "[INFO] Tag is ${releaseTag}" +echo "release-tag=${releaseTag}" >>"${GITHUB_OUTPUT}" + +# determine the branch name +releaseBranch="release-${releaseTag%.*}" +echo "[INFO] Branch is ${releaseBranch}" +echo "release-branch=${releaseBranch}" >>"${GITHUB_OUTPUT}" + +# download the release.yaml +gh release download "${releaseTag}" --clobber --pattern release.yaml --output /tmp/release.yaml +echo "release-yaml=/tmp/release.yaml" >>"${GITHUB_OUTPUT}" + +# look at the first image, download the entrypoint to determine the Go version +image="$(grep ghcr.io /tmp/release.yaml | sed -E 's/(image|value)://' | tr -d ' ' | head -n 1)" +entrypoint="$(crane config "${image}" | jq -r '.config.Entrypoint[0]')" +crane export "${image}" - | tar -xf - -C /tmp "${entrypoint}" +goVersion="$(go version "/tmp${entrypoint}" | sed "s#/tmp${entrypoint}: go##")" +goVersion="${goVersion:0:4}" +echo "[INFO] Go version is ${goVersion}" +echo "go-version=${goVersion}" >>"${GITHUB_OUTPUT}" diff --git a/.github/report-release-vulnerabilities.sh b/.github/report-release-vulnerabilities.sh index ee1386c03..991c82114 100755 --- a/.github/report-release-vulnerabilities.sh +++ b/.github/report-release-vulnerabilities.sh @@ -5,14 +5,12 @@ set -euo pipefail -# download the release.yaml -gh release download --clobber --pattern release.yaml --output /tmp/release.yaml - # extract the images -readarray -t images < <(grep ghcr.io /tmp/release.yaml | sed -E 's/(image|value)://' | tr -d ' ' | sort -u) +readarray -t images < <(grep ghcr.io "${RELEASE_YAML}" | sed -E 's/(image|value)://' | tr -d ' ' | sort -u) # capture whether vulnerabilities exist hasVulnerabilities=false +allVulnerabilitiesFixedByRebuild=true # iterate the images true>/tmp/report.md @@ -20,27 +18,46 @@ for image in "${images[@]}"; do echo "[INFO] Checking image ${image}" echo "## ${image}" >>/tmp/report.md + # Rebuilding image to compare vulnerabilities + entrypoint="$(crane config "${image}" | jq -r '.config.Entrypoint[0]')" + binaryName="$(basename "${entrypoint}")" + echo " [INFO] Rebuilding github.com/shipwright-io/build/cmd/${binaryName}" + pushd "${REPOSITORY}" >/dev/null + KO_DOCKER_REPO=dummy/image ko build "github.com/shipwright-io/build/cmd/${binaryName}" --bare --platform linux/amd64 --push=false --sbom none --tarball /tmp/image.tar + popd >/dev/null + # OS vulnerabilities echo " [INFO] Checking for OS vulnerabilities" echo "### OS vulnerabilities" >>/tmp/report.md osVulns="$(trivy image --format json --ignore-unfixed --no-progress --pkg-types os --scanners vuln --skip-db-update --timeout 10m "${image}")" osVulnsFound=false + osVulnsLatest="$(trivy image --format json --ignore-unfixed --input /tmp/image.tar --no-progress --pkg-types os --scanners vuln --skip-db-update --timeout 10m)" while read -r id pkg severity vulnerableVersion fixedVersion; do if [ "${id}" == "" ]; then continue fi + # Check if it exists in the latest image + if [ "$(jq --raw-output "(.Results[0].Vulnerabilities // [])[] | select(.VulnerabilityID == \"${id}\")" <<<"${osVulnsLatest}")" == "" ]; then + fixed=":white_check_mark:" + fixedSentence=" This vulnerability is fixed by a rebuild." + else + fixed=":x:" + fixedSentence= + allVulnerabilitiesFixedByRebuild=false + fi + if [ "${osVulnsFound}" == "false" ]; then - echo "| Vulnerability | Package | Severity | Version |" >>/tmp/report.md - echo "| -- | -- | -- | -- |" >>/tmp/report.md + echo "| Vulnerability | Package | Severity | Version | Fixed by rebuild |" >>/tmp/report.md + echo "| -- | -- | -- | -- | -- |" >>/tmp/report.md osVulnsFound=true hasVulnerabilities=true fi severityLower="$(tr '[:upper:]' '[:lower:]' <<<"${severity}")" - echo " [INFO] Found ${id} in ${pkg} with severity ${severityLower}. Requires upgrade from ${vulnerableVersion} to ${fixedVersion}." - echo "| ${id} | ${pkg} | ${severityLower} | ${vulnerableVersion} -> ${fixedVersion} |" >>/tmp/report.md + echo " [INFO] Found ${id} in ${pkg} with severity ${severityLower}. Requires upgrade from ${vulnerableVersion} to ${fixedVersion}.${fixedSentence}" + echo "| ${id} | ${pkg} | ${severityLower} | ${vulnerableVersion} -> ${fixedVersion} | ${fixed} |" >>/tmp/report.md done <<<"$(jq --raw-output '.Results[0].Vulnerabilities[] | [ .VulnerabilityID, .PkgName, .Severity, .InstalledVersion, .FixedVersion ] | @tsv' <<<"${osVulns}")" if [ "${osVulnsFound}" == "false" ]; then @@ -51,24 +68,36 @@ for image in "${images[@]}"; do # Go vulnerabilities echo " [INFO] Checking for Go vulnerabilities" echo "### Go vulnerabilities" >>/tmp/report.md - entrypoint="$(crane config "${image}" | jq -r '.config.Entrypoint[0]')" crane export "${image}" - | tar -xf - -C /tmp "${entrypoint}" goVulns="$(govulncheck -format json -mode binary "/tmp${entrypoint}")" goVulnsFound=false + cat /tmp/image.tar | crane export - - | tar -xf - -C /tmp "${entrypoint}" + goVulnsLatest="$(govulncheck -format json -mode binary "/tmp${entrypoint}")" + rm -f /tmp/image.tar "/tmp${entrypoint}" while read -r id pkg vulnerableVersion fixedVersion; do if [ "${id}" == "" ]; then continue fi + # Check if it exists in the latest image + if [ "$(jq --raw-output "select(.finding.osv == \"${id}\")" <<<"${goVulnsLatest}")" == "" ]; then + fixed=":white_check_mark:" + fixedSentence=" This vulnerability is fixed by a rebuild." + else + fixed=":x:" + fixedSentence= + allVulnerabilitiesFixedByRebuild=false + fi + if [ "${goVulnsFound}" == "false" ]; then - echo "| Vulnerability | Package | Version |" >>/tmp/report.md - echo "| -- | -- | -- |" >>/tmp/report.md + echo "| Vulnerability | Package | Version | Fixed by rebuild |" >>/tmp/report.md + echo "| -- | -- | -- | -- |" >>/tmp/report.md goVulnsFound=true hasVulnerabilities=true fi - echo " [INFO] Found ${id} in ${pkg}. Requires upgrade from ${vulnerableVersion} to ${fixedVersion}." - echo "| ${id} | ${pkg} | ${vulnerableVersion} -> ${fixedVersion} |" >>/tmp/report.md + echo " [INFO] Found ${id} in ${pkg}. Requires upgrade from ${vulnerableVersion} to ${fixedVersion}.${fixedSentence}" + echo "| ${id} | ${pkg} | ${vulnerableVersion} -> ${fixedVersion} | ${fixed} |" >>/tmp/report.md done <<<"$(jq --raw-output 'select(.finding != null and .finding.fixed_version != null) | [ .finding.osv, .finding.trace[0].module, .finding.trace[0].version, .finding.fixed_version ] | @tsv' <<<"${goVulns}" | sort -u)" if [ "${goVulnsFound}" == "false" ]; then @@ -81,18 +110,48 @@ done issues="$(gh issue list --label release-vulnerabilities --json number)" if [ "$(jq length <<<"${issues}")" == "0" ]; then + assignees="$(dyff json OWNERS | jq -r '.approvers | join(",")')" + if [ "${hasVulnerabilities}" == "true" ]; then # create new issue echo "[INFO] Creating new issue" - gh issue create --label release-vulnerabilities --title "Vulnerabilities found in latest release" --body-file /tmp/report.md + gh issue create \ + --assignee "${assignees}" \ + --label release-vulnerabilities \ + --title "Vulnerabilities found in latest release ${RELEASE_TAG}" \ + --body-file /tmp/report.md + + issues="$(gh issue list --label release-vulnerabilities --json number)" + issueNumber="$(jq '.[0].number' <<<"${issues}")" fi else issueNumber="$(jq '.[0].number' <<<"${issues}")" if [ "${hasVulnerabilities}" == "true" ]; then # update issue echo "[INFO] Updating existing issue ${issueNumber}" - gh issue edit "${issueNumber}" --body-file /tmp/report.md + gh issue edit "${issueNumber}" \ + --assignee "${assignees}" \ + --body-file /tmp/report.md + else + gh issue close --reason "No vulnerabilities found in the latest release ${RELEASE_TAG}" + fi +fi + +# Create release if all vulnerabilities are fixable by a rebuild +if [ "${hasVulnerabilities}" == "true" ] && [ "${allVulnerabilitiesFixedByRebuild}" == "true" ]; then + nextTag="$(semver bump patch "${RELEASE_TAG}")" + + # check if tag already exists + if gh release view "${nextTag}" >/dev/null 2>&1; then + echo "[INFO] There is already a new tag ${nextTag} which seemingly was not yet released by a maintainer" + gh issue comment "${issueNumber}" --body "All existing vulnerabilities in ${RELEASE_TAG} can be fixed by a rebuild, but such a rebuild seemingly already exists as ${nextTag}. A maintainer must release this." else - gh issue close --reason "No vulnerabilities found in the latest release" + echo "[INFO] Triggering build of release ${nextTag} for branch ${RELEASE_BRANCH}" + gh workflow run release.yaml \ + --raw-field "git-ref=${RELEASE_BRANCH}" \ + --raw-field "tags=${RELEASE_TAG}" \ + --raw-field "release=${nextTag}" + + gh issue comment "${issueNumber}" --body "Triggered a release build in branch ${RELEASE_BRANCH} for ${RELEASE_TAG}. Please check whether this succeeded. A maintainer must release this." fi fi diff --git a/.github/workflows/report-release-vulnerabilities.yaml b/.github/workflows/report-release-vulnerabilities.yaml index dcf65447b..bd4eadc6f 100644 --- a/.github/workflows/report-release-vulnerabilities.yaml +++ b/.github/workflows/report-release-vulnerabilities.yaml @@ -4,7 +4,7 @@ name: Report release vulnerabilities on: schedule: - - cron: '0 0 * * *' + - cron: '15 3 * * *' # 3:15 am UTC = 15 minutes after base image build workflow_dispatch: {} jobs: report-vulnerabilities: @@ -20,8 +20,12 @@ jobs: check-latest: true - name: Install crane run: curl --location --silent "https://github.com/google/go-containerregistry/releases/download/$(curl -s https://api.github.com/repos/google/go-containerregistry/releases/latest | jq -r '.tag_name')/go-containerregistry_$(uname -s)_$(uname -m | sed -e 's/aarch64/arm64/').tar.gz" | sudo tar -xzf - -C /usr/local/bin crane + - name: Install dyff + run: curl --silent --location https://raw.githubusercontent.com/homeport/dyff/main/scripts/download-latest.sh | bash - name: Install Retry - run: curl --silent --location https://raw.githubusercontent.com/homeport/retry/main/hack/download.sh | bash + run: curl --location --silent https://raw.githubusercontent.com/homeport/retry/main/hack/download.sh | bash + - name: Install semver + run: go install gitlab.com/usvc/utils/semver/cmd/semver@latest - name: Install Trivy run: make install-trivy - name: Update Trivy database @@ -31,7 +35,27 @@ jobs: run: retry trivy image --download-db-only - name: Install govulncheck run: go install golang.org/x/vuln/cmd/govulncheck@latest - - name: Run vulnerability check + - name: Download latest release + id: download-latest-release env: GH_TOKEN: ${{ github.token }} + run: ./.github/download-latest-release.sh + - name: Checkout release branch + uses: actions/checkout@v4 + with: + path: /tmp/release-branch + ref: ${{ steps.download-latest-release.release-branch }} + - name: Install Go version of latest release + uses: actions/setup-go@v5 + with: + go-version: "${{ steps.download-latest-release.go-version }}.x" + cache: true + check-latest: true + - name: Report vulnerabilities + env: + GH_TOKEN: ${{ github.token }} + RELEASE_BRANCH: ${{ steps.download-latest-release.release-branch }} + RELEASE_TAG: ${{ steps.download-latest-release.release-tag }} + RELEASE_YAML: ${{ steps.download-latest-release.release-yaml }} + REPOSITORY: /tmp/release-branch run: ./.github/report-release-vulnerabilities.sh