diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..ec9fdc41 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +# directories +**/bin/ +**/obj/ +**/out/ + +# files +Dockerfile* +**/*.trx +**/*.md +**/*.ps1 +**/*.cmd +**/*.sh diff --git a/.github/workflows/docker-images-single.yml b/.github/workflows/docker-images-single.yml new file mode 100644 index 00000000..4a3913f0 --- /dev/null +++ b/.github/workflows/docker-images-single.yml @@ -0,0 +1,376 @@ +name: Single Build Docker + +on: + push: + branches: + - main + tags: + - v* + pull_request: + branches: + - main + + workflow_dispatch: + inputs: + contrast_agent_version: + description: 'Contrast .NET Core agent version to build with' + required: false + default: 'latest' + +jobs: + prepare: + name: Prepare for multi-stage builds + runs-on: ubuntu-latest + outputs: + contrast_version: ${{ steps.versions.outputs.contrast_version }} + docker-meta-json: ${{ steps.docker-meta.outputs.json }} + docker-meta-bake-file: ${{ steps.docker-meta.outputs.bake-file }} + steps: + - name: Check latest Contrast agent version + id: versions + run: | + docker pull contrast/agent-dotnet-core:${{ github.event.inputs.contrast_agent_version || 'latest' }} + CONTRAST_VERSION=$(docker image inspect contrast/agent-dotnet-core:latest --format '{{ index .Config.Labels "org.opencontainers.image.version" }}') + echo "Contrast agent version: ${CONTRAST_VERSION}" + echo "contrast_version=${CONTRAST_VERSION}" >> $GITHUB_OUTPUT + + + # MULTISTAGE DOCKERFILE - first create and push the build stage to speed up + # subsequent builds. + docker-build: + name: Docker Build Images + runs-on: ubuntu-latest + needs: + - prepare + permissions: + contents: read + packages: write + attestations: write + id-token: write + outputs: + image-name-base: ${{ steps.inspect.outputs['image-name-base'] }} + image-digest-base: ${{ steps.inspect.outputs['image-digest-base'] }} + steps: + - name: Checkout branch + uses: actions/checkout@v4 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker daemon for multi-platform builds + uses: docker/setup-docker-action@v4 + with: + daemon-config: | + { + "debug": true, + "features": { + "containerd-snapshotter": true + } + } + + - name: Docker Setup QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + + - name: Docker Setup Buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract metadata + id: docker-meta + uses: docker/metadata-action@v5 + env: + DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index + with: + images: | + ghcr.io/${{ github.repository }} + flavor: | + latest=false + annotations: | + org.opencontainers.image.vendor=Contrast Security + tags: | + type=ref,event=branch + type=ref,event=pr + + - name: Check tags + run: | + echo "Tags: ${{ steps.docker-meta.outputs.tags }}" + echo "JSON: ${{ steps.docker-meta.outputs.json }}" + echo "Bake file: ${{ steps.docker-meta.outputs.bake-file }}" + + # To speed up the builds, we use caching of the docker image layers. + # Cache rules: + # - Use the existing base image cache from the main branch first for new PRs + # - After the first build in a PR, cache-to will save the cache to the registry under the PR name + # - Subsequent builds in the same PR will use the cache from the PR name + # - Otherwise, different PRs would cause cache pollution if they share the same base image cache + - name: Build the base docker image for this PR + id: bake-base-pr + uses: docker/bake-action@v6 + with: + files: | + docker-bake.hcl + cwd://${{ steps.docker-meta.outputs.bake-file }} + targets: | + build + push: true + set: | + *.platform=linux/amd64 + *.platform=linux/arm64 + *.cache-from=type=registry,ref=ghcr.io/${{ github.repository }}:base-buildcache + *.cache-from=type=registry,ref=${{ steps.docker-meta.outputs.json.tags }}-buildcache + *.cache-to=type=registry,ref=${{ steps.docker-meta.outputs.json.tags }}-buildcache,mode=max + + - name: Inspect the created images + id: inspect + run: | + image_name='${{ fromJSON(steps.bake-base-pr.outputs.metadata).build['image.name'] }}' + image_digest='${{ fromJSON(steps.bake-base-pr.outputs.metadata).build['containerimage.digest'] }}' + + echo "Image Name: $image_name" + echo "Image Digest: $image_digest" + echo "Image Artifact: $image_name@$image_digest" + + echo "image-name-base=$image_name" >> $GITHUB_OUTPUT + echo "image-digest-base=$image_digest" >> $GITHUB_OUTPUT + echo "image-artifact-base=$image_name@$image_digest" >> $GITHUB_OUTPUT + + - name: Adding markdown + run: | + echo '### Internal Docker Images ready for testing! 🚀' >> $GITHUB_STEP_SUMMARY + echo ' - $image_name' >> $GITHUB_STEP_SUMMARY + + # - name: Build all Docker images for this PR + # id: bake-pr + # uses: docker/bake-action@v6 + # with: + # files: | + # docker-bake.hcl + # cwd://${{ steps.docker-meta.outputs.bake-file}} + # push: false + # set: | + # *.platform=linux/amd64 + # *.platform=linux/arm64 + # *.cache-from=type=registry,ref=ghcr.io/${{ github.repository }}:${{ matrix.image.name }}-buildcache + # *.cache-from=type=registry,ref=${{ needs.build-base.outputs.image-name-base }}-buildcache + # *.cache-to=type=registry,ref=${{ needs.build-base.outputs.image-name-base }}-buildcache,mode=max + + # - name: Inspect the created images + # id: inspect-pr + # run: | + + # matrix_image_name='${{ matrix.image.name }}' + # echo "Matrix Image Name: $matrix_image_name" + # image_name='${{ fromJSON(steps.bake-pr.outputs.metadata)[matrix.image.name]['image.name'][0] }}' + # image_digest='${{ fromJSON(steps.bake-pr.outputs.metadata)[matrix.image.name]['containerimage.digest'] }}' + + # echo "Image Name: $image_name" + # echo "Image Digest: $image_digest" + # echo "Image Artifact: $image_name@$image_digest" + + # echo "image-name-${{ matrix.image.name }}=$image_name" >> $GITHUB_OUTPUT + # echo "image-digest-${{ matrix.image.name }}=$image_digest" >> $GITHUB_OUTPUT + # echo "image-artifact-${{ matrix.image.name }}=$image_name@$image_digest" >> $GITHUB_OUTPUT + + #TODO: Then share the base image with other jobs via cache + + # build: + # name: Docker Build ${{ matrix.image.name }} + # needs: + # - prepare + # - build-base + # runs-on: ubuntu-latest + # permissions: + # contents: read + # packages: write + # attestations: write + # id-token: write + # strategy: + # fail-fast: false + # matrix: + # image: + # - name: runtime + # - name: runtime-with-contrast + # - name: tests + # outputs: + # image-name-runtime: ${{ steps.inspect.outputs['image-name-runtime'] }} + # image-digest-runtime: ${{ steps.inspect.outputs['image-digest-runtime'] }} + # image-artifact-runtime: ${{ steps.inspect.outputs['image-artifact-runtime'] }} + # image-name-runtime-with-contrast: ${{ steps.inspect.outputs['image-name-runtime-with-contrast'] }} + # image-digest-runtime-with-contrast: ${{ steps.inspect.outputs['image-digest-runtime-with-contrast'] }} + # image-artifact-runtime-with-contrast: ${{ steps.inspect.outputs['image-artifact-runtime-with-contrast'] }} + # image-name-tests: ${{ steps.inspect.outputs['image-name-tests'] }} + # image-digest-tests: ${{ steps.inspect.outputs['image-digest-tests'] }} + # image-artifact-tests: ${{ steps.inspect.outputs['image-artifact-tests'] }} + # steps: + # - name: Checkout branch + # uses: actions/checkout@v4 + + # - name: Login to GHCR + # uses: docker/login-action@v3 + # with: + # registry: ghcr.io + # username: ${{ github.actor }} + # password: ${{ secrets.GITHUB_TOKEN }} + + # - name: Set up Docker daemon for multi-platform builds + # uses: docker/setup-docker-action@v4 + # with: + # daemon-config: | + # { + # "debug": true, + # "features": { + # "containerd-snapshotter": true + # } + # } + + # - name: Docker Setup QEMU + # uses: docker/setup-qemu-action@v3 + # with: + # platforms: arm64 + + # - name: Docker Setup Buildx + # uses: docker/setup-buildx-action@v3 + + # - name: Extract metadata + # id: docker-meta + # uses: docker/metadata-action@v5 + # with: + # images: | + # ghcr.io/${{ github.repository }} + # flavor: | + # latest=false + # annotations: | + # org.opencontainers.image.description=A deliberately vulnerable .NET Core Application for Contrast Security demos + # org.opencontainers.image.vendor=Contrast Security + # tags: | + # type=ref,event=branch,suffix=-${{ matrix.image.name }} + # type=ref,event=branch,suffix=-${{ matrix.image.name }}${{ matrix.image.name == 'runtime-with-contrast' && needs.prepare.outputs.contrast_version || null }} + # type=ref,event=pr,suffix=-${{ matrix.image.name }} + # type=ref,event=pr,suffix=-${{ matrix.image.name }}${{ matrix.image.name == 'runtime-with-contrast' && needs.prepare.outputs.contrast_version || null }} + + # - name: Pull base image + # run: | + # echo "Pulling base image: ${{ needs.build-base.outputs.image-name-base }}" + # docker pull '${{ needs.build-base.outputs.image-name-base }}' + + # - name: Build all Docker images for this PR + # id: bake-pr + # uses: docker/bake-action@v6 + # with: + # files: | + # docker-bake.hcl + # cwd://${{ steps.docker-meta.outputs.bake-file}} + # targets: | + # ${{ matrix.image.name }} + # push: true + # set: | + # *.platform=linux/amd64 + # *.platform=linux/arm64 + # *.cache-from=type=registry,ref=ghcr.io/${{ github.repository }}:${{ matrix.image.name }}-buildcache + # *.cache-from=type=registry,ref=${{ needs.build-base.outputs.image-name-base }}-buildcache + # *.cache-to=type=registry,ref=${{ needs.build-base.outputs.image-name-base }}-buildcache,mode=max + + # - name: Inspect the created images + # id: inspect + # run: | + + # matrix_image_name='${{ matrix.image.name }}' + # echo "Matrix Image Name: $matrix_image_name" + # image_name='${{ fromJSON(steps.bake-pr.outputs.metadata)[matrix.image.name]['image.name'][0] }}' + # image_digest='${{ fromJSON(steps.bake-pr.outputs.metadata)[matrix.image.name]['containerimage.digest'] }}' + + # echo "Image Name: $image_name" + # echo "Image Digest: $image_digest" + # echo "Image Artifact: $image_name@$image_digest" + + # echo "image-name-${{ matrix.image.name }}=$image_name" >> $GITHUB_OUTPUT + # echo "image-digest-${{ matrix.image.name }}=$image_digest" >> $GITHUB_OUTPUT + # echo "image-artifact-${{ matrix.image.name }}=$image_name@$image_digest" >> $GITHUB_OUTPUT + + # # - name: Login to Docker Hub + # # uses: docker/login-action@v3 + # # with: + # # username: ${{ secrets.DOCKERHUB_USERNAME }} + # # password: ${{ secrets.DOCKERHUB_TOKEN }} + + + + # test: + # needs: + # - build + # uses: ./.github/workflows/e2e-tests.yml + # with: + # staging_image_runtime: ${{ needs.build.outputs.image-name-runtime }} + # staging_image_runtime_with_contrast: ${{ needs.build.outputs.image-name-runtime-with-contrast }} + # staging_image_tests: ${{ needs.build.outputs.image-name-tests }} + + # # test-contrast: + # # needs: + # # - build + # # uses: ./.github/workflows/e2e-tests.yml + # # with: + # # staging_image_runtime: ${{ needs.build.outputs.image-name-runtime-with-contrast }} + # # staging_image_tests: ${{ needs.build.outputs.image-name-tests }} + # # secrets: + # # contrast_api_token: ${{ secrets.CONTRAST_API_TOKEN }} + + + # # + # # Release Internal + # # + # release-internal: + # needs: + # - prepare + # - build + # - test + # runs-on: ubuntu-latest + # permissions: + # contents: read + # packages: write + # attestations: write + # id-token: write + # strategy: + # fail-fast: false + # matrix: + # image: + # - name: runtime + # - name: runtime-with-contrast + # - name: tests + # steps: + + # - name: Extract metadata + # id: release-meta + # uses: docker/metadata-action@v5 + # with: + # images: | + # ghcr.io/${{ github.repository }} + # flavor: | + # latest=true + # annotations: | + # org.opencontainers.image.description=A deliberately vulnerable .NET Core Application for Contrast Security demos + # org.opencontainers.image.vendor=Contrast Security + # tags: | + # type=semver,pattern={{version}},prefix=contrast,value=${{ needs.prepare.outputs.contrast_version }},enable=${{ matrix.image.name == 'runtime-with-contrast' }} + # type=semver,pattern={{major}}.{{minor}},prefix=contrast,value=${{ needs.prepare.outputs.contrast_version }},enable=${{ matrix.image.name == 'runtime-with-contrast' }} + # type=semver,pattern={{major}},prefix=contrast,value=${{ needs.prepare.outputs.contrast_version }},enable=${{ matrix.image.name == 'runtime-with-contrast' }} + # type=raw,value=${{ matrix.image.name == 'runtime-with-contrast' && 'latest-contrast' || 'latest' }},enable=${{ matrix.image.name != 'tests' }} + # type=raw,value=tests,enable=${{ matrix.image.name == 'tests' }} + + # - name: Login to GHCR + # uses: docker/login-action@v3 + # with: + # registry: ghcr.io + # username: ${{ github.actor }} + # password: ${{ secrets.GITHUB_TOKEN }} + + # - name: Release image (internal) + # uses: akhilerm/tag-push-action@v2.1.0 + # with: + # src: ${{ needs.build.outputs[format('image-name-{0}', matrix.image.name)] }} + # dst: | + # ${{ steps.release-meta.outputs.tags }} diff --git a/.github/workflows/docker-images.yml b/.github/workflows/docker-images.yml index 55976d2d..36fce6f5 100644 --- a/.github/workflows/docker-images.yml +++ b/.github/workflows/docker-images.yml @@ -9,175 +9,333 @@ on: pull_request: branches: - main + workflow_dispatch: + inputs: + contrast_agent_version: + description: 'Contrast .NET Core agent version to build with' + required: false + default: 'latest' jobs: + prepare: + name: Prepare for multi-stage builds + runs-on: ubuntu-latest + outputs: + contrast_version: ${{ steps.versions.outputs.contrast_version }} + steps: + - name: Check latest Contrast agent version + id: versions + run: | + docker pull contrast/agent-dotnet-core:${{ github.event.inputs.contrast_agent_version || 'latest' }} + CONTRAST_VERSION=$(docker image inspect contrast/agent-dotnet-core:latest --format '{{ index .Config.Labels "org.opencontainers.image.version" }}') + echo "Contrast agent version: ${CONTRAST_VERSION}" + echo "contrast_version=${CONTRAST_VERSION}" >> $GITHUB_OUTPUT + # MULTISTAGE DOCKERFILE - first create and push the build stage to speed up + # subsequent builds. build-base: - name: Docker Build Base (no-agent) + name: Create Docker Build Stage runs-on: ubuntu-latest + needs: + - prepare + permissions: + contents: read + packages: write + attestations: write + id-token: write + outputs: + image-name-base: ${{ steps.inspect.outputs['image-name-base'] }} + image-digest-base: ${{ steps.inspect.outputs['image-digest-base'] }} steps: - - - name: Checkout branch + - name: Checkout branch uses: actions/checkout@v4 - - - name: Docker Setup QEMU + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker daemon for multi-platform builds + uses: docker/setup-docker-action@v4 + with: + daemon-config: | + { + "debug": true, + "features": { + "containerd-snapshotter": true + } + } + + - name: Docker Setup QEMU uses: docker/setup-qemu-action@v3 with: - platforms: all - - - name: Docker Setup Buildx + platforms: arm64 + + - name: Docker Setup Buildx uses: docker/setup-buildx-action@v3 - with: - platforms: linux/amd64,linux/arm64 - - - name: Create cache for docker images for use in the next job - uses: actions/cache@v4 + + - name: Extract metadata + id: docker-meta + uses: docker/metadata-action@v5 + env: + DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index with: - key: latest-no-agent - path: ${{ runner.temp }} - - - name: Build and push Docker images - uses: docker/build-push-action@v5 - with: - push: false - load: true - cache-from: type=gha - cache-to: type=gha,mode=max - tags: contrastsecuritydemo/netflicks:latest-no-agent - outputs: type=docker,dest=${{ runner.temp }}/latest-no-agent.tar + images: | + ghcr.io/${{ github.repository }} + flavor: | + latest=false + annotations: | + org.opencontainers.image.description=Base image - build stage and buildcache for the netflicks application + org.opencontainers.image.vendor=Contrast Security + tags: | + type=ref,event=branch,suffix=-base + type=ref,event=branch,suffix=-base + type=ref,event=pr,suffix=-base + type=ref,event=pr,suffix=-base + + # To speed up the builds, we use caching of the docker image layers. + # Cache rules: + # - Use the existing base image cache from the main branch first for new PRs + # - After the first build in a PR, cache-to will save the cache to the registry under the PR name + # - Subsequent builds in the same PR will use the cache from the PR name + # - Otherwise, different PRs would cause cache pollution if they share the same base image cache + - name: Build the base docker image for this PR + id: bake-base-pr + uses: docker/bake-action@v6 + with: + files: | + docker-bake.hcl + cwd://${{ steps.docker-meta.outputs.bake-file }} + targets: | + build + push: true + set: | + *.platform=linux/amd64 + *.platform=linux/arm64 + *.cache-from=type=registry,ref=ghcr.io/${{ github.repository }}:base-buildcache + *.cache-from=type=registry,ref=${{ steps.docker-meta.outputs.tags }}-buildcache + *.cache-to=type=registry,ref=${{ steps.docker-meta.outputs.tags }}-buildcache,mode=max + + - name: Inspect the created images + id: inspect + run: | + image_name='${{ fromJSON(steps.bake-base-pr.outputs.metadata).build['image.name'] }}' + image_digest='${{ fromJSON(steps.bake-base-pr.outputs.metadata).build['containerimage.digest'] }}' + echo "Image Name: $image_name" + echo "Image Digest: $image_digest" + echo "Image Artifact: $image_name@$image_digest" - - build-contrast: - name: Docker Build Contrast (agent) - runs-on: ubuntu-latest + echo "image-name-base=$image_name" >> $GITHUB_OUTPUT + echo "image-digest-base=$image_digest" >> $GITHUB_OUTPUT + echo "image-artifact-base=$image_name@$image_digest" >> $GITHUB_OUTPUT + + #TODO: Then share the base image with other jobs via cache + + build: + name: Docker Build ${{ matrix.image.name }} needs: + - prepare - build-base + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + attestations: write + id-token: write + strategy: + fail-fast: false + matrix: + image: + - name: runtime + - name: runtime-with-contrast + - name: tests + outputs: + image-name-runtime: ${{ steps.inspect.outputs['image-name-runtime'] }} + image-digest-runtime: ${{ steps.inspect.outputs['image-digest-runtime'] }} + image-artifact-runtime: ${{ steps.inspect.outputs['image-artifact-runtime'] }} + image-name-runtime-with-contrast: ${{ steps.inspect.outputs['image-name-runtime-with-contrast'] }} + image-digest-runtime-with-contrast: ${{ steps.inspect.outputs['image-digest-runtime-with-contrast'] }} + image-artifact-runtime-with-contrast: ${{ steps.inspect.outputs['image-artifact-runtime-with-contrast'] }} + image-name-tests: ${{ steps.inspect.outputs['image-name-tests'] }} + image-digest-tests: ${{ steps.inspect.outputs['image-digest-tests'] }} + image-artifact-tests: ${{ steps.inspect.outputs['image-artifact-tests'] }} steps: - - - name: Checkout branch + - name: Checkout branch uses: actions/checkout@v4 - - - name: Docker Setup QEMU + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker daemon for multi-platform builds + uses: docker/setup-docker-action@v4 + with: + daemon-config: | + { + "debug": true, + "features": { + "containerd-snapshotter": true + } + } + + - name: Docker Setup QEMU uses: docker/setup-qemu-action@v3 with: - platforms: all - - - name: Docker Setup Buildx + platforms: arm64 + + - name: Docker Setup Buildx uses: docker/setup-buildx-action@v3 - with: - platforms: linux/amd64,linux/arm64 - - - name: Create cache for docker images for use in the next job - uses: actions/cache@v4 - with: - key: latest - path: ${{ runner.temp }} - - - name: Build and push Docker images - uses: docker/build-push-action@v5 - with: - push: false - load: true - tags: contrastsecuritydemo/netflicks:latest - outputs: type=docker,dest=${{ runner.temp }}/latest.tar - - test: - name: Run Tests - runs-on: ubuntu-latest - needs: - - build-base - - build-contrast - steps: - - - name: Restore cached docker images - uses: actions/cache/restore@v4 - with: - path: ${{ runner.temp }} - key: latest-no-agent - - - name: Restore cached docker images - uses: actions/cache/restore@v4 + - name: Extract metadata + id: docker-meta + uses: docker/metadata-action@v5 with: - path: ${{ runner.temp }} - key: latest - - - name: Load images + images: | + ghcr.io/${{ github.repository }} + flavor: | + latest=false + annotations: | + org.opencontainers.image.description=A deliberately vulnerable .NET Core Application for Contrast Security demos + org.opencontainers.image.vendor=Contrast Security + tags: | + type=ref,event=branch,suffix=-${{ matrix.image.name }} + type=ref,event=pr,suffix=-${{ matrix.image.name }} + + - name: Pull base image run: | - docker load --input ${{ runner.temp }}/latest-no-agent.tar - docker load --input ${{ runner.temp }}/latest.tar - - - name: Checkout branch - uses: actions/checkout@v4 - - - name: Run docker-compose tests - run: | - docker compose up -d - - - name: Setup Node - uses: actions/setup-node@v4 + echo "Pulling base image: ${{ needs.build-base.outputs.image-name-base }}" + docker pull '${{ needs.build-base.outputs.image-name-base }}' + + - name: Build all Docker images for this PR + id: bake-pr + uses: docker/bake-action@v6 with: - node-version: lts/* - - - name: Install dependencies - run: | - cd tests - npm ci - - - - name: Install playwright browsers - run: | - cd tests - npx playwright install --with-deps chromium - - - name: Run Playwright tests + files: | + docker-bake.hcl + cwd://${{ steps.docker-meta.outputs.bake-file}} + targets: | + ${{ matrix.image.name }} + push: true + set: | + *.platform=linux/amd64 + *.platform=linux/arm64 + *.cache-from=type=registry,ref=ghcr.io/${{ github.repository }}:${{ matrix.image.name }}-buildcache + *.cache-from=type=registry,ref=${{ needs.build-base.outputs.image-name-base }}-buildcache + *.cache-to=type=registry,ref=${{ needs.build-base.outputs.image-name-base }}-buildcache,mode=max + + - name: Inspect the created images + id: inspect run: | - cd tests - npx playwright test assess/*.spec.ts - - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: playwright-report - path: tests/playwright-report/ - retention-days: 30 + + matrix_image_name='${{ matrix.image.name }}' + echo "Matrix Image Name: $matrix_image_name" + image_name='${{ fromJSON(steps.bake-pr.outputs.metadata)[matrix.image.name]['image.name'] }}' + image_digest='${{ fromJSON(steps.bake-pr.outputs.metadata)[matrix.image.name]['containerimage.digest'] }}' - pre-merge: - name: Prepare to merge - runs-on: ubuntu-latest + echo "Image Name: $image_name" + echo "Image Digest: $image_digest" + echo "Image Artifact: $image_name@$image_digest" + + echo "image-name-${{ matrix.image.name }}=$image_name" >> $GITHUB_OUTPUT + echo "image-digest-${{ matrix.image.name }}=$image_digest" >> $GITHUB_OUTPUT + echo "image-artifact-${{ matrix.image.name }}=$image_name@$image_digest" >> $GITHUB_OUTPUT + + # TODO: + # Add in semver tagging for agent versions (if desired) + # type=ref,event=branch,suffix=-${{ matrix.image.name }}${{ matrix.image.name == 'runtime-with-contrast' && needs.prepare.outputs.contrast_version || null }} + # type=ref,event=pr,suffix=-${{ matrix.image.name }}${{ matrix.image.name == 'runtime-with-contrast' && needs.prepare.outputs.contrast_version || null }} + + + + # - name: Login to Docker Hub + # uses: docker/login-action@v3 + # with: + # username: ${{ secrets.DOCKERHUB_USERNAME }} + # password: ${{ secrets.DOCKERHUB_TOKEN }} + + + + test: needs: + - build + uses: ./.github/workflows/e2e-tests.yml + with: + staging_image_runtime: ${{ needs.build.outputs.image-name-runtime }} + staging_image_runtime_with_contrast: ${{ needs.build.outputs.image-name-runtime-with-contrast }} + staging_image_tests: ${{ needs.build.outputs.image-name-tests }} + + # test-contrast: + # needs: + # - build + # uses: ./.github/workflows/e2e-tests.yml + # with: + # staging_image_runtime: ${{ needs.build.outputs.image-name-runtime-with-contrast }} + # staging_image_tests: ${{ needs.build.outputs.image-name-tests }} + # secrets: + # contrast_api_token: ${{ secrets.CONTRAST_API_TOKEN }} + + + # + # Release Internal + # + # + # Release Internal + # + release-internal: + needs: + - prepare + - build - test + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + attestations: write + id-token: write + strategy: + fail-fast: false + matrix: + image: + - name: runtime + - name: runtime-with-contrast + - name: tests + steps: - steps: - - name: Docker Metadata action - id: metadata + - name: Extract metadata + id: release-meta uses: docker/metadata-action@v5 with: - images: contrastsecuritydemo/netflicks + images: | + ghcr.io/${{ github.repository }} flavor: | - latest=true - suffix=agent + latest=false + annotations: | + org.opencontainers.image.description=A deliberately vulnerable .NET Core Application for Contrast Security demos + org.opencontainers.image.vendor=Contrast Security tags: | - type=semver,pattern={{version}}, priority=100 - type=semver,pattern={{major}}.{{minor}}, priority=200 - - - name: Version number - run: | - echo Getting the old build number - echo $(echo ${{ steps.metadata.outputs.tags }}) - - - + type=semver,pattern={{version}},prefix=contrast,value=${{ needs.prepare.outputs.contrast_version }},enable=${{ matrix.image.name == 'runtime-with-contrast' }} + type=semver,pattern={{major}}.{{minor}},prefix=contrast,value=${{ needs.prepare.outputs.contrast_version }},enable=${{ matrix.image.name == 'runtime-with-contrast' }} + type=semver,pattern={{major}},prefix=contrast,value=${{ needs.prepare.outputs.contrast_version }},enable=${{ matrix.image.name == 'runtime-with-contrast' }} + type=raw,value=${{ matrix.image.name == 'runtime-with-contrast' && 'latest-contrast' || 'latest' }},enable=${{ matrix.image.name != 'tests' }} + type=raw,value=tests,enable=${{ matrix.image.name == 'tests' }} - merge: - name: Merge if PR is merged and tests pass - if: github.event.pull_request.merged - runs-on: ubuntu-latest - needs: - - test + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - steps: - - run: | - echo The PR was merged + - name: Release image (internal) + uses: akhilerm/tag-push-action@v2.2.0 + with: + src: ghcr.io/contrast-security-oss/demo-netflicks:pr-8-${{ matrix.image.name }} + dst: | + ${{ steps.release-meta.outputs.tags }} diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 00000000..58701a88 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,77 @@ +name: End-to-End Tests + +on: + workflow_call: + inputs: + staging_image_runtime: + required: true + type: string + staging_image_runtime_with_contrast: + required: true + type: string + staging_image_tests: + required: true + type: string + workflow_dispatch: + +jobs: + test: + name: Test Docker Images + runs-on: ubuntu-latest + env: + STAGING_IMAGE_RUNTIME: ${{ inputs.staging_image_runtime }} + STAGING_IMAGE_RUNTIME_WITH_CONTRAST: ${{ inputs.staging_image_runtime_with_contrast }} + STAGING_IMAGE_TESTS: ${{ inputs.staging_image_tests }} + steps: + - name: Checkout branch + uses: actions/checkout@v4 + + - name: Pull the staging docker images + run: | + echo "STAGING_IMAGE_RUNTIME=$STAGING_IMAGE_RUNTIME" + echo "STAGING_IMAGE_RUNTIME_WITH_CONTRAST=$STAGING_IMAGE_RUNTIME_WITH_CONTRAST" + echo "STAGING_IMAGE_TESTS=$STAGING_IMAGE_TESTS" + + echo "STAGING_IMAGE_RUNTIME=$STAGING_IMAGE_RUNTIME" > .env + echo "STAGING_IMAGE_RUNTIME_WITH_CONTRAST=$STAGING_IMAGE_RUNTIME_WITH_CONTRAST" >> .env + echo "STAGING_IMAGE_TESTS=$STAGING_IMAGE_TESTS" >> .env + + docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile tests pull --include-deps + + - name: Run e2e tests with Playwright + run: | + echo "Running tests for the Docker images..." + docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile tests up --no-build --abort-on-container-exit --exit-code-from tests + + - name: check files + run: | + echo "ls -la /tests" + ls -la /tests + echo "---" + echo "ls -la /tests/playwright-report" + ls -la /tests/playwright-report + echo "---" + echo "ls -la /tests/test-results" + ls -la /tests/test-results + echo "---" + + + - name: Save Playwright report + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: /tests/playwright-report/ + retention-days: 7 + + - name: Save Playwright test results + uses: actions/upload-artifact@v4 + with: + name: playwright-test-results + path: /tests/test-results/ + retention-days: 7 + + - name: Generate Playwright test results summary + uses: daun/playwright-report-summary@v3.9.0 + with: + report-file: /tests/test-results/results.json + job-summary: true diff --git a/.github/workflows/release-testing.yml b/.github/workflows/release-testing.yml new file mode 100644 index 00000000..659be48d --- /dev/null +++ b/.github/workflows/release-testing.yml @@ -0,0 +1,105 @@ +name: Release Docker Images + +on: + # push: + # branches: + # - main + # tags: + # - v* + # pull_request: + # branches: + # - main + + workflow_dispatch: + inputs: + contrast_agent_version: + description: 'Contrast .NET Core agent version to build with' + required: false + default: 'latest' + +jobs: + prepare: + name: Prepare for multi-stage builds + runs-on: ubuntu-latest + outputs: + contrast_version: ${{ steps.versions.outputs.contrast_version }} + steps: + - name: Check latest Contrast agent version + id: versions + run: | + docker pull contrast/agent-dotnet-core:${{ github.event.inputs.contrast_agent_version || 'latest' }} + CONTRAST_VERSION=$(docker image inspect contrast/agent-dotnet-core:latest --format '{{ index .Config.Labels "org.opencontainers.image.version" }}') + echo "Contrast agent version: ${CONTRAST_VERSION}" + echo "contrast_version=${CONTRAST_VERSION}" >> $GITHUB_OUTPUT + + #TODO: Then share the base image with other jobs via cache + + build: + name: Docker Build ${{ matrix.image.name }} + needs: + - prepare + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + attestations: write + id-token: write + strategy: + fail-fast: false + matrix: + image: + - name: runtime + - name: runtime-with-contrast + - name: tests + outputs: + image-name-runtime: ${{ steps.inspect.outputs['image-name-runtime'] }} + image-digest-runtime: ${{ steps.inspect.outputs['image-digest-runtime'] }} + image-artifact-runtime: ${{ steps.inspect.outputs['image-artifact-runtime'] }} + image-name-runtime-with-contrast: ${{ steps.inspect.outputs['image-name-runtime-with-contrast'] }} + image-digest-runtime-with-contrast: ${{ steps.inspect.outputs['image-digest-runtime-with-contrast'] }} + image-artifact-runtime-with-contrast: ${{ steps.inspect.outputs['image-artifact-runtime-with-contrast'] }} + image-name-tests: ${{ steps.inspect.outputs['image-name-tests'] }} + image-digest-tests: ${{ steps.inspect.outputs['image-digest-tests'] }} + image-artifact-tests: ${{ steps.inspect.outputs['image-artifact-tests'] }} + steps: + + - name: Extract metadata + id: docker-meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ github.repository }} + flavor: | + latest=false + annotations: | + org.opencontainers.image.description=A deliberately vulnerable .NET Core Application for Contrast Security demos + org.opencontainers.image.vendor=Contrast Security + tags: | + type=ref,event=branch,suffix=-${{ matrix.image.name }} + type=ref,event=branch,suffix=-${{ matrix.image.name }}${{ matrix.image.name == 'runtime-with-contrast' && needs.prepare.outputs.contrast_version || null }} + type=ref,event=pr,suffix=-${{ matrix.image.name }} + type=ref,event=pr,suffix=-${{ matrix.image.name }}${{ matrix.image.name == 'runtime-with-contrast' && needs.prepare.outputs.contrast_version || null }} + + + - name: Inspect the created images + id: inspect + run: | + matrix_image_name='${{ matrix.image.name }}' + echo "Matrix Image Name: $matrix_image_name" + + # Use the json output instead of the tags output + image_name='${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }}' + + echo "$image_name" + image_digest='abcdefghijk' # Placeholder for actual digest + + echo "Image Name: $image_name" + echo "Image Digest: $image_digest" + echo "Image Artifact: $image_name@$image_digest" + + echo "image-name-${{ matrix.image.name }}=$image_name" >> $GITHUB_OUTPUT + echo "image-digest-${{ matrix.image.name }}=$image_digest" >> $GITHUB_OUTPUT + echo "image-artifact-${{ matrix.image.name }}=$image_name@$image_digest" >> $GITHUB_OUTPUT + + + diff --git a/Dockerfile b/Dockerfile index 7372c927..570a94a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,24 @@ -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS publish +# MULTI STAGE BUILD +ARG CONTRAST_AGENT_VERSION=latest +# Build stage for building the Netflicks application and it's dependencies +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build ARG TARGETARCH + WORKDIR /src + +# Copy project files and restore dependencies (leveraging Docker layer caching) +COPY *.sln . +COPY DotNetFlicks.Accessors/*.csproj ./DotNetFlicks.Accessors/ +COPY DotNetFlicks.Common/*.csproj ./DotNetFlicks.Common/ +COPY DotNetFlicks.Engines/*.csproj ./DotNetFlicks.Engines/ +COPY DotNetFlicks.Managers/*.csproj ./DotNetFlicks.Managers/ +COPY DotNetFlicks.ViewModels/*.csproj ./DotNetFlicks.ViewModels/ +COPY DotNetFlicks.Web/*.csproj ./DotNetFlicks.Web/ + +# RUN dotnet restore "DotNetFlicks.Web/Web.csproj" --arch $TARGETARCH +RUN dotnet restore "DotNetFlicks.Web/Web.csproj" /p:Platform=$TARGETARCH + +# Copy the rest of the source code and build COPY ./DotNetFlicks.Accessors ./DotNetFlicks.Accessors COPY ./DotNetFlicks.Common ./DotNetFlicks.Common COPY ./DotNetFlicks.Engines ./DotNetFlicks.Engines @@ -8,13 +26,45 @@ COPY ./DotNetFlicks.Managers ./DotNetFlicks.Managers COPY ./DotNetFlicks.ViewModels ./DotNetFlicks.ViewModels COPY ./DotNetFlicks.Web ./DotNetFlicks.Web COPY ./DotNetFlicks.sln ./DotNetFlicks.sln -RUN dotnet publish "DotNetFlicks.Web/Web.csproj" /p:Platform=$TARGETARCH -c Release -o /app +RUN dotnet publish "DotNetFlicks.Web/Web.csproj" \ + # --arch $TARGETARCH \ + /p:Platform=$TARGETARCH \ + --configuration Release \ + --no-restore \ + --output /app \ + --self-contained false -FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS final -RUN uname -a -RUN apt-get update && apt-get --assume-yes install libnss3-tools -WORKDIR /app -EXPOSE 80 -COPY --from=publish /app . +# Runtime stage for running the application without the Contrast agent +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS runtime + +RUN apt-get update && \ + apt-get install --assume-yes --no-install-recommends \ + libnss3-tools \ + curl \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY --from=build /app . ENTRYPOINT ["dotnet", "DotNetFlicks.Web.dll"] + + +# Contrast agent image for .NET Core applications +# Need the extra FROM line here for the CONTRAST_AGENT_VERSION argument to work +FROM contrast/agent-dotnet-core:${CONTRAST_AGENT_VERSION} AS contrast-agent + + +# Final stage for running the Netflicks application with the Contrast agent +FROM runtime AS runtime-with-contrast +ARG TARGETARCH + +# Copy the agent from the contrast agent image +COPY --from=contrast-agent /contrast /opt/contrast + +# Handle architecture naming differences between TARGETARCH and Contrast using +# shell variable substitution: ${TARGETARCH/arm64/x64} (requires bash) +SHELL ["/bin/bash", "-c"] +# Needs to be linux-arm64 or linux-x64 or win-x64 or win-x86 +ENV CORECLR_PROFILER_PATH_64=/opt/contrast/runtimes/linux-${TARGETARCH/arm64/x64}/native/ContrastProfiler.so \ + CORECLR_PROFILER={8B2CE134-0948-48CA-A4B2-80DDAD9F5791} \ + CORECLR_ENABLE_PROFILING=1 \ + CONTRAST_CORECLR_LOGS_DIRECTORY=/opt/contrast diff --git a/Dockerfile.contrast b/Dockerfile.contrast deleted file mode 100644 index 70a1cf4b..00000000 --- a/Dockerfile.contrast +++ /dev/null @@ -1,10 +0,0 @@ -FROM contrastsecuritydemo/netflicks:latest-no-agent -ARG TARGETARCH - -# Copy the agent from the contrast agent image -COPY --from=contrast/agent-dotnet-core:latest /contrast /opt/contrast -# Workaround for architecture naming differences between .NET Core and Contrast -RUN ln -s /opt/contrast/runtimes/linux-x64 /opt/contrast/runtimes/linux-amd64 - -# Needs to be linux-arm64 or linux-x64 or win-x64 or win-x86 -ENV CORECLR_PROFILER_PATH_64 /opt/contrast/runtimes/linux-$TARGETARCH/native/ContrastProfiler.so diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 00000000..42c22dd3 --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,41 @@ + +variable "CONTRAST_AGENT_VERSION" { + description = "Target version of the Contrast Security agent to use" + default = "latest" +} + +group "default" { + targets = ["runtime", "runtime-with-contrast", "tests"] +} + +target "docker-metadata-action" {} + +target "build" { + inherits = ["docker-metadata-action"] + target = "build" + context = "." + dockerfile = "Dockerfile" +} + +target "runtime" { + inherits = ["docker-metadata-action"] + context = "." + dockerfile = "Dockerfile" + target = "runtime" +} + +target "runtime-with-contrast" { + inherits = ["docker-metadata-action"] + context = "." + dockerfile = "Dockerfile" + target = "runtime-with-contrast" + args = { + CONTRAST_AGENT_VERSION = CONTRAST_AGENT_VERSION + } +} + +target "tests" { + inherits = ["docker-metadata-action"] + context = "./tests" + dockerfile = "Dockerfile" +} diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml new file mode 100644 index 00000000..7d36f571 --- /dev/null +++ b/docker-compose.ci.yml @@ -0,0 +1,18 @@ + +services: + + # Development environment for Contrast Assess demos + web-dev: + image: "${STAGING_IMAGE_RUNTIME}" + + web-prod: + image: "${STAGING_IMAGE_RUNTIME}" + + # Testing service + tests: + image: "${STAGING_IMAGE_TESTS}" + volumes: + - /tests/playwright-report:/tests/playwright-report + - /tests/test-results:/tests/test-results + + diff --git a/docker-compose.yml b/docker-compose.yml index d5d10a43..f0119f85 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,30 +1,77 @@ -version: '3' services: database: - image: mcr.microsoft.com/azure-sql-edge + image: mcr.microsoft.com/mssql/server:2022-latest environment: - ACCEPT_EULA=Y - SA_PASSWORD=reallyStrongPwd123 ports: - '1433:1433' - - web: - image: contrastsecuritydemo/netflicks:latest + volumes: + - mssql-data:/var/opt/mssql + healthcheck: + test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -C -U sa -P reallyStrongPwd123 -Q 'SELECT 1' -b"] + # test: ["CMD", "/opt/mssql-tools18/bin/sqlcmd", "-S", "http://localhost:1433", "-U", "sa", "-P", "reallyStrongPwd123", "-Q", "SELECT 1"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 10s + + # Development environment for Contrast Assess demos + web-dev: + image: contrastsecuritydemo/netflicks:latest-contrast + build: + context: . + dockerfile: Dockerfile + target: runtime-with-contrast + depends_on: + database: + condition: service_healthy + ports: + - '8888:80' + volumes: + - ./contrast_security.yaml:/etc/contrast/dotnet-core/contrast_security.yaml:ro + environment: + - ConnectionStrings__DotNetFlicksConnection=Server=tcp:database,1433;Initial Catalog=DotNetFlicksDb;Persist Security Info=False;User ID=sa;Password=reallyStrongPwd123;MultipleActiveResultSets=False;TrustServerCertificate=yes; + - CONTRAST__SERVER__NAME=docker-netflicks-dev + - CONTRAST__SERVER__ENVIRONMENT=development + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost || exit 1"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 10s + + web-prod: + image: contrastsecuritydemo/netflicks:latest-contrast build: context: . - dockerfile: Dockerfile.contrast + dockerfile: Dockerfile + target: runtime-with-contrast depends_on: - - database + database: + condition: service_healthy ports: - - '8081:80' + - '8889:80' volumes: - - ./contrast_security.yaml:/etc/contrast/dotnet-core/contrast_security.yaml + - ./contrast_security.yaml:/etc/contrast/dotnet-core/contrast_security.yaml:ro + environment: + - ConnectionStrings__DotNetFlicksConnection=Server=tcp:database,1433;Initial Catalog=DotNetFlicksDb;Persist Security Info=False;User ID=sa;Password=reallyStrongPwd123;MultipleActiveResultSets=False;TrustServerCertificate=yes; + - CONTRAST__SERVER__NAME=docker-netflicks-prod + - CONTRAST__SERVER__ENVIRONMENT=production + + # Testing service + tests: + build: + context: ./tests + dockerfile: Dockerfile + depends_on: + web-dev: + condition: service_healthy environment: - - ConnectionStrings__DotNetFlicksConnection=Server=tcp:database,1433;Initial Catalog=DotNetFlicksDb;Persist Security Info=False;User ID=sa;Password=reallyStrongPwd123;MultipleActiveResultSets=False; - - CORECLR_PROFILER={8B2CE134-0948-48CA-A4B2-80DDAD9F5791} - - CORECLR_ENABLE_PROFILING=1 - - CONTRAST_CORECLR_LOGS_DIRECTORY=/opt/contrast/ + - BASEURL=http://web-dev + profiles: + - tests volumes: - mysql-data: + mssql-data: diff --git a/tests/Dockerfile b/tests/Dockerfile index efcd0894..608dc415 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -1,26 +1,20 @@ -FROM mcr.microsoft.com/playwright:v1.32.1-jammy -# copy project (including tests) -# COPY . /tests +FROM mcr.microsoft.com/playwright:v1.50.0 WORKDIR /tests -# COPY ./tests/package.json ./tests/package-lock.json /tests/ -COPY package.json /tests/ +COPY package.json package-lock.json /tests/ # Install dependencies from the package-lock.json file above -# RUN npm ci -RUN npm install -# Install browsers - TODO: Do we need this line? -# RUN npx playwright install +RUN npm ci # Add the base playwright config. This will need to be overwritten with a volume if changes are needed. COPY playwright.config.ts /tests/playwright.config.ts COPY global-setup.ts /tests/global-setup.ts # Add example test for testing the container. Will be overwritten with the actual tests via volumes. -COPY tests /tests +COPY . /tests # Run playwright test ENV BASEURL="http://demo-netflicks-web-1" +ENV CI=true CMD [ "npx", "playwright", "test", "assess" ] -# EXPOSE 9323 -# CMD ["/bin/bash"] + diff --git a/tests/playwright.config.ts b/tests/playwright.config.ts index ce151ded..78c530d6 100644 --- a/tests/playwright.config.ts +++ b/tests/playwright.config.ts @@ -33,6 +33,8 @@ const config: PlaywrightTestConfig = { /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [ ['list', { printSteps: true}], + ['html', { open: 'never' }], + ['json', { outputFile: '/tests/test-results/results.json' }], ], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: {