From 9f75b39be4409406fdc5bb2cd77438bde848d39e Mon Sep 17 00:00:00 2001 From: Kareem Hepburn Date: Tue, 7 Jan 2025 12:50:02 -0600 Subject: [PATCH] refactor(WIP): define reusable docker content trust workflow --- .actrc | 9 + .github/workflows/cd.yml | 77 ++++++ .github/workflows/ci.yml | 11 +- ...eusable.build-container-image-artifact.yml | 249 ------------------ .../reusable.build-container-image.yml | 56 +--- .../reusable.docker-content-trust.yml | 129 +++++++++ .gitignore | 7 +- 7 files changed, 226 insertions(+), 312 deletions(-) create mode 100644 .actrc create mode 100644 .github/workflows/cd.yml delete mode 100644 .github/workflows/reusable.build-container-image-artifact.yml create mode 100644 .github/workflows/reusable.docker-content-trust.yml diff --git a/.actrc b/.actrc new file mode 100644 index 0000000000..2a40ed97ca --- /dev/null +++ b/.actrc @@ -0,0 +1,9 @@ +--rm +--reuse +--container-options=--privileged +--pull=false +--platform=ubuntu-latest=catthehacker/ubuntu:custom-20.04 +--container-architecture=linux/amd64 +--workflows=./.github/workflows +--secret-file=./.act.secrets +--eventpath=./.act.event.json diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000000..424109a582 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,77 @@ +--- +# Copyright 2024 Specter Ops, Inc. +# +# Licensed under the Apache License, Version 2.0 +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +name: Continuous Deployment (CD) + +on: + pull_request: + branches: + - main + - develop + types: + - assigned + - opened + - synchronize + - reopened + - closed + push: + branches: + - main + - develop + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + - 'v[0-9]+.[0-9]+.[0-9]+rc[0-9]+' + +jobs: + bloodhound-container-image: + name: Build and Publish BloodHound Container Image + uses: ./.github/workflows/reusable.build-container-image.yml + with: + container_image_repository_name: docker.io/specterops/bloodhound + image_sbom: true + image_provenance: mode=max + build_target: bloodhound + build_outputs: type=image,push=true + dockerfile: dockerfiles/bloodhound.Dockerfile + image_cache_from: |- + type=registry,ref=docker.io/specterops/bloodhound:buildcache + type=registry,ref=ghcr.io/specterops/bloodhound:buildcache + image_cache_to: |- + type=registry,ref=docker.io/specterops/bloodhound:buildcache,mode=max + type=registry,ref=ghcr.io/specterops/bloodhound:buildcache,mode=max + secrets: + dockerhub_account: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} + ghcr_account: ${{ github.actor }} + ghcr_token: ${{ secrets.GITHUB_TOKEN }} + gh_access_token: ${{ secrets.GITHUB_TOKEN }} + + docker-content-trust-sign-image: + needs: bloodhound-container-image + name: Sign Docker Image using Docker Content Trust + uses: ./.github/workflows/reusable.dockder-content-trust.yml + with: + dockerhub_image_reference: ${{ needs.bloodhound-container-image.outputs.image_reference }} + secrets: + dockerhub_account: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} + ghcr_account: ${{ github.actor }} + ghcr_token: ${{ secrets.GITHUB_TOKEN }} + gh_access_token: ${{ secrets.GITHUB_TOKEN }} + docker_content_trust_repository_key_id: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_KEY_ID }} + docker_content_trust_repository_passphrase: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }} + docker_content_trust_repository_key: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_KEY }} + docker_content_trust_repository_public_key: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PUBLIC_KEY }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a9640af74..d94512c4b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,9 +55,9 @@ jobs: with: container_image_repository_name: docker.io/specterops/bloodhound build_target: bloodhound - image_sbom: true - image_provenance: mode=max - build_outputs: type=image,push=true + image_sbom: false + image_provenance: false + build_outputs: type=image,push=false dockerfile: dockerfiles/bloodhound.Dockerfile image_cache_from: |- type=registry,ref=docker.io/specterops/bloodhound:buildcache @@ -65,17 +65,12 @@ jobs: image_cache_to: |- type=registry,ref=docker.io/specterops/bloodhound:buildcache,mode=max type=registry,ref=ghcr.io/specterops/bloodhound:buildcache,mode=max - docker_content_trust_server: https://notary.docker.io secrets: dockerhub_account: ${{ secrets.DOCKERHUB_USERNAME }} dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} ghcr_account: ${{ github.actor }} ghcr_token: ${{ secrets.GITHUB_TOKEN }} gh_access_token: ${{ secrets.GITHUB_TOKEN }} - docker_content_trust_repository_key_id: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_KEY_ID }} - docker_content_trust_repository_passphrase: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }} - docker_content_trust_repository_key: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_KEY }} - docker_content_trust_repository_public_key: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PUBLIC_KEY }} # static-code-analysis: # name: Static Code Analysis diff --git a/.github/workflows/reusable.build-container-image-artifact.yml b/.github/workflows/reusable.build-container-image-artifact.yml deleted file mode 100644 index 932018770b..0000000000 --- a/.github/workflows/reusable.build-container-image-artifact.yml +++ /dev/null @@ -1,249 +0,0 @@ -# Copyright 2024 Specter Ops, Inc. -# -# Licensed under the Apache License, Version 2.0 -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 - -name: Container Image Builder Pipeline -run-name: Building Container Image for ${{ inputs.image_repository }} by @${{ github.actor }} on ${{ github.ref_name }} - -on: - workflow_call: - inputs: - build_context: - type: string - description: |- - Specifies the build context directory for Docker. - Choose your build context carefully to: - - Control which files are available during build - - Optimize build performance by limiting included files - - Ensure security by excluding sensitive files - - Support multi-stage builds with specific contexts - image_repository: - type: string - required: true - description: |- - The name of the repository where the container image will be stored in the container registry. - This repository name uniquely identifies the image within the registry. - Combine this with the 'container_registry' input to form the full URL for the image. - - Example: 'my-app' for a repository named 'my-app' in the container registry. - dockerfile: - type: string - description: |- - The name of the Dockerfile used for building the container image. - If not specified, it defaults to 'Dockerfile' in the repository root. - - Example: 'Dockerfile.prod' for a production-specific Dockerfile. - build_target: - type: string - description: |- - The build stage target for multi-stage Docker builds, if applicable. - Specify this if your Dockerfile has multiple stages, and you want to - build a specific one. - - Example: 'production' for a multi-stage Dockerfile with a 'production' - stage. - build_args: - type: string - description: |- - Build-time variables that customize the container build process. - Use build args to: - - Inject version information at build time - - Configure build-specific settings without modifying Dockerfile - - Support different configurations for dev/staging/prod - - Pass secrets safely during build (using --secret) - - Predefined values may already be present, and any inputs provided here will be appended. - - Example: 'VERSION=${GITHUB_SHA}' to embed git commit information - build_contexts: - type: string - description: |- - Additional named build contexts for multi-stage builds. - Use multiple build contexts when you need to: - - Separate build dependencies from runtime dependencies - - Include files from different locations without copying - - Optimize layer caching for different build stages - - Support complex multi-stage build patterns - - In Dockerfile, access these contexts using FROM name or --from=name. - Note: These contexts override same-named stages in the Dockerfile. - - Example: 'deps=/path/to/dependencies,assets=/path/to/static-files' - build_outputs: - type: string - required: true - description: |- - ... - image_provenance: - type: string - description: |- - Controls inclusion of SLSA provenance in image metadata. - Enable provenance when you need to: - - Meet supply chain security requirements - - Provide audit trails for compliance - - Verify image build authenticity - - Support automated security policy enforcement - - Provenance data includes build source, tooling, and environment details. - image_sbom: - type: string - default: "false" - description: |- - Controls generation of Software Bill of Materials (SBOM) for the image. - Enable SBOM generation to: - - Track and audit all software dependencies - - Identify and respond to security vulnerabilities - - Meet compliance requirements for software transparency - - Support automated vulnerability scanning - - Enable dependency analysis and lifecycle management - image_flavor: - type: string - description: |- - Additional image flavor information or tags. - image_cache_from: - type: string - description: |- - The source image repository from which to cache layers during the build. - This can help improve build speed by reusing layers from a previously built image. - - Example: 'docker.io/my-app:cache' to cache from a specific image. - image_cache_to: - type: string - description: |- - The destination image cache settings to optimize the caching strategy during the build. - This input specifies where to store cached layers and how they are scoped. - Values provided here will be appended to any default cache settings. - - Predefined values may already be present, and any inputs provided here will be appended. - - Example: "type=gha,mode=max,scope=\$\{\{ github.workflow \}\}" - build_output_tar_dir: - type: string - description: |- - The directory path where the tar file of the built image will be saved, - to be used as an artifact upload location with the `actions/upload-artifact@v4` GitHub Action. - This tar archive can then be retrieved from the workflow artifacts for further use or distribution. - default: "/tmp" - timeout_minutes: - description: Job timeout configuration in minutes - type: number - default: 30 - push_image: - type: boolean - default: false - description: |- - Whether to push the built container image to the registry after building. - Set this to 'true' if you want to automatically push the image. - - Example: 'true' to push the image to the registry, 'false' to skip pushing. - build_automation_ref: - type: string - description: |- - When the workflow is reused in another repo, we have to import the - .github/actions directory for repository. - secrets: - dockerhub_account: - required: true - dockerhub_token: - required: true - ghcr_account: - required: true - ghcr_token: - required: true - gh_access_token: - required: true - outputs: - image_reference: - value: ${{ jobs.build-container-image.outputs.image_reference }} - image_name: - value: ${{ jobs.build-container-image.outputs.image_name }} - image_tar_path: - value: ${{ jobs.build-container-image.outputs.image_tar_path }} - image_tar_artifact_name: - value: ${{ jobs.build-container-image.outputs.image_tar_artifact_name }} - -jobs: - build-container-image: - name: Build and Package ${{ inputs.image_repository }} Container - runs-on: ubuntu-latest - timeout-minutes: ${{ inputs.timeout_minutes }} - outputs: - image_reference: ${{ steps.container-image-metadata.outputs.image_reference }} - image_name: ${{ steps.container-image-metadata.outputs.image_name }} - image_tar_path: ${{ inputs.build_output_tar_dir }}/${{ steps.container-image-metadata.outputs.version }}.tar - image_tar_artifact_name: ${{ steps.container-image-metadata.outputs.version }} - steps: - - name: Checkout Source Code Repository - uses: actions/checkout@v4 - - - if: ${{ github.repository != 'SpecterOps/BloodHound' }} - name: Checkout Build Automation Workflows - uses: actions/checkout@v4 - with: - clean: false - repository: SpecterOps/BloodHound - ref: main - token: ${{ secrets.gh_access_token }} - sparse-checkout-cone-mode: false - sparse-checkout: |- - .github/workflows - .github/actions - - - uses: docker/login-action@v3 - name: Authenticate with DockerHub Registry - with: - registry: docker.io - username: ${{ secrets.dockerhub_account }} - password: ${{ secrets.dockerhub_token }} - - - uses: docker/login-action@v3 - name: Authenticate with GitHub Container Registry - with: - registry: docker.io - username: ${{ secrets.ghcr_account }} - password: ${{ secrets.ghcr_token }} - - - uses: ./.github/actions/container-image-metadata - id: container-image-metadata - with: - image_repository: ${{ inputs.image_repository }} - image_flavor: ${{ inputs.image_flavor }} - - - uses: ./.github/actions/build-container-image - id: build-container-image - with: - build_args: ${{ inputs.build_args }} - build_context: ${{ inputs.build_context }} - build_contexts: ${{ inputs.build_contexts }} - build_target: ${{ inputs.build_target }} - build_outputs: ${{ inputs.build_outputs }} - cache_from: ${{ inputs.image_cache_from }} - cache_to: ${{ inputs.image_cache_to }} - dockerfile: ${{ inputs.dockerfile }} - image_labels: ${{ steps.container-image-metadata.outputs.labels }} - image_metadata_json: ${{ steps.container-image-metadata.outputs.json }} - image_provenance: ${{ inputs.image_provenance }} - image_sbom: ${{ inputs.image_sbom }} - image_tags: ${{ steps.container-image-metadata.outputs.tags }} - push_image: ${{ inputs.push_image }} - - # - if: inputs.build_output_tar_dir != '' - # name: Archive Container Image for Downstream Jobs - # uses: actions/upload-artifact@v4 - # with: - # if-no-files-found: error - # retention-days: 1 - # name: ${{ steps.container-image-metadata.outputs.version }} - # path: ${{ inputs.build_output_tar_dir }}/${{ steps.container-image-metadata.outputs.version }}.tar diff --git a/.github/workflows/reusable.build-container-image.yml b/.github/workflows/reusable.build-container-image.yml index 519dde9916..6d229fd1c9 100644 --- a/.github/workflows/reusable.build-container-image.yml +++ b/.github/workflows/reusable.build-container-image.yml @@ -148,9 +148,6 @@ on: Set this to 'true' if you want to automatically push the image. Example: 'true' to push the image to the registry, 'false' to skip pushing. - docker_content_trust_server: - type: string - required: true secrets: dockerhub_account: required: true @@ -162,14 +159,6 @@ on: required: true gh_access_token: required: true - docker_content_trust_repository_key_id: - required: true - docker_content_trust_repository_passphrase: - required: true - docker_content_trust_repository_key: - required: true - docker_content_trust_repository_public_key: - required: true outputs: image_reference: value: ${{ jobs.build-container-image.outputs.image_reference }} @@ -208,8 +197,8 @@ jobs: - uses: ./.github/actions/build-container-image id: build-container-image - env: - DOCKER_CONTENT_TRUST: 1 + # env: + # DOCKER_CONTENT_TRUST: 1 with: dockerhub_account: ${{ secrets.dockerhub_account }} dockerhub_token: ${{ secrets.dockerhub_token }} @@ -234,44 +223,3 @@ jobs: image_provenance: ${{ inputs.image_provenance }} image_sbom: ${{ inputs.image_sbom }} image_tags: ${{ steps.container-image-metadata.outputs.tags }} - - - name: Pull Image - shell: sh - run: |- - docker pull ${{ steps.container-image-metadata.outputs.image_reference }} - - - name: Sign Image - env: - DOCKER_CONTENT_TRUST: "1" - DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.docker_content_trust_repository_passphrase }} - DOCKER_CONTENT_TRUST_SERVER: ${{ inputs.docker_content_trust_server }} - shell: bash - run: |- - echo "::group::Loading Key" - mkdir -p ~/.docker/trust/private/ - echo "${{ secrets.docker_content_trust_repository_key }}" >> ~/.docker/trust/private/${{ secrets.docker_content_trust_repository_key_id }}.key - chmod 600 ~/.docker/trust/private/${{ secrets.docker_content_trust_repository_key_id }}.key - docker trust key load ~/.docker/trust/private/${{ secrets.docker_content_trust_repository_key_id }}.key - echo "::groupend::" - echo "::group::Signing Image" - docker trust sign --local ${{ steps.container-image-metadata.outputs.image_reference }} - echo "::groupend::" - - - name: Inspect Signed Image - env: - DOCKER_CONTENT_TRUST: "1" - DOCKER_CONTENT_TRUST_SERVER: ${{ inputs.docker_content_trust_server }} - run: |- - docker trust inspect --pretty "${{ steps.container-image-metadata.outputs.image_reference }}" - - - name: Remove Trust Key - shell: sh - run: |- - echo "::group::Cleaning Up Trust Keys" - echo "::notice::Removing Trust Key" - rm -v ~/.docker/trust/private/${{ secrets.docker_content_trust_repository_key_id }}.key - echo "::notice::Trust Key Removed" - echo "::notice::Removing Trust Key Directory" - rm -rvf ~/.docker/trust/private/ - echo "::notice::Trust Key Directory Removed" - echo "::groupend::" diff --git a/.github/workflows/reusable.docker-content-trust.yml b/.github/workflows/reusable.docker-content-trust.yml new file mode 100644 index 0000000000..0f2da7fe54 --- /dev/null +++ b/.github/workflows/reusable.docker-content-trust.yml @@ -0,0 +1,129 @@ +--- +# Copyright 2024 Specter Ops, Inc. +# +# Licensed under the Apache License, Version 2.0 +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +name: Docker Content Trust +run-name: Running Docker Content Trust for ${{ inputs.dockerhub_image_reference }} by @${{ github.actor }} on ${{ github.ref_name }} + +on: + workflow_call: + inputs: + dockerhub_image_reference: + type: string + required: true + description: |- + TBD ... + docker_content_trust_server: + type: string + default: https://notary.docker.io + secrets: + gh_access_token: + required: true + dockerhub_account: + required: true + dockerhub_token: + required: true + docker_content_trust_repository_key_id: + required: true + docker_content_trust_repository_passphrase: + required: true + docker_content_trust_repository_key: + required: true + docker_content_trust_repository_public_key: + required: true + +jobs: + sign-image: + name: Sign Image + runs-on: ubuntu-latest + timeout-minutes: ${{ inputs.timeout_minutes }} + steps: + - if: ${{ github.repository != 'SpecterOps/BloodHound' }} + name: Checkout Reusable Workflows and Composite Actions + uses: actions/checkout@v4 + with: + clean: false + repository: SpecterOps/BloodHound + ref: main + token: ${{ secrets.gh_access_token }} + sparse-checkout-cone-mode: false + sparse-checkout: |- + .github/actions + .github/workflows + + - uses: docker/login-action@v3 + name: Authenticate with DockerHub Registry + with: + registry: docker.io + username: ${{ secrets.dockerhub_account }} + password: ${{ secrets.dockerhub_token }} + + - name: Prepare Docker Content Trust Credentials + env: + DOCKER_CONTENT_TRUST: "1" + DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.docker_content_trust_repository_passphrase }} + DOCKER_CONTENT_TRUST_SERVER: ${{ inputs.docker_content_trust_server }} + shell: bash + run: |- + set eu; + echo "::group::Loading Docker Content Trust Key" + echo "::notice title=Ensure .docker/trust/private/ directory exists" + mkdir -p ~/.docker/trust/private/ + echo "::notice title=Writing docker content trust key" + echo "${{ secrets.docker_content_trust_repository_key }}" >> ~/.docker/trust/private/${{ secrets.docker_content_trust_repository_key_id }}.key + echo "::notice title=Set appropriate file permissions for the docker content trust key" + chmod 600 ~/.docker/trust/private/${{ secrets.docker_content_trust_repository_key_id }}.key + echo "::notice title=Load docker content trust key" + docker trust key load ~/.docker/trust/private/${{ secrets.docker_content_trust_repository_key_id }}.key + echo "::groupend::" + + - name: Pull Image + shell: bash + run: |- + set eu; + echo "::notice title=Pulling Image::${{ inputs.dockerhub_image_reference }}" + docker pull ${{ inputs.dockerhub_image_reference }} + + - name: Signing Image + env: + DOCKER_CONTENT_TRUST: "1" + DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.docker_content_trust_repository_passphrase }} + DOCKER_CONTENT_TRUST_SERVER: ${{ inputs.docker_content_trust_server }} + shell: bash + run: |- + set eu; + docker trust sign --local ${{ inputs.dockerhub_image_reference }} + + - name: Inspect Signed Image + env: + DOCKER_CONTENT_TRUST: "1" + DOCKER_CONTENT_TRUST_SERVER: ${{ inputs.docker_content_trust_server }} + run: |- + set eu; + echo "::notice title=Inspect Docker Content Trust Signed Image" + docker trust inspect --pretty "${{ steps.container-image-metadata.outputs.dockerhub_image_reference }}" + + - name: Remove Prepared Docker Content Trust Credentials + if: always() + shell: sh + run: |- + set eu; + echo "::group::Cleaning Up Trust Keys" + echo "::notice title=Removing Trust Key" + rm -v ~/.docker/trust/private/${{ secrets.docker_content_trust_repository_key_id }}.key + echo "::notice title=Removing Trust Key Directory" + rm -rvf ~/.docker/trust/private/ + echo "::groupend::" diff --git a/.gitignore b/.gitignore index ca0085843f..714eca970f 100644 --- a/.gitignore +++ b/.gitignore @@ -449,4 +449,9 @@ local-harnesses/* !local-harnesses/*.template # folder used by antlr4 VSCode extension -.antlr \ No newline at end of file +.antlr + +# act related files +.act* + +!.actrc