From a64227c424a0c322358847ce58c18937f206455b Mon Sep 17 00:00:00 2001 From: Christoph Hamsen Date: Thu, 1 Feb 2024 13:50:23 +0100 Subject: [PATCH] ci: add main ci jobs --- .github/actions/build/action.yml | 173 +++++++++ .github/actions/context/action.yaml | 153 ++++++++ .github/actions/grype/action.yaml | 49 +++ .github/actions/k3s-cluster/action.yaml | 74 ++++ .../actions/k8s-version-config/action.yaml | 25 ++ .github/actions/trivy-config/action.yaml | 53 +++ .github/actions/trivy-image/action.yaml | 54 +++ .github/workflows/.reusable-build.yml | 99 +++++ .github/workflows/.reusable-ci.yml | 177 +++++++++ .../workflows/.reusable-cleanup-registry.yml | 40 ++ .github/workflows/.reusable-compliance.yml | 84 +++++ .github/workflows/.reusable-docs.yaml | 43 +++ .../workflows/.reusable-integration-test.yml | 344 ++++++++++++++++++ .github/workflows/.reusable-sast.yml | 227 ++++++++++++ .github/workflows/.reusable-sca.yml | 100 +++++ .github/workflows/.reusable-unit-test.yml | 32 ++ .github/workflows/pr.yml | 29 ++ .github/workflows/push.yml | 29 ++ Makefile | 4 +- README.md | 8 +- {helm => charts/semgr8s}/Chart.yaml | 0 ...ivilege-escalation-no-securitycontext.yaml | 0 .../semgr8s}/rules/hostnetwork-pod.yaml | 0 .../semgr8s}/rules/privileged-container.yaml | 0 .../semgr8s}/rules/run-as-non-root.yaml | 0 {helm => charts/semgr8s}/templates/NOTES.txt | 0 .../semgr8s}/templates/_helpers.tpl | 0 .../semgr8s}/templates/deployment.yaml | 0 {helm => charts/semgr8s}/templates/env.yaml | 0 {helm => charts/semgr8s}/templates/role.yaml | 0 .../semgr8s}/templates/rolebinding.yaml | 0 {helm => charts/semgr8s}/templates/rules.yaml | 0 .../semgr8s}/templates/service.yaml | 0 .../semgr8s}/templates/serviceaccount.yaml | 0 .../semgr8s}/templates/webhook.yaml | 0 {helm => charts/semgr8s}/values.yaml | 0 helm/.helmignore | 23 -- 37 files changed, 1791 insertions(+), 29 deletions(-) create mode 100644 .github/actions/build/action.yml create mode 100644 .github/actions/context/action.yaml create mode 100644 .github/actions/grype/action.yaml create mode 100644 .github/actions/k3s-cluster/action.yaml create mode 100644 .github/actions/k8s-version-config/action.yaml create mode 100644 .github/actions/trivy-config/action.yaml create mode 100644 .github/actions/trivy-image/action.yaml create mode 100644 .github/workflows/.reusable-build.yml create mode 100644 .github/workflows/.reusable-ci.yml create mode 100644 .github/workflows/.reusable-cleanup-registry.yml create mode 100644 .github/workflows/.reusable-compliance.yml create mode 100644 .github/workflows/.reusable-docs.yaml create mode 100644 .github/workflows/.reusable-integration-test.yml create mode 100644 .github/workflows/.reusable-sast.yml create mode 100644 .github/workflows/.reusable-sca.yml create mode 100644 .github/workflows/.reusable-unit-test.yml create mode 100644 .github/workflows/pr.yml create mode 100644 .github/workflows/push.yml rename {helm => charts/semgr8s}/Chart.yaml (100%) rename {helm => charts/semgr8s}/rules/allow-privilege-escalation-no-securitycontext.yaml (100%) rename {helm => charts/semgr8s}/rules/hostnetwork-pod.yaml (100%) rename {helm => charts/semgr8s}/rules/privileged-container.yaml (100%) rename {helm => charts/semgr8s}/rules/run-as-non-root.yaml (100%) rename {helm => charts/semgr8s}/templates/NOTES.txt (100%) rename {helm => charts/semgr8s}/templates/_helpers.tpl (100%) rename {helm => charts/semgr8s}/templates/deployment.yaml (100%) rename {helm => charts/semgr8s}/templates/env.yaml (100%) rename {helm => charts/semgr8s}/templates/role.yaml (100%) rename {helm => charts/semgr8s}/templates/rolebinding.yaml (100%) rename {helm => charts/semgr8s}/templates/rules.yaml (100%) rename {helm => charts/semgr8s}/templates/service.yaml (100%) rename {helm => charts/semgr8s}/templates/serviceaccount.yaml (100%) rename {helm => charts/semgr8s}/templates/webhook.yaml (100%) rename {helm => charts/semgr8s}/values.yaml (100%) delete mode 100644 helm/.helmignore diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml new file mode 100644 index 0000000..54be081 --- /dev/null +++ b/.github/actions/build/action.yml @@ -0,0 +1,173 @@ +name: build +description: 'Build connaisseur image' +inputs: + image_registry: + description: 'Image registry to be used' + required: true + image_repo: + description: 'Image repository to be used' + required: true + image_tag: + description: 'Image tag to be used' + required: true + ref_tags: + description: 'Reference tags to be used' + required: true + image_labels: + description: 'Image labels to be used' + required: true + repo_owner: + description: 'Name of repository owner, e.g. "github.repository_owner" for ghcr.io' + required: true + repo_token: + description: 'Access token for repository owner, e.g. "secrets.GITHUB_TOKEN" for ghcr.io' + required: true + cosign_version: + description: 'Cosign version to be used' + required: true + cosign_private_key: + description: 'Cosign private key' + required: true + cosign_password: + description: 'Cosign private key password' + required: true +outputs: + cosign_public_key: + description: 'Cosign public key' + value: ${{ steps.verify.outputs.public_key }} +runs: + using: "composite" + steps: + - name: Install Cosign + uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 # v3.1.2 (probably) + - name: Set up Docker buildx + uses: docker/setup-buildx-action@f03ac48505955848960e80bbb68046aa35c7b9e7 # v2.4.1 + - name: Login with registry + uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0 + with: + registry: ${{ inputs.image_registry }} + username: ${{ inputs.repo_owner }} + password: ${{ inputs.repo_token }} + - name: Generate tags + id: tags + run: | + echo "${{ inputs.ref_tags }}" + export PREFIX="${{ inputs.image_registry }}/${{ inputs.image_repo }}:" + TAGS="${PREFIX}${{ inputs.image_tag }},$(echo ${{ inputs.ref_tags }} | tr ' ' '\n' | awk '{print "${PREFIX}"$1}' | envsubst | tr '\n' ',')" + echo tags=${TAGS} >> ${GITHUB_OUTPUT} + shell: bash + - name: Build and push image + id: build + uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 # v4.0.0 + with: + push: true + cache-from: type=gha + cache-to: type=gha,mode=max + file: build/Dockerfile + labels: ${{ inputs.image_labels }} + tags: ${{ steps.tags.outputs.tags }} + provenance: true + sbom: true + - name: Create SBOM + uses: anchore/sbom-action@07978da4bdb4faa726e52dfc6b1bed63d4b56479 # v0.13.3 + with: + image: ${{ inputs.image_registry }}/${{ inputs.image_repo }}@${{ steps.build.outputs.digest }} + format: cyclonedx-json + artifact-name: sbom.cdx + output-file: sbom.cdx + - name: Sign image + id: sign + run: | + cosign sign --key env://COSIGN_PRIVATE_KEY -a tag=${{ inputs.image_tag }} -y ${TAGS} + cosign attach sbom --sbom sbom.cdx --type cyclonedx ${TAGS} + cosign sign --key env://COSIGN_PRIVATE_KEY --attachment sbom -y ${TAGS} + env: + TAGS: ${{ inputs.image_registry }}/${{ inputs.image_repo }}@${{ steps.build.outputs.digest }} + COSIGN_PRIVATE_KEY: ${{ inputs.cosign_private_key }} + COSIGN_PASSWORD: ${{ inputs.cosign_password }} + shell: bash + - name: Verify build data + id: verify + run: | + mkdir ci + cosign public-key --key env://COSIGN_PRIVATE_KEY > ci/cosign.pub + PUBLIC_KEY="$(cat ci/cosign.pub)" + cosign tree ${TAGS} + PUBLIC_KEY=${PUBLIC_KEY} cosign verify --key env://PUBLIC_KEY ${TAGS} + PUBLIC_KEY=${PUBLIC_KEY} cosign verify --key env://PUBLIC_KEY --attachment sbom ${TAGS} + SIGNATURE=$(cosign triangulate ${TAGS}) + PUBLIC_KEY="${PUBLIC_KEY//$'\n'/'
'}" + SBOM="${SIGNATURE::-4}.sbom" + echo public_key="${PUBLIC_KEY}" >> ${GITHUB_OUTPUT} + echo signature=${SIGNATURE} >> ${GITHUB_OUTPUT} + echo sbom=${SBOM} >> ${GITHUB_OUTPUT} + env: + TAGS: ${{ inputs.image_registry }}/${{ inputs.image_repo }}@${{ steps.build.outputs.digest }} + COSIGN_PRIVATE_KEY: ${{ inputs.cosign_private_key }} + COSIGN_PASSWORD: ${{ inputs.cosign_password }} + shell: bash + - name: Upload public key + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + with: + name: cosign.pub + path: ci/cosign.pub + - name: Show build and signature information + run: | + CONFIGURE="yq '. *+ load(\"tests/integration/var-img.yaml\")' tests/integration/ghcr-values.yaml > ghcr.yaml &&\n\t IMAGE=\"${{ inputs.image_registry }}/${{ inputs.image_repo }}\" TAG=\"${{ inputs.image_tag }}\" IMAGEPULLSECRET=\"\" envsubst < ghcr.yaml > update &&\n\t yq '. *+ load(\"update\")' -i charts/connaisseur/values.yaml &&\n\t rm ghcr.yaml update" + CONFIGURE=$(printf -- "${CONFIGURE}") + PUBLIC_KEY="${{ steps.verify.outputs.public_key }}" + PUBLIC_KEY="$(printf -- "${PUBLIC_KEY//'
'/'\n'}")" + HELM_PATCH="yq e '.kubernetes.deployment.image.repository = \"${{ inputs.image_registry }}/${{ inputs.image_repo }}\"' -i charts/connaisseur/values.yaml\nyq e '.kubernetes.deployment.image.tag = \"${{ inputs.image_tag }}\"' -i charts/connaisseur/values.yaml" + HELM_PATCH=$(printf -- "${HELM_PATCH}") + echo "# :building_construction: Build Information" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "
Build artifactsValue
Registry${{ inputs.image_registry }}
Repository${{ inputs.image_repo }}
Tags${{ inputs.image_tag }}, ${{ inputs.ref_tags }}
Workflow image${{ inputs.image_registry }}/${{ inputs.image_repo }}:${{ inputs.image_tag }}
All reference tags$(echo ${{ steps.tags.outputs.tags }} | tr ',' '\n')
Digest${{ steps.build.outputs.digest }}
Signature${{ steps.verify.outputs.signature }}
Public key${PUBLIC_KEY}
SBOM (cyclonedx-json)${{ steps.verify.outputs.sbom }}
" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "
:bookmark_tabs: Metadata" >> ${GITHUB_STEP_SUMMARY} + echo "
${{ steps.build.outputs.metadata }}
" >> ${GITHUB_STEP_SUMMARY} + echo "
" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "
:hammer_and_wrench: Use Build Artifacts" >> ${GITHUB_STEP_SUMMARY} + echo "(needs docker login via PAT with package:read permission)" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "
" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "
:mag: Verify Build" >> ${GITHUB_STEP_SUMMARY} + echo "(needs Docker login via PAT with package:read permission)" >> ${GITHUB_STEP_SUMMARY} + echo "
    " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Show BuildKit image details:
    docker buildx imagetools inspect ${{ inputs.image_registry }}/${{ inputs.image_repo }}:${{ inputs.image_tag }}
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Extract BuildKit SBOM (can be consumed by e.g. syft or grype):
    docker buildx imagetools inspect ${{ inputs.image_registry }}/${{ inputs.image_repo }}:${{ inputs.image_tag }} --format \"{{ json .SBOM.SPDX }}\"
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Extract BuildKit provenance (SLSA) data:
    docker buildx imagetools inspect ${{ inputs.image_registry }}/${{ inputs.image_repo }}:${{ inputs.image_tag }} --format \"{{ json .Provenance.SLSA }}\"
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Cosign public key:
    ${PUBLIC_KEY}
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Store Cosign public key:
    echo \"${PUBLIC_KEY}\" > cosign.pub 
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Verify Cosign signature using cosign.pub file:
    cosign verify --key cosign.pub ${{ inputs.image_registry }}/${{ inputs.image_repo }}:${{ inputs.image_tag }} 
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Verify Cosign signature :
    COSIGN_PUBLIC_KEY='${PUBLIC_KEY}' cosign verify --key env://COSIGN_PUBLIC_KEY ${{ inputs.image_registry }}/${{ inputs.image_repo }}:${{ inputs.image_tag }} 
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Display all Cosign supply chain security artifacts:
    cosign tree ${{ inputs.image_registry }}/${{ inputs.image_repo }}:${{ inputs.image_tag }} 
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Download Cosign-attached SBOM (syft-generated cyclonedx-json):
    
    +        COSIGN_PUBLIC_KEY='${PUBLIC_KEY}' cosign verify --key env://COSIGN_PUBLIC_KEY --attachment sbom ${{ inputs.image_registry }}/${{ inputs.image_repo }}:${{ inputs.image_tag }} && # verify signature before download
    +        cosign download sbom ${{ inputs.image_registry }}/${{ inputs.image_repo }}:${{ inputs.image_tag }} > sbom.cdx # perform actual download
    +          
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
" >> ${GITHUB_STEP_SUMMARY} + echo "
" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "Let's start testing :rocket:" >> ${GITHUB_STEP_SUMMARY} + shell: bash diff --git a/.github/actions/context/action.yaml b/.github/actions/context/action.yaml new file mode 100644 index 0000000..4ecc0bf --- /dev/null +++ b/.github/actions/context/action.yaml @@ -0,0 +1,153 @@ +name: context +description: 'Get the current context' +inputs: + build_registry: + description: "Build registry to be used" + required: false + default: "ghcr.io" + build_repo: + description: "Base build repository to be used (non-protected branches will push to '${build_repo}-test')" + required: false + default: "${{ github.repository }}" +outputs: + chart_version: + description: "Semgr8s Helm chart version" + value: ${{ steps.get_context.outputs.CHART_VERSION }} + original_registry: + description: "Public semgr8s registry" + value: ${{ steps.get_context.outputs.ORIGINAL_REGISTRY }} + original_repo: + description: "Public semgr8s repo" + value: ${{ steps.get_context.outputs.ORIGINAL_REPO }} + original_tag: + description: "Current semgr8s tag, i.e. version" + value: ${{ steps.get_context.outputs.ORIGINAL_TAG }} + original_image: + description: "Full semg8s image reference, i.e. registry + repository + tag" + value: ${{ steps.get_context.outputs.ORIGINAL_IMAGE }} + build_registry: + description: "Workflow build registry used for testing" + value: ${{ steps.get_context.outputs.BUILD_REGISTRY }} + build_repo: + description: "Workflow build repository used for testing" + value: ${{ steps.get_context.outputs.BUILD_REPO }} + build_tag: + description: "Workflow build tag used for testing (unique for each run)" + value: ${{ steps.show_context.outputs.BUILD_TAG }} + build_image: + description: "Workflow build image used for testing, i.e. registry + repository + tag" + value: ${{ steps.show_context.outputs.BUILD_IMAGE }} + ref_tags: + description: "All reference tags used for build" + value: ${{ steps.show_context.outputs.REF_TAGS }} + build_labels: + description: "Repository- and workflow-specific build labels" + value: ${{ steps.meta.outputs.labels }} +runs: + using: "composite" + steps: + - name: Get chart version + id: get_chart_version + uses: mikefarah/yq@47f4f8c7939f887e851b35f14def6741b8f5396e # v4.31.2 + with: + cmd: yq '.version' charts/semgr8s/Chart.yaml + - name: Get app version + id: get_app_version + uses: mikefarah/yq@47f4f8c7939f887e851b35f14def6741b8f5396e # v4.31.2 + with: + cmd: yq '.appVersion' charts/semgr8s/Chart.yaml + - name: Get original image + id: get_original_image_repository + uses: mikefarah/yq@47f4f8c7939f887e851b35f14def6741b8f5396e # v4.31.2 + with: + cmd: yq '.kubernetes.deployment.image.repository' charts/semgr8s/values.yaml + - name: Get context + id: get_context + run: | + GHREF=${{ github.ref }} + echo "github.ref is: ${GHREF}" + CHART_VERSION=${{ steps.get_chart_version.outputs.result }} + CONFIGURED_IMAGE_REPO=${{ steps.get_original_image_repository.outputs.result }} + ORIGINAL_REGISTRY=$(echo "${CONFIGURED_IMAGE_REPO}" | cut -d "/" -f 1) + ORIGINAL_REPO=$(echo "${CONFIGURED_IMAGE_REPO}" | cut -d "/" -f 2- | cut -d ":" -f 1) + ORIGINAL_TAG=v${{ steps.get_app_version.outputs.result }} + BUILD_REGISTRY=${{ inputs.build_registry }} + BUILD_REPO=${{ inputs.build_repo }} + if [[ "${GHREF}" != "refs/heads/master" && + "${GHREF}" != "refs/tags/v"* && + "${GHREF}" != "refs/heads/develop" + ]]; then + BUILD_REPO="${BUILD_REPO}-test" + fi + + echo CHART_VERSION=${CHART_VERSION} >> ${GITHUB_OUTPUT} + echo ORIGINAL_REGISTRY=${ORIGINAL_REGISTRY} >> ${GITHUB_OUTPUT} + echo ORIGINAL_REPO=${ORIGINAL_REPO} >> ${GITHUB_OUTPUT} + echo ORIGINAL_TAG=${ORIGINAL_TAG} >> ${GITHUB_OUTPUT} + echo ORIGINAL_IMAGE=${CONFIGURED_IMAGE_REPO}:${ORIGINAL_TAG} >> ${GITHUB_OUTPUT} + echo BUILD_REGISTRY=${BUILD_REGISTRY} >> ${GITHUB_OUTPUT} + echo BUILD_REPO=${BUILD_REPO} >> ${GITHUB_OUTPUT} + shell: bash + - name: Generate metadata + id: meta + uses: docker/metadata-action@507c2f2dc502c992ad446e3d7a5dfbe311567a96 # v4.3.0 + with: + images: ${{ steps.get_context.outputs.BUILD_REGISTRY }}/${{ steps.get_context.outputs.BUILD_REPO }} + flavor: | + latest=true + tags: | + type=schedule + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=sha + - name: Show context + id: show_context + run: | + PREFIX=$(echo "${{ steps.get_context.outputs.BUILD_REGISTRY }}/${{ steps.get_context.outputs.BUILD_REPO }}:" | sed 's%/%\/%g') + TAGS="${{ steps.meta.outputs.tags }}" + REF_TAGS="${TAGS//${PREFIX}/}" + BUILD_IMAGE=$(echo "${TAGS}" | tail -2 | head -1) + BUILD_TAG="${BUILD_IMAGE//${PREFIX}/}" + [[ ${BUILD_TAG} == "sha-"* ]] || exit 1 # check as parsing of the BUILD_TAG maybe fragile and dependent on docker/metadata-action priorities + REF_TAGS="${REF_TAGS//${BUILD_TAG}/}" + echo BUILD_TAG=${BUILD_TAG} >> ${GITHUB_OUTPUT} + echo BUILD_IMAGE=${BUILD_IMAGE} >> ${GITHUB_OUTPUT} + echo REF_TAGS=${REF_TAGS} >> ${GITHUB_OUTPUT} + echo "# :clipboard: Context" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "
Build ContextValue
Helm chart version${{ steps.get_context.outputs.CHART_VERSION }}
Original registry${{ steps.get_context.outputs.ORIGINAL_REGISTRY }}
Original repository${{ steps.get_context.outputs.ORIGINAL_REPO }}
Original tag${{ steps.get_context.outputs.ORIGINAL_TAG }}
Original image${{ steps.get_context.outputs.ORIGINAL_IMAGE }}
Build registry${{ steps.get_context.outputs.BUILD_REGISTRY }}
Build repository${{ steps.get_context.outputs.BUILD_REPO }}
Build tag${BUILD_TAG}
Build image${BUILD_IMAGE}
Ref tags${REF_TAGS}
All build images${{ steps.meta.outputs.tags }}
Build labels${{ steps.meta.outputs.labels }}
" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "
:pushpin: Context Variables References" >> ${GITHUB_STEP_SUMMARY} + echo "( job must run in workflow and needs: [context] mut be set for job)" >> ${GITHUB_STEP_SUMMARY} + echo "
    " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Helm chart version:
    ${{ needs.context.outputs.chart_version }}
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Original registry:
    ${{ needs.context.outputs.original_registry }}
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Original repository:
    ${{ needs.context.outputs.original_repo }}
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Original tag:
    ${{ needs.context.outputs.original_tag }}
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Original image:
    ${{ needs.context.outputs.original_image }}
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Build registry:
    ${{ needs.context.outputs.build_registry }}
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Build repository:
    ${{ needs.context.outputs.build_repo }}
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Build tag (workflow):
    ${{ needs.context.outputs.build_tag }}
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Ref tags:
    ${{ needs.context.outputs.ref_tags }}
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Build image:
    ${{ needs.context.outputs.build_image }}
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
  • Build labels:
    ${{ needs.context.outputs.build_labels }}
  • " >> ${GITHUB_STEP_SUMMARY} + echo "
" >> ${GITHUB_STEP_SUMMARY} + echo "
" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "Let's start building :rocket:" >> ${GITHUB_STEP_SUMMARY} + shell: bash + diff --git a/.github/actions/grype/action.yaml b/.github/actions/grype/action.yaml new file mode 100644 index 0000000..058f4e5 --- /dev/null +++ b/.github/actions/grype/action.yaml @@ -0,0 +1,49 @@ +name: grype +description: 'Run Grype on image' +inputs: + image: + description: 'Image name' + required: true + registry: + description: 'Registry to login to pull image, e.g. "ghcr.io" for GHCR, leave empty if image is public' + required: false + default: '' + repo_owner: + description: 'Name of repository owner, e.g. "github.repository_owner" for ghcr.io' + required: false + repo_token: + description: 'Access token for repository owner, e.g. "secrets.GITHUB_TOKEN" for ghcr.io' + required: false + output: + description: 'Grype output either "sarif" (GITHUB_TOKEN with security-events:write) or print results as "table" and fail on error' + required: false +runs: + using: "composite" + steps: + - name: Login with registry + if: inputs.registry != '' + uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0 + with: + registry: ${{ inputs.registry }} + username: ${{ inputs.repo_owner }} + password: ${{ inputs.repo_token }} + - name: Scan + if: inputs.output == 'table' + uses: anchore/scan-action@dafbc97d7259af88b61bd260f2fde565d0668a72 # v3.3.4 + with: + image: ${{ inputs.image }} + fail-build: true + output-format: table + - name: Scan + id: scan + if: inputs.output == 'sarif' + uses: anchore/scan-action@dafbc97d7259af88b61bd260f2fde565d0668a72 # v3.3.4 + with: + image: ${{ inputs.image }} + fail-build: false + output-format: sarif + - name: Upload + if: inputs.output == 'sarif' + uses: github/codeql-action/upload-sarif@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2.2.5 + with: + sarif_file: ${{ steps.scan.outputs.sarif }} diff --git a/.github/actions/k3s-cluster/action.yaml b/.github/actions/k3s-cluster/action.yaml new file mode 100644 index 0000000..8fca6a3 --- /dev/null +++ b/.github/actions/k3s-cluster/action.yaml @@ -0,0 +1,74 @@ +# Adjusted from https://github.com/jupyterhub/action-k3s-helm +--- +name: K3S with Helm +description: | + Install Kubernetes (K3S) and Helm. + +inputs: + k3s-channel: + description: K3S channel (https://update.k3s.io/v1-release/channels) + required: false + default: "" + +outputs: + kubeconfig: + description: Path to kubeconfig file + value: ${{ steps.set-versions.outputs.kubeconfig }} + k3s-version: + description: "Installed k3s version, such as v1.20.0+k3s2" + value: "${{ steps.set-versions.outputs.k3s-version }}" + k8s-version: + description: "Installed k8s version, such as v1.20.0" + value: "${{ steps.set-versions.outputs.k8s-version }}" + helm-version: + description: "Installed helm version, such as v3.4.2" + value: "${{ steps.set-versions.outputs.helm-version }}" + +runs: + using: "composite" + steps: + - name: Setup k3s ${{ inputs.k3s-channel }} + run: | + curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL="${{ inputs.k3s-channel }}" sh -s - + shell: bash + + # By providing a kubeconfig owned by the current user with 600 permissions, + # kubectl becomes usable without sudo, and helm won't emit warnings about + # bloated access to group/world. + - name: Prepare a kubeconfig in ~/.kube/config + run: | + mkdir -p ~/.kube + sudo cat /etc/rancher/k3s/k3s.yaml > "$HOME/.kube/config" + chmod 600 "$HOME/.kube/config" + echo "KUBECONFIG=$HOME/.kube/config" >> $GITHUB_ENV + shell: bash + + - name: Setup Helm + run: | + curl -sf https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash + shell: bash + + - name: Set version output + id: set-versions + run: | + echo "::group::Set version output" + echo "kubeconfig=$HOME/.kube/config" >> $GITHUB_OUTPUT + echo "k3s-version=$(k3s --version | grep 'k3s' | sed 's/.*\(v[0-9][^ ]*\).*/\1/')" >> $GITHUB_OUTPUT + echo "k8s-version=$(k3s --version | grep 'k3s' | sed 's/.*\(v[0-9][^+]*\).*/\1/')" >> $GITHUB_OUTPUT + echo "::endgroup::" + shell: bash + + - name: Wait for coredns, metrics server, traefik + run: | + # Wait for a few seconds to allow deployments spin up + sleep 10 + + kubectl rollout status --watch --timeout 300s deployment/coredns -n kube-system + + kubectl rollout status --watch --timeout 300s deployment/metrics-server -n kube-system + + kubectl wait --for=condition=complete --timeout=300s job/helm-install-traefik-crd -n kube-system || true + kubectl wait --for=condition=complete --timeout=300s job/helm-install-traefik -n kube-system || true + kubectl rollout status --watch --timeout 300s deployment/traefik -n kube-system + shell: bash + diff --git a/.github/actions/k8s-version-config/action.yaml b/.github/actions/k8s-version-config/action.yaml new file mode 100644 index 0000000..353475c --- /dev/null +++ b/.github/actions/k8s-version-config/action.yaml @@ -0,0 +1,25 @@ +name: k8s-version-config +description: 'action to prepare testing different k8s versions' +inputs: + k8s-version: + description: 'k8s version to be tested' + required: true +runs: + using: "composite" + steps: + - name: Install yq and bash + run: | + sudo snap install yq + sudo apt update + sudo apt install bash -y + shell: bash + - uses: ./.github/actions/k3s-cluster + with: + k3s-channel: ${{ inputs.k8s-version }} + - name: Adjust Configuration + run: | + if [[ $(echo "${{ inputs.k8s-version }}" | tail -c 3) -lt "19" ]]; then + yq e 'del(.kubernetes.deployment.securityContext.seccompProfile)' -i charts/semgr8s/values.yaml + yq e '.kubernetes.deployment.annotations."seccomp.security.alpha.kubernetes.io/pod" = "runtime/default"' -i charts/semgr8s/values.yaml + fi + shell: bash diff --git a/.github/actions/trivy-config/action.yaml b/.github/actions/trivy-config/action.yaml new file mode 100644 index 0000000..4e23b58 --- /dev/null +++ b/.github/actions/trivy-config/action.yaml @@ -0,0 +1,53 @@ +name: trivy-config +description: 'Run Trivy on config' +inputs: + output: + description: 'Trivy output either "sarif" (GITHUB_TOKEN with security-events:write) or print results as "table" and fail on error' + required: false +runs: + using: "composite" + steps: + - name: Create reports folder + run: | + mkdir reports + shell: bash + - name: Render Helm charts + run: | + mkdir deployment + helm template charts/connaisseur > deployment/deployment.yaml + shell: bash + - name: Scan deployment.yaml + uses: aquasecurity/trivy-action@fbd16365eb88e12433951383f5e99bd901fc618f # v0.12.0 + if: inputs.output == 'table' + with: + scan-type: "config" + scan-ref: "deployment" + format: 'table' + - name: Scan Dockerfiles + uses: aquasecurity/trivy-action@fbd16365eb88e12433951383f5e99bd901fc618f # v0.12.0 + if: inputs.output == 'table' + with: + scan-type: "config" + scan-ref: "build" + format: 'table' + - name: Scan deployment.yaml + uses: aquasecurity/trivy-action@fbd16365eb88e12433951383f5e99bd901fc618f # v0.12.0 + if: inputs.output == 'sarif' + with: + scan-type: "config" + scan-ref: "deployment" + format: 'sarif' + output: 'reports/trivy-k8s-results.sarif' + - name: Scan Dockerfiles + uses: aquasecurity/trivy-action@fbd16365eb88e12433951383f5e99bd901fc618f # v0.12.0 + if: inputs.output == 'sarif' + with: + scan-type: "config" + scan-ref: "build" + format: 'sarif' + output: 'reports/trivy-docker-results.sarif' + - name: Upload + uses: github/codeql-action/upload-sarif@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2.2.5 + if: inputs.output == 'sarif' + with: + sarif_file: 'reports' diff --git a/.github/actions/trivy-image/action.yaml b/.github/actions/trivy-image/action.yaml new file mode 100644 index 0000000..4fd2fd7 --- /dev/null +++ b/.github/actions/trivy-image/action.yaml @@ -0,0 +1,54 @@ +name: trivy-image +description: 'Run Trivy on image' +inputs: + image: + description: 'Image name' + required: true + registry: + description: 'Registry to login to pull image, e.g. "ghcr.io" for GHCR, leave empty if image is public' + required: false + default: '' + repo_owner: + description: 'Name of repository owner, e.g. "github.repository_owner" for ghcr.io' + required: false + repo_token: + description: 'Access token for repository owner, e.g. "secrets.GITHUB_TOKEN" for ghcr.io' + required: false + output: + description: 'Trivy output either "sarif" (GITHUB_TOKEN with security-events:write) or print results as "table" and fail on error' + required: false +runs: + using: "composite" + steps: + - name: Login with registry + if: inputs.registry != '' + uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0 + with: + registry: ${{ inputs.registry }} + username: ${{ inputs.repo_owner }} + password: ${{ inputs.repo_token }} + - name: Create reports folder + run: | + mkdir reports + shell: sh + - name: Run Trivy on image + if: inputs.output == 'sarif' + uses: aquasecurity/trivy-action@fbd16365eb88e12433951383f5e99bd901fc618f # v0.12.0 + with: + image-ref: ${{ inputs.image }} + scan-type: "image" + format: 'sarif' + output: 'reports/trivy-vuln-results.sarif' + - name: Run Trivy on image + if: inputs.output == 'table' + uses: aquasecurity/trivy-action@fbd16365eb88e12433951383f5e99bd901fc618f # v0.12.0 + with: + image-ref: ${{ inputs.image }} + scan-type: "image" + exit-code: 1 + format: 'table' + - name: Upload + if: inputs.output == 'sarif' + uses: github/codeql-action/upload-sarif@32dc499307d133bb5085bae78498c0ac2cf762d5 # v2.2.5 + with: + sarif_file: 'reports' diff --git a/.github/workflows/.reusable-build.yml b/.github/workflows/.reusable-build.yml new file mode 100644 index 0000000..75ec9c5 --- /dev/null +++ b/.github/workflows/.reusable-build.yml @@ -0,0 +1,99 @@ +name: build + +#permissions: {} #TODO: reactivate for non-private + +on: + workflow_call: + inputs: + skip: + description: "Want to skip running certain jobs 'none', 'non-required', 'all'?" + type: string + default: "none" + outputs: + cosign_public_key: + description: "Cosign public key used for signing Connaisseur image" + value: ${{ jobs.build.outputs.cosign_public_key }} + chart_version: + description: "Connaisseur Helm chart version" + value: ${{ jobs.context.outputs.chart_version }} + original_registry: + description: "Public Connaisseur registry" + value: ${{ jobs.context.outputs.original_registry }} + original_repo: + description: "Public Connaisseur repo" + value: ${{ jobs.context.outputs.original_repo }} + original_tag: + description: "Current Connaisseur tag, i.e. version" + value: ${{ jobs.context.outputs.original_tag }} + original_image: + description: "Full Connaisseur image reference, i.e. registry + repository + tag" + value: ${{ jobs.context.outputs.original_image }} + build_registry: + description: "Workflow build registry used for testing" + value: ${{ jobs.context.outputs.build_registry }} + build_repo: + description: "Workflow build repository used for testing" + value: ${{ jobs.context.outputs.build_repo }} + build_tag: + description: "Workflow build tag used for testing (unique for each run)" + value: ${{ jobs.context.outputs.build_tag }} + branch_tag: + description: "Branch tag used for all builds on branch" + value: ${{ jobs.context.outputs.branch_tag }} + build_image: + description: "Workflow build image used for testing, i.e. registry + repository + tag" + value: ${{ jobs.context.outputs.build_image }} + build_labels: + description: "Repository- and workflow-specific build labels" + value: ${{ jobs.context.outputs.build_labels }} + +jobs: + context: + runs-on: ubuntu-latest + if: inputs.skip != 'all' + # permissions: {} #TODO: reactivate for non-private + outputs: + chart_version: ${{ steps.get_context.outputs.chart_version }} + original_registry: ${{ steps.get_context.outputs.original_registry }} + original_repo: ${{ steps.get_context.outputs.original_repo }} + original_image: ${{ steps.get_context.outputs.original_image }} + original_tag: ${{ steps.get_context.outputs.original_tag }} + build_registry: ${{ steps.get_context.outputs.build_registry }} + build_repo: ${{ steps.get_context.outputs.build_repo }} + build_tag: ${{ steps.get_context.outputs.build_tag }} + ref_tags: ${{ steps.get_context.outputs.ref_tags }} + build_image: ${{ steps.get_context.outputs.build_image }} + build_labels: ${{ steps.get_context.outputs.build_labels }} + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Get context + id: get_context + uses: ./.github/actions/context + + build: + runs-on: ubuntu-latest + if: | + inputs.skip != 'non-required' && + inputs.skip != 'all' + needs: [context] + # permissions: #TODO: reactivate for non-private + # packages: write + outputs: + cosign_public_key: ${{ steps.build.outputs.cosign_public_key }} + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Build Connaisseur + id: build + uses: ./.github/actions/build + with: + image_registry: ${{ needs.context.outputs.build_registry }} + image_repo: ${{ needs.context.outputs.build_repo }} + image_tag: ${{ needs.context.outputs.build_tag }} + ref_tags: ${{ needs.context.outputs.ref_tags }} + image_labels: ${{ needs.context.outputs.build_labels }} + repo_owner: ${{ github.repository_owner }} + repo_token: ${{ secrets.GITHUB_TOKEN }} + cosign_private_key: ${{ secrets.COSIGN_PRIVATE_KEY }} + cosign_password: ${{ secrets.COSIGN_PASSWORD }} diff --git a/.github/workflows/.reusable-ci.yml b/.github/workflows/.reusable-ci.yml new file mode 100644 index 0000000..6c1393d --- /dev/null +++ b/.github/workflows/.reusable-ci.yml @@ -0,0 +1,177 @@ +name: ci + +#permissions: {} #TODO: reactivate for non-private + +on: + workflow_call: + inputs: + skip_build: + description: "Want to skip running certain build jobs 'none', 'non-required', 'all'?" + type: string + default: "all" + required: false + skip_compliance_checks: + description: "Want to skip running certain compliance jobs 'none', 'non-required', 'all'?" + type: string + default: "all" + required: false + skip_unit_tests: + description: "Want to skip running certain unit test jobs 'none', 'non-required', 'all'?" + type: string + default: "all" + required: false + skip_sast: + description: "Want to skip running certain sast jobs 'none', 'non-required', 'all'?" + type: string + default: "all" + required: false + skip_sca: + description: "Want to skip running certain sca jobs 'none', 'non-required', 'all'?" + type: string + default: "all" + required: false + skip_docs: + description: "Want to skip running certain docs jobs 'none', 'non-required', 'all'?" + type: string + default: "all" + required: false + skip_integration_tests: + description: "Want to skip running certain integration test jobs 'none', 'non-required', 'all'?" + type: string + default: "all" + required: false + output_type: + description: 'Output either "sarif" (GITHUB_TOKEN with security-events:write) or print results as "table" and fail on error' + type: string + default: 'sarif' + required: false + +defaults: + run: + shell: bash + +jobs: + conditionals: + runs-on: ubuntu-latest + outputs: + skip_build: ${{ steps.conditionals.outputs.skip_build }} + skip_compliance_checks: ${{ steps.conditionals.outputs.skip_compliance_checks }} + skip_unit_tests: ${{ steps.conditionals.outputs.skip_unit_tests }} + skip_sast: ${{ steps.conditionals.outputs.skip_sast }} + skip_sca: ${{ steps.conditionals.outputs.skip_sca }} + skip_docs: ${{ steps.conditionals.outputs.skip_docs }} + skip_integration_tests: ${{ steps.conditionals.outputs.skip_integration_tests }} + output_type: ${{ steps.conditionals.outputs.output_type }} + steps: + - name: CI conditionals + id: conditionals + run: | + echo "skip_build=${{ inputs.skip_build }}" >> ${GITHUB_OUTPUT} + echo "skip_compliance_checks=${{ inputs.skip_compliance_checks }}" >> ${GITHUB_OUTPUT} + echo "skip_unit_tests=${{ inputs.skip_unit_tests }}" >> ${GITHUB_OUTPUT} + echo "skip_sast=${{ inputs.skip_sast }}" >> ${GITHUB_OUTPUT} + echo "skip_sca=${{ inputs.skip_sca }}" >> ${GITHUB_OUTPUT} + echo "skip_docs=${{ inputs.skip_docs }}" >> ${GITHUB_OUTPUT} + echo "skip_integration_tests=${{ inputs.skip_integration_tests }}" >> ${GITHUB_OUTPUT} + echo "output_type=${{ inputs.output_type }}" >> ${GITHUB_OUTPUT} + - name: Show conditionals + id: show_conditionals + run: | + get_output() { case "$1" in "none") echo ":white_check_mark:";; "non-required") echo ":information_source:";; "all") echo ":x:";; *) echo "Unknown value";; esac; } + echo "# :pencil: CI Settings" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + echo "
SettingValue
Run Docs$(get_output ${{ steps.conditionals.outputs.skip_docs }})
Run Build$(get_output ${{ steps.conditionals.outputs.skip_build }})
Run Compliance$(get_output ${{ steps.conditionals.outputs.skip_compliance_checks }})
Run Unit Tests$(get_output ${{ steps.conditionals.outputs.skip_unit_tests }})
Run SAST$(get_output ${{ steps.conditionals.outputs.skip_sast }})
Run SCA$(get_output ${{ steps.conditionals.outputs.skip_sca }})
Run Integration Tests$(get_output ${{ steps.conditionals.outputs.skip_integration_tests }})
Report type${{ steps.conditionals.outputs.output_type }}
" >> ${GITHUB_STEP_SUMMARY} + echo "($(get_output 'none') - run all jobs, $(get_output 'non-required') - run important/required jobs only, $(get_output 'all') - skip jobs)" >> ${GITHUB_STEP_SUMMARY} + echo "" >> ${GITHUB_STEP_SUMMARY} + + build: + uses: ./.github/workflows/.reusable-build.yml + needs: [conditionals] + # permissions: #TODO: reactivate for non-private + # packages: write + secrets: inherit + with: + skip: ${{ needs.conditionals.outputs.skip_build }} + + compliance: + uses: ./.github/workflows/.reusable-compliance.yml + needs: [conditionals] + # permissions: #TODO: reactivate for non-private + # contents: write + # id-token: write + # security-events: write + # actions: read + # checks: read + # deployments: read + # issues: read + # discussions: read + # packages: read + # pages: read + # pull-requests: read + # repository-projects: read + # statuses: read + secrets: inherit + with: + skip: ${{ needs.conditionals.outputs.skip_compliance_checks }} + + unit-test: + uses: ./.github/workflows/.reusable-unit-test.yml + needs: [conditionals] + with: + skip: ${{ needs.conditionals.outputs.skip_unit_tests }} + + sast: + uses: ./.github/workflows/.reusable-sast.yml + needs: [conditionals] + # permissions: #TODO: reactivate for non-private + # security-events: write + # pull-requests: read + with: + skip: ${{ needs.conditionals.outputs.skip_sast }} + output: ${{ needs.conditionals.outputs.output_type }} + + sca: + uses: ./.github/workflows/.reusable-sca.yml + needs: [conditionals, build] + # permissions: #TODO: reactivate for non-private + # contents: write + # security-events: write + # packages: read + secrets: inherit + with: + registry: ${{ needs.build.outputs.build_registry }} + repo_owner: ${{ github.repository_owner }} + image: ${{ needs.build.outputs.build_image }} + skip: ${{ needs.conditionals.outputs.skip_sca }} + output: ${{ needs.conditionals.outputs.output_type }} + + docs: + uses: ./.github/workflows/.reusable-docs.yaml + needs: [conditionals] + # permissions: #TODO: reactivate for non-private + # contents: write + with: + skip: ${{ needs.conditionals.outputs.skip_docs }} + + integration-test: + uses: ./.github/workflows/.reusable-integration-test.yml + needs: [conditionals, build] + # permissions: #TODO: reactivate for non-private + # packages: read + secrets: inherit + with: + build_registry: ${{ needs.build.outputs.build_registry }} + repo_owner: ${{ github.repository_owner }} + build_image_repository: ${{ needs.build.outputs.build_registry }}/${{ needs.build.outputs.build_repo }} + build_tag: ${{ needs.build.outputs.build_tag }} + skip: ${{ needs.conditionals.outputs.skip_integration_tests }} + cosign_public_key: ${{ needs.build.outputs.cosign_public_key }} diff --git a/.github/workflows/.reusable-cleanup-registry.yml b/.github/workflows/.reusable-cleanup-registry.yml new file mode 100644 index 0000000..2bef523 --- /dev/null +++ b/.github/workflows/.reusable-cleanup-registry.yml @@ -0,0 +1,40 @@ +name: cleanup registry + +on: + workflow_call: + +permissions: {} + +jobs: + cleanup-registry: + runs-on: ubuntu-latest + steps: + - name: Cleanup test images in 'connaisseur-test' + uses: snok/container-retention-policy@3d27e6a0361deed0b7dc5099a82eadd07924b177 # v2.1.3 + with: + image-names: connaisseur-test + cut-off: three weeks ago UTC+1 + timestamp-to-use: updated_at + account-type: org + org-name: sse-secure-systems + token: ${{ secrets.GHCR_PAT }} + - name: Cleanup dangling images without tag + uses: snok/container-retention-policy@3d27e6a0361deed0b7dc5099a82eadd07924b177 # v2.1.3 + with: + image-names: connaisseur* + untagged-only: true + cut-off: four hours ago UTC+1 + timestamp-to-use: updated_at + account-type: org + org-name: sse-secure-systems + token: ${{ secrets.GHCR_PAT }} + - name: Cleanup all connaisseur images + uses: snok/container-retention-policy@3d27e6a0361deed0b7dc5099a82eadd07924b177 # v2.1.3 + with: + image-names: connaisseur + skip-tags: master, develop, v*, sha256-* + cut-off: four days ago UTC+1 + timestamp-to-use: updated_at + account-type: org + org-name: sse-secure-systems + token: ${{ secrets.GHCR_PAT }} diff --git a/.github/workflows/.reusable-compliance.yml b/.github/workflows/.reusable-compliance.yml new file mode 100644 index 0000000..1f9a8cc --- /dev/null +++ b/.github/workflows/.reusable-compliance.yml @@ -0,0 +1,84 @@ +name: compliance + +on: + workflow_call: + inputs: + skip: + description: "Want to skip running certain jobs 'none', 'non-required', 'all'?" + type: string + default: "none" + +#permissions: read-all + +jobs: + ossf-scorecard: + runs-on: ubuntu-latest + if: | + (github.ref_name == 'master' || github.event_name == 'pull_request') && + inputs.skip != 'non-required' && + inputs.skip != 'all' + # permissions: #TODO: reactivate for non-private + # security-events: write + # id-token: write + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + persist-credentials: false + - name: Analyze + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + with: + results_file: results.sarif + results_format: sarif + repo_token: ${{ secrets.SCORECARD_TOKEN }} + publish_results: ${{ github.ref_name == 'master' }} + - name: Upload + uses: github/codeql-action/upload-sarif@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 + with: + sarif_file: results.sarif + + dependency-review: + name: dependency review + runs-on: ubuntu-latest + if: | + github.event_name == 'pull_request' && + inputs.skip != 'non-required' && + inputs.skip != 'all' + # permissions: #TODO: reactivate for non-private + # contents: write + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Review + uses: actions/dependency-review-action@7bbfa034e752445ea40215fff1c3bf9597993d3f # v3.1.3 + + check-commit-message: + runs-on: ubuntu-latest + if: | + github.event_name == 'pull_request' && + inputs.skip != 'all' + # permissions: {} #TODO: reactivate for non-private + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + ref: ${{ github.event.pull_request.head.sha }} # Otherwise will checkout merge commit, which isn't conform + fetch-depth: ${{ github.event.pull_request.commits }} # Fetch all commits of the MR, but only those + - name: Check commit messages for conformity + run: | + echo "Commits between dev branch and current SHA:" + COMMITS=$(git log --pretty=%H) + echo "${COMMITS}" + EXIT=0 + COMMIT_MSGS=$(git log --pretty=%s) # show subject only + for commit in ${COMMITS}; do + MSG=$(git log ${commit} -n1 --pretty=%s) + TYPE=$(echo ${MSG} | awk '{{ print $1 }}') + if ! [[ "${TYPE}" =~ ^(build|ci|docs|feat|fix|refactor|test|update):$ ]]; then + EXIT=1 + echo "Commit message of commit ${commit} doesn't conform to 'type: msg' format:" + echo "${MSG}" + echo "-------------------------" + fi + done + exit ${EXIT} diff --git a/.github/workflows/.reusable-docs.yaml b/.github/workflows/.reusable-docs.yaml new file mode 100644 index 0000000..3e5fc0e --- /dev/null +++ b/.github/workflows/.reusable-docs.yaml @@ -0,0 +1,43 @@ +name: docs + +#permissions: {} #TODO: reactivate for non-private + +on: + workflow_call: + inputs: + skip: + description: "Want to skip running certain jobs 'none', 'non-required', 'all'?" + type: string + default: "none" + +jobs: + deploy: + runs-on: ubuntu-latest + if: inputs.skip != 'all' + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + fetch-depth: 0 + - name: Set release env + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + - name: Configure the git user + run: | + git config user.name "versioning_user" + git config user.email "connaisseur@securesystems.dev" + - name: Install + run: | + pip install -r docs/requirements_docs.txt + - name: Deploy + if: inputs.skip != 'non-required' + run: | + if [[ "${GITHUB_REF}" == "refs/tags/v"* ]]; + then + mike deploy --push --update-aliases ${RELEASE_VERSION} latest + elif [[ "${GITHUB_REF}" == "refs/heads/develop" ]]; then + mike deploy --push ${RELEASE_VERSION} + else + mkdocs build + fi diff --git a/.github/workflows/.reusable-integration-test.yml b/.github/workflows/.reusable-integration-test.yml new file mode 100644 index 0000000..f98cd13 --- /dev/null +++ b/.github/workflows/.reusable-integration-test.yml @@ -0,0 +1,344 @@ +name: integration-test + +#permissions: {} #TODO: reactivate for non-private + +on: + workflow_call: + inputs: + build_registry: + description: "Workflow build registry used for testing" + type: string + repo_owner: + description: 'Name of repository owner, e.g. "inputs.repo_owner" for ghcr.io' + type: string + build_image_repository: + description: "Workflow build image used for testing, excluding the tag i.e. registry + repository" + type: string + build_tag: + description: "Tag of build image used for testing" + type: string + skip: + description: "Want to skip running certain jobs 'none', 'non-required', 'all'?" + type: string + default: "none" + cosign_public_key: + description: "Cosign public key used for signing the build image" + type: string + +env: + IMAGEPULLSECRET: dockerconfigjson-ghcr + +jobs: + integration-test: + name: functional + runs-on: ubuntu-latest + if: inputs.skip != 'all' + # permissions: #TODO: reactivate for non-private + # packages: read + env: + IMAGE: ${{ inputs.build_image_repository }} + TAG: ${{ inputs.build_tag }} + COSIGN_PUBLIC_KEY: ${{ inputs.cosign_public_key }} + strategy: + fail-fast: false + matrix: + integration-test-arg: + [ + "regular", + "cosign", + "multi-cosigned", + "rekor-cosigned", + "namespace-val", + "deployment", + "pre-config", + "other-ns", + "configured-cert", + ] + services: + alerting-endpoint: + image: securesystemsengineering/alerting-endpoint + ports: + - 56243:56243 + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Login with registry + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ inputs.build_registry }} + username: ${{ inputs.repo_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Install yq + run: | + sudo snap install yq + - uses: ./.github/actions/k8s-version-config + name: Setup k8s cluster + with: + k8s-version: v1.25 + - name: Get alerting endpoint IP + id: get_ip + uses: ./.github/actions/alerting-endpoint + - name: Run test + run: | + bash tests/integration/integration-test.sh "${{ matrix.integration-test-arg }}" + env: + ALERTING_ENDPOINT_IP: ${{ steps.get_ip.outputs.ip }} + - name: Display Connaisseur configuration + if: always() + run: | + echo "::group::values.yaml" + yq e '... comments=""' charts/connaisseur/values.yaml + echo "::endgroup::" + - name: Display k8s state if integration test failed + if: failure() + run: | + kubectl describe deployments.apps -n connaisseur -lapp.kubernetes.io/name=connaisseur + kubectl describe pods -n connaisseur -lapp.kubernetes.io/name=connaisseur + - name: Display logs if integration test failed + if: failure() + run: | + kubectl logs -n connaisseur -lapp.kubernetes.io/name=connaisseur --prefix=true --tail=-1 + + optional-integration-test: + name: optional + runs-on: ubuntu-latest + if: | + inputs.skip != 'non-required' && + inputs.skip != 'all' + # permissions: #TODO: reactivate for non-private + # packages: read + env: + IMAGE: ${{ inputs.build_image_repository }} + TAG: ${{ inputs.build_tag }} + COSIGN_PUBLIC_KEY: ${{ inputs.cosign_public_key }} + strategy: + fail-fast: false + matrix: + integration-test-arg: + [ + "complexity", + "load", + "upgrade", + ] + services: + alerting-endpoint: + image: securesystemsengineering/alerting-endpoint + ports: + - 56243:56243 + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Login with registry + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ inputs.build_registry }} + username: ${{ inputs.repo_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Install yq + run: | + sudo snap install yq + - uses: ./.github/actions/k8s-version-config + name: Setup k8s cluster + with: + k8s-version: v1.25 + - name: Get alerting endpoint IP + id: get_ip + uses: ./.github/actions/alerting-endpoint + - name: Run test + run: | + bash tests/integration/integration-test.sh "${{ matrix.integration-test-arg }}" + env: + ALERTING_ENDPOINT_IP: ${{ steps.get_ip.outputs.ip }} + - name: Display Connaisseur configuration + if: always() + run: | + echo "::group::values.yaml" + yq e '... comments=""' charts/connaisseur/values.yaml + echo "::endgroup::" + - name: Display k8s state if integration test failed + if: failure() + run: | + kubectl describe deployments.apps -n connaisseur -lapp.kubernetes.io/name=connaisseur + kubectl describe pods -n connaisseur -lapp.kubernetes.io/name=connaisseur + - name: Display logs if integration test failed + if: failure() + run: | + kubectl logs -n connaisseur -lapp.kubernetes.io/name=connaisseur --prefix=true --tail=-1 + + k8s-versions: + name: k8s versions + runs-on: ubuntu-latest + if: inputs.skip != 'all' + # permissions: #TODO: reactivate for non-private + # packages: read + env: + IMAGE: ${{ inputs.build_image_repository }} + TAG: ${{ inputs.build_tag }} + COSIGN_PUBLIC_KEY: ${{ inputs.cosign_public_key }} + strategy: + fail-fast: false + matrix: + k8s-version: [ + "v1.25", + "v1.26", + "v1.27", + "v1.28", + ] + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Login with registry + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ inputs.build_registry }} + username: ${{ inputs.repo_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Install yq + run: | + sudo snap install yq + - uses: ./.github/actions/k8s-version-config + name: Setup k8s cluster + with: + k8s-version: ${{ matrix.k8s-version }} + - name: Run pre-config and workload integration tests + run: | + bash tests/integration/integration-test.sh "pre-and-workload" + - name: Display k8s state and logs if integration test failed + if: failure() + run: | + kubectl describe deployments.apps -n connaisseur -lapp.kubernetes.io/name=connaisseur + kubectl describe pods -n connaisseur -lapp.kubernetes.io/name=connaisseur + kubectl logs -n connaisseur -lapp.kubernetes.io/name=connaisseur --prefix=true --tail=-1 + - name: Display Connaisseur configuration + if: always() + run: | + echo "::group::values.yaml" + yq e '... comments=""' charts/connaisseur/values.yaml + echo "::endgroup::" + + optional-k8s-versions: + name: optional k8s versions + runs-on: ubuntu-latest + if: | + inputs.skip != 'non-required' && + inputs.skip != 'all' + # permissions: #TODO: reactivate for non-private + # packages: read + env: + IMAGE: ${{ inputs.build_image_repository }} + TAG: ${{ inputs.build_tag }} + COSIGN_PUBLIC_KEY: ${{ inputs.cosign_public_key }} + strategy: + fail-fast: false + matrix: + k8s-version: [ + "v1.20", + "v1.21", + "v1.22", + "v1.23", + "v1.24", + ] + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Login with registry + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ inputs.build_registry }} + username: ${{ inputs.repo_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Install yq + run: | + sudo snap install yq + - uses: ./.github/actions/k8s-version-config + name: Setup k8s cluster + with: + k8s-version: ${{ matrix.k8s-version }} + - name: Run pre-config and workload integration tests + run: | + bash tests/integration/integration-test.sh "pre-and-workload" + - name: Display k8s state and logs if integration test failed + if: failure() + run: | + kubectl describe deployments.apps -n connaisseur -lapp.kubernetes.io/name=connaisseur + kubectl describe pods -n connaisseur -lapp.kubernetes.io/name=connaisseur + kubectl logs -n connaisseur -lapp.kubernetes.io/name=connaisseur --prefix=true --tail=-1 + - name: Display Connaisseur configuration + if: always() + run: | + echo "::group::values.yaml" + yq e '... comments=""' charts/connaisseur/values.yaml + echo "::endgroup::" + + self-hosted-notary: + name: self-hosted-notary + runs-on: ubuntu-latest + if: | + inputs.skip != 'non-required' && + inputs.skip != 'all' + permissions: + packages: read + env: + IMAGE: ${{ inputs.build_image_repository }} + TAG: ${{ inputs.build_tag }} + COSIGN_PUBLIC_KEY: ${{ inputs.cosign_public_key }} + strategy: + fail-fast: false + matrix: + integration-test-arg: + [ + "self-hosted-notary" + ] + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Login with registry + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ inputs.build_registry }} + username: ${{ inputs.repo_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Install yq + run: | + sudo snap install yq + - name: Setup notary signer instance + run: | + docker run -d -p 7899:7899 -v ./tests/data/notary_service_container/signer:/etc/docker/notary-signer/ notary:signer -config=/etc/docker/notary-signer/config.json + - name: Get notary signer instance IP + id: get_notary_signer_ip + uses: ./.github/actions/notary-signer-ip + - name: Setup notary server instance + run: | + docker run -d -p 4443:4443 --add-host notary.signer:${{ steps.get_notary_signer_ip.outputs.notary_signer_ip }} -v ./tests/data/notary_service_container/server:/etc/docker/notary-server notary:server -config=/etc/docker/notary-server/config.json -logf=json + - name: Get container IPs + id: get_notary_server_ip + uses: ./.github/actions/notary-server-ip + - name: Populate notary instance with trust data + uses: ./.github/actions/setup-notary + id: setup_notary + env: + NOTARY_IP: ${{ steps.get_notary_server_ip.outputs.notary_ip }} + - uses: ./.github/actions/k8s-version-config + name: Setup k8s cluster + with: + k8s-version: v1.28 + - name: Run test + run: | + bash tests/integration/integration-test.sh "${{ matrix.integration-test-arg }}" + env: + NOTARY_IP: ${{ steps.get_notary_server_ip.outputs.notary_ip }} + - name: Display Connaisseur configuration + if: always() + run: | + echo "::group::values.yaml" + yq e '... comments=""' charts/semgr8s/values.yaml + echo "::endgroup::" + - name: Display k8s state if integration test failed + if: failure() + run: | + kubectl describe deployments.apps -n connaisseur -lapp.kubernetes.io/name=connaisseur + kubectl describe pods -n connaisseur -lapp.kubernetes.io/name=connaisseur + - name: Display logs if integration test failed + if: failure() + run: | + kubectl logs -n connaisseur -lapp.kubernetes.io/name=connaisseur --prefix=true --tail=-1 diff --git a/.github/workflows/.reusable-sast.yml b/.github/workflows/.reusable-sast.yml new file mode 100644 index 0000000..e673831 --- /dev/null +++ b/.github/workflows/.reusable-sast.yml @@ -0,0 +1,227 @@ +name: sast + +on: + workflow_call: + inputs: + skip: + description: "Want to skip running certain jobs 'none', 'non-required', 'all'?" + type: string + default: "none" + output: + description: 'Output either "sarif" (GITHUB_TOKEN with security-events:write) or print results as "table" and fail on error' + type: string + required: false + default: 'sarif' + +#permissions: {} #TODO: reactivate for non-private + +jobs: + checkov: + runs-on: ubuntu-latest + if: | + (github.actor != 'dependabot[bot]') && + inputs.skip != 'non-required' && + inputs.skip != 'all' + # permissions: #TODO: reactivate for non-private + # security-events: write + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Render Helm charts + run: | + rm -rf test # remove 'test' folder from scan #TODO: fix once final + rm -rf tests # remove 'tests' folder from scan + mkdir deployment + helm template charts/connaisseur > deployment/deployment.yaml + shell: bash + - name: Scan + if: inputs.output == 'table' + uses: bridgecrewio/checkov-action@558f721c4bd65a6fc59b02448ffc792eb721cb9b # v12.2580.0 + with: + output_format: cli + soft_fail: false + - name: Scan + if: inputs.output == 'sarif' + uses: bridgecrewio/checkov-action@558f721c4bd65a6fc59b02448ffc792eb721cb9b # v12.2580.0 + with: + output_file_path: console,checkov-results.sarif + output_format: cli,sarif + soft_fail: true + - name: Upload + if: inputs.output == 'sarif' + uses: github/codeql-action/upload-sarif@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 + with: + sarif_file: checkov-results.sarif + + codeql: + runs-on: ubuntu-latest + if: | + (github.actor != 'dependabot[bot]') && + inputs.skip != 'non-required' && + inputs.skip != 'all' && + inputs.output == 'sarif' + # permissions: #TODO: reactivate for non-private + # pull-requests: read + # security-events: write + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Initialize CodeQL + uses: github/codeql-action/init@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 + with: + languages: 'go' + - name: Analyze + uses: github/codeql-action/analyze@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 + + golangci-lint: + runs-on: ubuntu-latest + if: | + (github.actor != 'dependabot[bot]') && + inputs.skip != 'all' + # permissions: #TODO: reactivate for non-private + # security-events: write + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + cache: false + go-version: '1.21' + - name: Analyze + uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3.7.0 + with: + version: latest + args: '--timeout=10m --skip-dirs="test" --tests=false' + + gosec: + runs-on: ubuntu-latest + if: | + (github.actor != 'dependabot[bot]') && + inputs.skip != 'all' + # permissions: #TODO: reactivate for non-private + # security-events: write + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Analyze + uses: securego/gosec@55d79496019a560e16e73e1948dee20a1fad631a # v2.18.2 + if: inputs.output == 'table' + with: + args: '-fmt text -exclude-dir=test -exclude-dir=tools ./...' + - name: Analyze + uses: securego/gosec@55d79496019a560e16e73e1948dee20a1fad631a # v2.18.2 + if: inputs.output == 'sarif' + with: + args: '-exclude-dir=test -exclude-dir=tools -no-fail -fmt sarif -out gosec-results.sarif ./...' + - name: Upload + uses: github/codeql-action/upload-sarif@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 + if: inputs.output == 'sarif' + with: + sarif_file: 'gosec-results.sarif' + + hadolint: + runs-on: ubuntu-latest + if: | + (github.actor != 'dependabot[bot]') && + inputs.skip != 'non-required' && + inputs.skip != 'all' + # permissions: #TODO: reactivate for non-private + # security-events: write + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Scan + uses: hadolint/hadolint-action@54c9adbab1582c2ef04b2016b760714a4bfde3cf # v3.1.0 + if: inputs.output == 'table' + with: + dockerfile: build/Dockerfile + format: tty + no-fail: false + - name: Scan + uses: hadolint/hadolint-action@54c9adbab1582c2ef04b2016b760714a4bfde3cf # v3.1.0 + if: inputs.output == 'sarif' + with: + dockerfile: build/Dockerfile + format: sarif + no-fail: true + output-file: hadolint-results.sarif + - name: Upload + uses: github/codeql-action/upload-sarif@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 + if: inputs.output == 'sarif' + with: + sarif_file: 'hadolint-results.sarif' + + kubelinter: + runs-on: ubuntu-latest + if: | + (github.actor != 'dependabot[bot]') && + inputs.skip != 'non-required' && + inputs.skip != 'all' + # permissions: #TODO: reactivate for non-private + # security-events: write + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Scan + uses: stackrox/kube-linter-action@ca0d55b925470deb5b04b556e6c4276ea94d03c3 # v1.0.4 + if: inputs.output == 'table' + with: + config: .kube-linter/config.yaml + directory: charts/connaisseur + format: plain + - name: Scan + uses: stackrox/kube-linter-action@ca0d55b925470deb5b04b556e6c4276ea94d03c3 # v1.0.4 + if: inputs.output == 'sarif' + with: + config: .kube-linter/config.yaml + directory: charts/connaisseur + format: sarif + output-file: kubelinter-results.sarif + - name: Upload + uses: github/codeql-action/upload-sarif@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 + if: inputs.output == 'sarif' + with: + sarif_file: 'kubelinter-results.sarif' + + semgrep: + runs-on: ubuntu-latest + if: | + (github.actor != 'dependabot[bot]') && + inputs.skip != 'non-required' && + inputs.skip != 'all' + # permissions: #TODO: reactivate for non-private + # security-events: write + container: + image: returntocorp/semgrep + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Scan + if: inputs.output == 'table' + run: semgrep ci --config=auto --suppress-errors --text + - name: Scan + if: inputs.output == 'sarif' + run: semgrep ci --config=auto --suppress-errors --sarif --output=semgrep-results.sarif || exit 0 + - name: Upload + uses: github/codeql-action/upload-sarif@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 + if: inputs.output == 'sarif' + with: + sarif_file: semgrep-results.sarif + + trivy-config-scan: + name: trivy config + runs-on: ubuntu-latest + if: | + (github.actor != 'dependabot[bot]') && + inputs.skip != 'non-required' && + inputs.skip != 'all' + # permissions: #TODO: reactivate for non-private + # security-events: write + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Run Trivy + uses: ./.github/actions/trivy-config + with: + output: ${{ inputs.output }} + diff --git a/.github/workflows/.reusable-sca.yml b/.github/workflows/.reusable-sca.yml new file mode 100644 index 0000000..66c5d76 --- /dev/null +++ b/.github/workflows/.reusable-sca.yml @@ -0,0 +1,100 @@ +name: sca + +#permissions: {} #TODO: reactivate for non-private + +on: + workflow_call: + inputs: + image: + description: "Image used for testing, i.e. registry + repository + tag" + type: string + required: true + registry: + description: 'Registry to login to pull image, e.g. "ghcr.io" for GHCR, leave empty if image is public' + type: string + required: false + default: '' + repo_owner: + description: 'Name of repository owner, e.g. "github.repository_owner" for ghcr.io' + type: string + required: false + default: '' + skip: + description: "Want to skip running certain jobs 'none', 'non-required', 'all'?" + type: string + default: "none" + output: + description: 'Output either "sarif" (GITHUB_TOKEN with security-events:write) or print results as "table" and fail on error' + type: string + required: false + default: 'sarif' + +jobs: + trivy-image-scan: + name: trivy image + runs-on: ubuntu-latest + if: inputs.skip != 'all' + # permissions: #TODO: reactivate for non-private + # packages: read + # security-events: write + container: + image: docker:stable + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Run + uses: ./.github/actions/trivy-image + with: + image: ${{ inputs.image }} + registry: ${{ inputs.registry }} + repo_owner: ${{ inputs.repo_owner }} + repo_token: ${{ secrets.GITHUB_TOKEN }} + output: ${{ inputs.output }} + + grype: + name: grype + runs-on: ubuntu-latest + if: | + inputs.skip != 'non-required' && + inputs.skip != 'all' + # permissions: #TODO: reactivate for non-private + # packages: read + # security-events: write + container: + image: docker:stable + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Run + uses: ./.github/actions/grype + with: + image: ${{ inputs.image }} + registry: ${{ inputs.registry }} + repo_owner: ${{ inputs.repo_owner }} + repo_token: ${{ secrets.GITHUB_TOKEN }} + output: ${{ inputs.output }} + +# WIP: Syft issue seems to cause error (https://github.com/anchore/syft/issues/1622) + dependency-submission: + name: syft / dependency review + runs-on: ubuntu-latest + if: | + inputs.skip != 'non-required' && + inputs.skip != 'all' + # permissions: #TODO: reactivate for non-private + # packages: read + # contents: write + steps: + - name: Login with registry + if: inputs.registry != '' + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ inputs.registry }} + username: ${{ inputs.repo_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Run + uses: anchore/sbom-action@78fc58e266e87a38d4194b2137a3d4e9bcaf7ca1 # v0.14.3 + with: + image: ${{ inputs.image }} + format: cyclonedx-json + dependency-snapshot: ${{ inputs.output == 'sarif' }} diff --git a/.github/workflows/.reusable-unit-test.yml b/.github/workflows/.reusable-unit-test.yml new file mode 100644 index 0000000..b33fb28 --- /dev/null +++ b/.github/workflows/.reusable-unit-test.yml @@ -0,0 +1,32 @@ +name: unit-test + +#permissions: {} #TODO: reactivate for non-private + +on: + workflow_call: + inputs: + skip: + description: "Want to skip running certain jobs 'none', 'non-required', 'all'?" + type: string + default: "none" + +jobs: + gotest: + name: unit tests + runs-on: ubuntu-latest + if: inputs.skip != 'all' + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Setup + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version: '1.21' + - name: Test + if: inputs.skip == 'non-required' + run: go test ./cmd/... -coverprofile=coverage.out -covermode=atomic + - name: Test + if: inputs.skip != 'non-required' + run: go test ./... -race -coverprofile=coverage.out -covermode=atomic + - name: Upload + uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..413459d --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,29 @@ +name: pr + +#permissions: {} #TODO: reactivate for non-private + +on: + pull_request: + branches: + - main + - dev + +defaults: + run: + shell: bash + +jobs: + ci: + uses: ./.github/workflows/.reusable-ci.yml + # permissions: #TODO: adjust for non-private + secrets: inherit + with: + #TODO: adjust for non private + skip_build: 'none' + skip_compliance_checks: 'all' + skip_unit_tests: 'all' + skip_sast: 'all' + skip_sca: 'all' + skip_docs: 'all' + skip_integration_tests: 'all' + output_type: 'sarif' diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 0000000..ebd4cb8 --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,29 @@ +name: push + +#permissions: {} #TODO: reactivate for non-private + +on: + push: + branches: + - main + - dev + +defaults: + run: + shell: bash + +jobs: + ci: + uses: ./.github/workflows/.reusable-ci.yml + # permissions: #TODO: adjust for non-private + secrets: inherit + with: + #TODO: adjust for non private + skip_build: 'none' + skip_compliance_checks: 'none' + skip_unit_tests: 'all' + skip_sast: 'all' + skip_sca: 'all' + skip_docs: 'all' + skip_integration_tests: 'all' + output_type: 'sarif' diff --git a/Makefile b/Makefile index 22aa7d4..72f399c 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ webhookName := semgr8s -image := $(shell yq e '.deployment.image.repository' helm/values.yaml) -version := $(shell yq e '.appVersion' helm/Chart.yaml) +image := $(shell yq e '.deployment.image.repository' charts/semgr8s/values.yaml) +version := $(shell yq e '.appVersion' charts/semgr8s/Chart.yaml) tag := $(image):$(version) ns := semgr8ns diff --git a/README.md b/README.md index 79fe996..e6cb536 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,10 @@ cd semgr8s Semgr8s comes preconfigured with some basic rules. However, configuration can be adjusted to your needs: -- Central configuration is maintained in `helm/values.yaml`. +- Central configuration is maintained in `charts/semgr8s/values.yaml`. - Configuration aims to provide the most native integration of Semgrep's functionality into Kubernetes. Working knowledge of Kubernetes and the [Semgrep documentation](https://semgrep.dev/docs/) should be sufficient to understand the concepts and options being used here. -- [Remote Semgrep](https://registry.semgrep.dev/rule) rules, rulesets, [repository rules](https://github.com/returntocorp/semgrep-rules) are configured via `.application.remoteRules` in `helm/values.yaml`, e.g. set to `"r/yaml.kubernetes.security.allow-privilege-escalation.allow-privilege-escalation"` or `"p/kubernetes"`, or `"r/yaml.kubernetes"` respectively. -- [Custom Semgrep rules](https://semgrep.dev/docs/writing-rules/overview/) can placed in `helm/rules/` and will be auto-mounted into the admission controller. +- [Remote Semgrep](https://registry.semgrep.dev/rule) rules, rulesets, [repository rules](https://github.com/returntocorp/semgrep-rules) are configured via `.application.remoteRules` in `charts/semgr8s/values.yaml`, e.g. set to `"r/yaml.kubernetes.security.allow-privilege-escalation.allow-privilege-escalation"` or `"p/kubernetes"`, or `"r/yaml.kubernetes"` respectively. +- [Custom Semgrep rules](https://semgrep.dev/docs/writing-rules/overview/) can placed in `charts/semgr8s/rules/` and will be auto-mounted into the admission controller. - Semgrep provides online tools to [learn](https://semgrep.dev/learn) and [create](https://semgrep.dev/playground/new) custom rules. To deploy the preconfigured admission controller simply run: @@ -119,7 +119,7 @@ Once all resources are in `READY` state, you have successfully installed semgr8s ### Testing Several test resources are provided under `tests/`. -Semgr8s denies creating pods with insecure configuration according to the rules in `helm/rules`: +Semgr8s denies creating pods with insecure configuration according to the rules in `charts/semgr8s/rules`: ```bash kubectl create -f tests/failing_deployment.yaml diff --git a/helm/Chart.yaml b/charts/semgr8s/Chart.yaml similarity index 100% rename from helm/Chart.yaml rename to charts/semgr8s/Chart.yaml diff --git a/helm/rules/allow-privilege-escalation-no-securitycontext.yaml b/charts/semgr8s/rules/allow-privilege-escalation-no-securitycontext.yaml similarity index 100% rename from helm/rules/allow-privilege-escalation-no-securitycontext.yaml rename to charts/semgr8s/rules/allow-privilege-escalation-no-securitycontext.yaml diff --git a/helm/rules/hostnetwork-pod.yaml b/charts/semgr8s/rules/hostnetwork-pod.yaml similarity index 100% rename from helm/rules/hostnetwork-pod.yaml rename to charts/semgr8s/rules/hostnetwork-pod.yaml diff --git a/helm/rules/privileged-container.yaml b/charts/semgr8s/rules/privileged-container.yaml similarity index 100% rename from helm/rules/privileged-container.yaml rename to charts/semgr8s/rules/privileged-container.yaml diff --git a/helm/rules/run-as-non-root.yaml b/charts/semgr8s/rules/run-as-non-root.yaml similarity index 100% rename from helm/rules/run-as-non-root.yaml rename to charts/semgr8s/rules/run-as-non-root.yaml diff --git a/helm/templates/NOTES.txt b/charts/semgr8s/templates/NOTES.txt similarity index 100% rename from helm/templates/NOTES.txt rename to charts/semgr8s/templates/NOTES.txt diff --git a/helm/templates/_helpers.tpl b/charts/semgr8s/templates/_helpers.tpl similarity index 100% rename from helm/templates/_helpers.tpl rename to charts/semgr8s/templates/_helpers.tpl diff --git a/helm/templates/deployment.yaml b/charts/semgr8s/templates/deployment.yaml similarity index 100% rename from helm/templates/deployment.yaml rename to charts/semgr8s/templates/deployment.yaml diff --git a/helm/templates/env.yaml b/charts/semgr8s/templates/env.yaml similarity index 100% rename from helm/templates/env.yaml rename to charts/semgr8s/templates/env.yaml diff --git a/helm/templates/role.yaml b/charts/semgr8s/templates/role.yaml similarity index 100% rename from helm/templates/role.yaml rename to charts/semgr8s/templates/role.yaml diff --git a/helm/templates/rolebinding.yaml b/charts/semgr8s/templates/rolebinding.yaml similarity index 100% rename from helm/templates/rolebinding.yaml rename to charts/semgr8s/templates/rolebinding.yaml diff --git a/helm/templates/rules.yaml b/charts/semgr8s/templates/rules.yaml similarity index 100% rename from helm/templates/rules.yaml rename to charts/semgr8s/templates/rules.yaml diff --git a/helm/templates/service.yaml b/charts/semgr8s/templates/service.yaml similarity index 100% rename from helm/templates/service.yaml rename to charts/semgr8s/templates/service.yaml diff --git a/helm/templates/serviceaccount.yaml b/charts/semgr8s/templates/serviceaccount.yaml similarity index 100% rename from helm/templates/serviceaccount.yaml rename to charts/semgr8s/templates/serviceaccount.yaml diff --git a/helm/templates/webhook.yaml b/charts/semgr8s/templates/webhook.yaml similarity index 100% rename from helm/templates/webhook.yaml rename to charts/semgr8s/templates/webhook.yaml diff --git a/helm/values.yaml b/charts/semgr8s/values.yaml similarity index 100% rename from helm/values.yaml rename to charts/semgr8s/values.yaml diff --git a/helm/.helmignore b/helm/.helmignore deleted file mode 100644 index 0e8a0eb..0000000 --- a/helm/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/