diff --git a/.github/workflows/get-repo-config.yml b/.github/workflows/get-repo-config.yml new file mode 100644 index 0000000..8439187 --- /dev/null +++ b/.github/workflows/get-repo-config.yml @@ -0,0 +1,145 @@ +name: Get Repo Config and Image +on: + workflow_call: + inputs: + registry: + required: false + type: string + default: ghcr.io + namespace: + required: false + type: string + default: nhsdigital/eps-devcontainers + owner: + required: false + type: string + default: NHSDigital + verify_published_from_main_image: + required: false + type: boolean + default: true + predicate_type: + required: false + type: string + default: https://slsa.dev/provenance/v1 + outputs: + tag_format: + description: The tag format to be used for releases, as defined in .github/config/settings.yml + value: ${{ jobs.get_config_values.outputs.tag_format }} + devcontainer_image: + description: The devcontainer image name as defined in .devcontainer/devcontainer.json + value: ${{ jobs.get_config_values.outputs.devcontainer_image }} + devcontainer_version: + description: The devcontainer image version as defined in .devcontainer/devcontainer.json + value: ${{ jobs.get_config_values.outputs.devcontainer_version }} + pinned_image: + description: Fully-qualified digest-pinned image reference + value: ${{ jobs.verify_attestation.outputs.pinned_image }} + resolved_digest: + description: Resolved digest for the supplied image reference + value: ${{ jobs.verify_attestation.outputs.resolved_digest }} + +jobs: + get_config_values: + runs-on: ubuntu-22.04 + outputs: + tag_format: ${{ steps.load-config.outputs.TAG_FORMAT }} + devcontainer_version: ${{ steps.load-config.outputs.DEVCONTAINER_VERSION }} + devcontainer_image: ${{ steps.load-config.outputs.DEVCONTAINER_IMAGE }} + runtime_docker_image: ${{ steps.load-config.outputs.RUNTIME_DOCKER_IMAGE }} + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ env.BRANCH_NAME }} + fetch-depth: 0 + + - name: Load config value + id: load-config + run: | + TAG_FORMAT=$(yq '.TAG_FORMAT' .github/config/settings.yml) + DEVCONTAINER_IMAGE=$(jq -r '.build.args.IMAGE_NAME' .devcontainer/devcontainer.json) + DEVCONTAINER_VERSION=$(jq -r '.build.args.IMAGE_VERSION' .devcontainer/devcontainer.json) + RUNTIME_DOCKER_IMAGE="${DEVCONTAINER_IMAGE}:githubactions-${DEVCONTAINER_VERSION}" + { + echo "TAG_FORMAT=$TAG_FORMAT" + echo "DEVCONTAINER_IMAGE=$DEVCONTAINER_IMAGE" + echo "DEVCONTAINER_VERSION=$DEVCONTAINER_VERSION" + echo "RUNTIME_DOCKER_IMAGE=$RUNTIME_DOCKER_IMAGE" + } >> "$GITHUB_OUTPUT" + + verify_attestation: + runs-on: ubuntu-22.04 + needs: get_config_values + permissions: + contents: read + packages: read + attestations: read + outputs: + pinned_image: ${{ steps.resolve.outputs.pinned_image }} + resolved_digest: ${{ steps.resolve.outputs.resolved_digest }} + steps: + - name: Login to github container registry + if: startsWith(inputs.registry, 'ghcr.io') + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Resolve digest + id: resolve + shell: bash + env: + RUNTIME_DOCKER_IMAGE: ${{ needs.get_config_values.outputs.runtime_docker_image }} + REGISTRY: ${{ inputs.registry }} + NAMESPACE: ${{ inputs.namespace }} + run: | + set -euo pipefail + + if [[ "$RUNTIME_DOCKER_IMAGE" == *"/"* ]]; then + IMAGE_REF="$RUNTIME_DOCKER_IMAGE" + else + IMAGE_REF="${REGISTRY}/${NAMESPACE}/${RUNTIME_DOCKER_IMAGE}" + fi + + if [[ "$IMAGE_REF" == *@sha256:* ]]; then + IMAGE_BASE="${IMAGE_REF%@*}" + RESOLVED_DIGEST="${IMAGE_REF#*@}" + else + RESOLVED_DIGEST="$(docker buildx imagetools inspect "$IMAGE_REF" | awk '/^Digest:/ {print $2; exit}')" + IMAGE_BASE="${IMAGE_REF%:*}" + fi + + if [[ -z "$RESOLVED_DIGEST" ]]; then + echo "Could not resolve digest for image: $IMAGE_REF" >&2 + exit 1 + fi + + PINNED_IMAGE="${IMAGE_BASE}@${RESOLVED_DIGEST}" + echo "resolved_digest=${RESOLVED_DIGEST}" >> "$GITHUB_OUTPUT" + echo "pinned_image=${PINNED_IMAGE}" >> "$GITHUB_OUTPUT" + echo "Resolved image reference: ${IMAGE_REF}" + echo "Resolved digest: ${RESOLVED_DIGEST}" + echo "Resolved image reference: ${PINNED_IMAGE}" + + - name: Verify attestation + shell: bash + env: + GH_TOKEN: ${{ github.token }} + OWNER: ${{ inputs.owner }} + VERIFY_PUBLISHED_FROM_MAIN_IMAGE: ${{ inputs.verify_published_from_main_image }} + PREDICATE_TYPE: ${{ inputs.predicate_type }} + PINNED_IMAGE: ${{ steps.resolve.outputs.pinned_image }} + run: | + set -euo pipefail + + args=("oci://${PINNED_IMAGE}" "--owner" "$OWNER" "--predicate-type" "$PREDICATE_TYPE") + + if [[ "$VERIFY_PUBLISHED_FROM_MAIN_IMAGE" == "true" ]]; then + args+=("--source-ref" "refs/heads/main") + fi + + + GH_FORCE_TTY=120 gh attestation verify "${args[@]}" 2>&1 + echo "Verified attestation for ${PINNED_IMAGE}" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index cdb4ec7..354eab6 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,4 +1,4 @@ -name: pr +name: Pull Request on: pull_request: @@ -17,35 +17,16 @@ jobs: pr_title_format_check: uses: ./.github/workflows/pr_title_check.yml get_config_values: - runs-on: ubuntu-22.04 - outputs: - tag_format: ${{ steps.load-config.outputs.TAG_FORMAT }} - devcontainer_version: ${{ steps.load-config.outputs.DEVCONTAINER_VERSION }} - devcontainer_image: ${{ steps.load-config.outputs.DEVCONTAINER_IMAGE }} - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - - name: Load config value - id: load-config - run: | - TAG_FORMAT=$(yq '.TAG_FORMAT' .github/config/settings.yml) - DEVCONTAINER_IMAGE=$(jq -r '.build.args.IMAGE_NAME' .devcontainer/devcontainer.json) - DEVCONTAINER_VERSION=$(jq -r '.build.args.IMAGE_VERSION' .devcontainer/devcontainer.json) - { - echo "TAG_FORMAT=$TAG_FORMAT" - echo "DEVCONTAINER_IMAGE=$DEVCONTAINER_IMAGE" - echo "DEVCONTAINER_VERSION=$DEVCONTAINER_VERSION" - } >> "$GITHUB_OUTPUT" + uses: ./.github/workflows/get-repo-config.yml quality_checks: uses: ./.github/workflows/quality-checks-devcontainer.yml needs: [get_config_values] with: - runtime_docker_image: "${{ needs.get_config_values.outputs.devcontainer_image }}:githubactions-${{ needs.get_config_values.outputs.devcontainer_version }}" + pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} tag_release: - needs: [quality_checks, get_config_values] + needs: get_config_values uses: ./.github/workflows/tag-release-devcontainer.yml permissions: contents: read @@ -53,7 +34,7 @@ jobs: attestations: read with: dry_run: true - runtime_docker_image: "${{ needs.get_config_values.outputs.devcontainer_image }}:githubactions-${{ needs.get_config_values.outputs.devcontainer_version }}" + pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} branch_name: ${{ github.event.pull_request.head.ref }} tag_format: ${{ needs.get_config_values.outputs.tag_format }} verify_published_from_main_image: false diff --git a/.github/workflows/quality-checks-devcontainer.yml b/.github/workflows/quality-checks-devcontainer.yml index 6f8afad..4314c61 100644 --- a/.github/workflows/quality-checks-devcontainer.yml +++ b/.github/workflows/quality-checks-devcontainer.yml @@ -21,21 +21,15 @@ on: description: comma separated list of docker image references to scan when docker scanning is enabled. default: "" required: false - runtime_docker_image: + pinned_image: type: string required: true jobs: - verify_attestation: - uses: ./.github/workflows/verify-attestation.yml - with: - runtime_docker_image: "${{ inputs.runtime_docker_image }}" - verify_published_from_main_image: false quality_checks: runs-on: ubuntu-22.04 - needs: verify_attestation container: - image: ${{ needs.verify_attestation.outputs.pinned_image }} + image: ${{ inputs.pinned_image }} options: --user 1001:1001 --group-add 128 defaults: run: @@ -213,9 +207,8 @@ jobs: get_docker_images_to_scan: runs-on: ubuntu-22.04 - needs: verify_attestation container: - image: ${{ needs.verify_attestation.outputs.pinned_image }} + image: ${{ inputs.pinned_image }} options: --user 1001:1001 --group-add 128 defaults: run: @@ -283,9 +276,9 @@ jobs: docker_vulnerability_scan: runs-on: ubuntu-22.04 - needs: [get_docker_images_to_scan, verify_attestation] + needs: get_docker_images_to_scan container: - image: ${{ needs.verify_attestation.outputs.pinned_image }} + image: ${{ inputs.pinned_image }} options: --user 1001:1001 --group-add 128 defaults: run: @@ -326,9 +319,8 @@ jobs: IaC-validation: runs-on: ubuntu-22.04 - needs: verify_attestation container: - image: ${{ needs.verify_attestation.outputs.pinned_image }} + image: ${{ inputs.pinned_image }} options: --user 1001:1001 --group-add 128 defaults: run: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5aeaab9..66172a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,31 +9,12 @@ env: jobs: get_config_values: - runs-on: ubuntu-22.04 - outputs: - tag_format: ${{ steps.load-config.outputs.TAG_FORMAT }} - devcontainer_version: ${{ steps.load-config.outputs.DEVCONTAINER_VERSION }} - devcontainer_image: ${{ steps.load-config.outputs.DEVCONTAINER_IMAGE }} - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - - name: Load config value - id: load-config - run: | - TAG_FORMAT=$(yq '.TAG_FORMAT' .github/config/settings.yml) - DEVCONTAINER_IMAGE=$(jq -r '.build.args.IMAGE_NAME' .devcontainer/devcontainer.json) - DEVCONTAINER_VERSION=$(jq -r '.build.args.IMAGE_VERSION' .devcontainer/devcontainer.json) - { - echo "TAG_FORMAT=$TAG_FORMAT" - echo "DEVCONTAINER_IMAGE=$DEVCONTAINER_IMAGE" - echo "DEVCONTAINER_VERSION=$DEVCONTAINER_VERSION" - } >> "$GITHUB_OUTPUT" + uses: ./.github/workflows/get-repo-config.yml quality_checks: needs: [get_config_values] uses: ./.github/workflows/quality-checks-devcontainer.yml with: - runtime_docker_image: "${{ needs.get_config_values.outputs.devcontainer_image }}:githubactions-${{ needs.get_config_values.outputs.devcontainer_version }}" + pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} tag_release: @@ -41,7 +22,7 @@ jobs: uses: ./.github/workflows/tag-release-devcontainer.yml with: dry_run: false - runtime_docker_image: "${{ needs.get_config_values.outputs.devcontainer_image }}:githubactions-${{ needs.get_config_values.outputs.devcontainer_version }}" + pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} branch_name: main tag_format: ${{ needs.get_config_values.outputs.tag_format }} verify_published_from_main_image: true diff --git a/.github/workflows/tag-release-devcontainer.yml b/.github/workflows/tag-release-devcontainer.yml index ba93091..7647e7b 100644 --- a/.github/workflows/tag-release-devcontainer.yml +++ b/.github/workflows/tag-release-devcontainer.yml @@ -11,7 +11,7 @@ on: description: "The branch name to base the release on" required: true type: string - runtime_docker_image: + pinned_image: type: string required: true publish_packages: @@ -62,16 +62,10 @@ on: required: false description: "NPM token to publish packages" jobs: - verify_attestation: - uses: ./.github/workflows/verify-attestation.yml - with: - runtime_docker_image: "${{ inputs.runtime_docker_image }}" - verify_published_from_main_image: ${{ inputs.verify_published_from_main_image }} tag_release: runs-on: ubuntu-22.04 - needs: verify_attestation container: - image: ${{ needs.verify_attestation.outputs.pinned_image }} + image: ${{ inputs.pinned_image }} options: --user 1001:1001 --group-add 128 defaults: run: diff --git a/.github/workflows/verify-attestation.yml b/.github/workflows/verify-attestation.yml deleted file mode 100644 index a996e34..0000000 --- a/.github/workflows/verify-attestation.yml +++ /dev/null @@ -1,111 +0,0 @@ -name: Verify image digest and attestation -"on": - workflow_call: - inputs: - runtime_docker_image: - required: true - type: string - description: Image reference as name:tag (for example node_24_python_3_12:v1.2.3) or fully qualified image ref - registry: - required: false - type: string - default: ghcr.io - namespace: - required: false - type: string - default: nhsdigital/eps-devcontainers - owner: - required: false - type: string - default: NHSDigital - verify_published_from_main_image: - required: false - type: boolean - default: true - predicate_type: - required: false - type: string - default: https://slsa.dev/provenance/v1 - outputs: - pinned_image: - description: Fully-qualified digest-pinned image reference - value: ${{ jobs.verify_attestation.outputs.pinned_image }} - resolved_digest: - description: Resolved digest for the supplied image reference - value: ${{ jobs.verify_attestation.outputs.resolved_digest }} - -jobs: - verify_attestation: - runs-on: ubuntu-22.04 - permissions: - contents: read - packages: read - attestations: read - outputs: - pinned_image: ${{ steps.resolve.outputs.pinned_image }} - resolved_digest: ${{ steps.resolve.outputs.resolved_digest }} - steps: - - name: Login to github container registry - if: startsWith(inputs.registry, 'ghcr.io') - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Resolve digest - id: resolve - shell: bash - env: - RUNTIME_DOCKER_IMAGE: ${{ inputs.runtime_docker_image }} - REGISTRY: ${{ inputs.registry }} - NAMESPACE: ${{ inputs.namespace }} - run: | - set -euo pipefail - - if [[ "$RUNTIME_DOCKER_IMAGE" == *"/"* ]]; then - IMAGE_REF="$RUNTIME_DOCKER_IMAGE" - else - IMAGE_REF="${REGISTRY}/${NAMESPACE}/${RUNTIME_DOCKER_IMAGE}" - fi - - if [[ "$IMAGE_REF" == *@sha256:* ]]; then - IMAGE_BASE="${IMAGE_REF%@*}" - RESOLVED_DIGEST="${IMAGE_REF#*@}" - else - RESOLVED_DIGEST="$(docker buildx imagetools inspect "$IMAGE_REF" | awk '/^Digest:/ {print $2; exit}')" - IMAGE_BASE="${IMAGE_REF%:*}" - fi - - if [[ -z "$RESOLVED_DIGEST" ]]; then - echo "Could not resolve digest for image: $IMAGE_REF" >&2 - exit 1 - fi - - PINNED_IMAGE="${IMAGE_BASE}@${RESOLVED_DIGEST}" - echo "resolved_digest=${RESOLVED_DIGEST}" >> "$GITHUB_OUTPUT" - echo "pinned_image=${PINNED_IMAGE}" >> "$GITHUB_OUTPUT" - echo "Resolved image reference: ${IMAGE_REF}" - echo "Resolved digest: ${RESOLVED_DIGEST}" - echo "Resolved image reference: ${PINNED_IMAGE}" - - - name: Verify attestation - shell: bash - env: - GH_TOKEN: ${{ github.token }} - OWNER: ${{ inputs.owner }} - VERIFY_PUBLISHED_FROM_MAIN_IMAGE: ${{ inputs.verify_published_from_main_image }} - PREDICATE_TYPE: ${{ inputs.predicate_type }} - PINNED_IMAGE: ${{ steps.resolve.outputs.pinned_image }} - run: | - set -euo pipefail - - args=("oci://${PINNED_IMAGE}" "--owner" "$OWNER" "--predicate-type" "$PREDICATE_TYPE") - - if [[ "$VERIFY_PUBLISHED_FROM_MAIN_IMAGE" == "true" ]]; then - args+=("--source-ref" "refs/heads/main") - fi - - - GH_FORCE_TTY=120 gh attestation verify "${args[@]}" 2>&1 - echo "Verified attestation for ${PINNED_IMAGE}"