Security scan #1647
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| name: Security scan | |
| # Periodically scan production images for security vulnerabilities | |
| on: | |
| schedule: | |
| # Once a day at midnight | |
| - cron: '0 0 * * *' | |
| # Once an hour | |
| # - cron: '0 * * * *' | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| # Name of image | |
| IMAGE_NAME: foo-app | |
| # Name of org in GHCR Docker repository (must be lowercase) | |
| IMAGE_OWNER: ${{ github.repository_owner }} | |
| # IMAGE_OWNER: cogini | |
| # AWS ECR Docker repo "org" name (may be blank, otherwise must have trailing slash) | |
| ECR_IMAGE_OWNER: cogini/ | |
| # ECR_IMAGE_OWNER: '' | |
| # Tag for release images, used to find the latest deployed image. | |
| IMAGE_TAG: latest | |
| IMAGE_VER: ${{ github.sha }} | |
| # Registry to pull internal images from | |
| REGISTRY: ghcr.io/ | |
| # Registry for public images, default (blank) is docker.io | |
| # PUBLIC_REGISTRY: '' | |
| # Assume that base image has been synced to local registry | |
| PUBLIC_REGISTRY: 'ghcr.io/' | |
| AWS_OTEL_COLLECTOR_REPO_ORG: ${{ github.repository_owner }} | |
| POSTGRES_REPO_ORG: ${{ github.repository_owner }} | |
| RABBITMQ_REPO_ORG: ${{ github.repository_owner }} | |
| # Git "main" branch. This might be "master" for old repos | |
| MAIN_BRANCH: main | |
| # GitHub Environment secrets and variables | |
| # Docker Hub credentials to pull base images without rate limits | |
| # secrets.DOCKERHUB_USERNAME | |
| # secrets.DOCKERHUB_TOKEN | |
| # AWS Account | |
| # secrets.AWS_ACCOUNT_ID | |
| # AWS default region | |
| # vars.AWS_REGION: us-east-1 | |
| # AWS role allowing GitHub Actions to access resources and deploy | |
| # secrets.AWS_ROLE_TO_ASSUME: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/foo-${{ environment }}-github-action | |
| # S3 bucket where assets are deployed, e.g., for use with CloudFront CDN | |
| # vars.S3_BUCKET_ASSETS: cogini-foo-app-dev-app-assets | |
| # S3 bucket with data for testing | |
| # vars.S3_BUCKET_CI: cogini-prod-foo-ci | |
| # SSH key to access private repos during build | |
| # secrets.SSH_PRIVATE_KEY | |
| # GitHub access token to access other repositories during build | |
| # secrets.DEVOPS_ACCESS_TOKEN | |
| # AWS ECS deployment role names to put in task definition | |
| # secrets.TASK_ROLE_ARN: "arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/foo-app" | |
| # secrets.EXECUTION_ROLE_ARN: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/foo-ecs-task-execution-role | |
| # AWS Systems Manager Parameter Store prefix for config keys | |
| # vars.AWS_PS_PREFIX: cogini/foo/prod | |
| # AWS CloudFront distribution ID to invalidate cache after deploy | |
| # secrets.CLOUDFRONT_CDN_DISTRIBUTION_ID | |
| # AppSignal API key for error reporting | |
| # secrets.APPSIGNAL_PUSH_API_KEY | |
| # DataDog API key for reporting test results | |
| # secrets.ACTIONS_DD_API_KEY | |
| # secrets.DD_API_KEY | |
| # Oban Pro license | |
| # secrets.OBAN_KEY_FINGERPRINT | |
| # secrets.OBAN_LICENSE_KEY | |
| # Sentry.io | |
| # secrets.SENTRY_AUTH_TOKEN | |
| # secrets.SENTRY_ORG | |
| # secrets.SENTRY_PROJECT | |
| # secrets.GITLEAKS_LICENSE | |
| # secrets.SNYK_TOKEN | |
| # Target port for prod image tests | |
| APP_PORT: 4000 | |
| # Elixir module, used in health checks | |
| ELIXIR_MODULE: PhoenixContainerExample | |
| # Elixir module Erlang app name | |
| ELIXIR_APP: phoenix_container_example | |
| # AWS SSM Parameter Store name prefix | |
| # AWS_PS_PREFIX: cogini/foo/dev | |
| # APPSIGNAL_APP_NAME: bounce | |
| # Name of environment for resources created by Terraform | |
| # TERRAFORM_ENV: dev | |
| # secrets.TASK_ROLE_ARN: "arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/foo-app" | |
| # secrets.EXECUTION_ROLE_ARN: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/foo-ecs-task-execution-role | |
| # Docker | |
| DOCKER_BUILDKIT: '1' | |
| COMPOSE_DOCKER_CLI_BUILD: '1' | |
| COMPOSE_FILE: docker-compose.gha.yml | |
| DOCKER_FILE: deploy/debian.Dockerfile | |
| jobs: | |
| config: | |
| name: Configure build | |
| runs-on: ubuntu-latest | |
| outputs: | |
| # Combinations of Elixir/Erlang/OS to run internal tests on | |
| # Generally this will match prod | |
| test-matrix: ${{ steps.common-matrix.outputs.result }} | |
| # Combinations to tests on in parallel, i.e., mix test with partitions | |
| test-matrix-parallel: ${{ steps.test-matrix-parallel.outputs.result }} | |
| # Combinations for prod images | |
| # Used for the final deploy image as well as external tests | |
| prod-matrix: ${{ steps.prod-matrix.outputs.result }} | |
| # Combinations to deploy (to prod or another environment) | |
| # Usually only a single variant | |
| deploy-matrix: ${{ steps.deploy-matrix.outputs.result }} | |
| # Combinations used to build assets | |
| # Usually only a single variant | |
| assets-matrix: ${{ steps.assets-matrix.outputs.result }} | |
| # Combinations to sync images to GHCR for | |
| sync-matrix: ${{ steps.common-matrix.outputs.result }} | |
| # Select environment to deploy to based on git branch/tag | |
| environment: ${{ (github.ref_name == 'main' && 'staging') || (github.ref_name == 'prod' && 'production') }} | |
| # Whether to deploy. Only standard tags/branches deploy, not other dev branches. | |
| # deploy: ${{ contains(fromJson('["main", "staging", "qa", "prod"]'), github.ref_name) }} | |
| deploy: '1' | |
| # Sync base and 3rd-party images to GHCR | |
| # sync-images: '${{ steps.get-sync-images.outputs.result }}' | |
| sync-images: '1' | |
| # Sync 3rd-party images to ECR | |
| # sync-images-ecr: '${{ steps.get-sync-images.outputs.result }}' | |
| sync-images-ecr: '0' | |
| # Tag for GHCR images. The repo is shared between all environments, so use tags to distinguish. | |
| image-tag: ${{ (github.ref_name == 'main' && 'test') || (github.ref_name == 'qa' && 'qa') || (github.ref_name == 'prod' && 'latest') || github.ref_name }} | |
| # Tag for ECR images. Repos are separate between environments, so each gets "latest" tag | |
| image-tag-ecr: ${{ contains(fromJson('["main", "staging", "prod"]'), github.ref_name) && 'latest' || github.ref_name }} | |
| image-tag-deploy: ${{ steps.get-image-tag-deploy.outputs.result }} | |
| # Enable AWS interactions in the build, including deploys. | |
| aws-enabled: '1' | |
| # Run unit tests | |
| test: '1' | |
| # Run dialyzer | |
| test-dialyzer: '0' | |
| # Security scan prod image | |
| scan-prod-image: '0' | |
| # Security scan code image | |
| scan-code: '0' | |
| # GitHub Advanced Security, free for open source, otherwise a paid feature | |
| # https://docs.github.com/en/get-started/learning-about-github/about-github-advanced-security | |
| # https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning | |
| # https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/uploading-a-sarif-file-to-github | |
| github-advanced-security: '0' | |
| # Upload assets to S3 | |
| upload-assets: '0' | |
| # Create GitHub release for this build | |
| create-release: '0' | |
| # Deploy using AWS CodeDeploy | |
| deploy-codedeploy: '0' | |
| # Send results to Datadog | |
| datadog: '0' | |
| # Notify AppSignal of new app revision | |
| notify-appsignal: '1' | |
| # Notify Sentry of new app release | |
| notify-sentry: '0' | |
| steps: | |
| - name: Configure common-matrix | |
| id: common-matrix | |
| uses: actions/github-script@v8 | |
| # Specify versions of Erlang, Elixir, and base OS | |
| # in a combination supported by https://hub.docker.com/r/hexpm/elixir/tags | |
| # { | |
| # os: "alma", | |
| # elixir: "1.18.4", | |
| # otp: "28.1", | |
| # build_os_ver: "8", | |
| # prod_os_ver: "8" | |
| # }, | |
| # { | |
| # os: "centos7", | |
| # elixir: "1.18.4", | |
| # otp: "28.1", | |
| # build_os_ver: "7.9.2009", | |
| # prod_os_ver: "7.9.2009" | |
| # }, | |
| with: | |
| script: | | |
| return { | |
| include: [ | |
| { | |
| os: "debian", | |
| elixir: "1.19.5", | |
| otp: "28.5", | |
| build_os_ver: "trixie-20260421-slim", | |
| prod_os_ver: "trixie-20260421-slim" | |
| }, | |
| ] | |
| } | |
| - name: Configure test-matrix-parallel | |
| id: test-matrix-parallel | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| let platforms = [ | |
| { | |
| os: "debian", | |
| elixir: "1.19.5", | |
| otp: "28.5", | |
| build_os_ver: "trixie-20260421-slim", | |
| prod_os_ver: "trixie-20260421-slim" | |
| }, | |
| ] | |
| let ci_nodes = [1, 2] | |
| let ci_node_total = ci_nodes.length | |
| let matrix = [] | |
| for (let i = 0; i < platforms.length; i++) { | |
| for (let j = 0; j < ci_nodes.length; j++) { | |
| matrix.push(Object.assign({}, platforms[i], {ci_node_total: ci_node_total, ci_node_index: ci_nodes[j]}) ) | |
| } | |
| } | |
| return { include: matrix } | |
| - name: Configure prod-matrix | |
| id: prod-matrix | |
| uses: actions/github-script@v8 | |
| # { | |
| # os: "alma", | |
| # elixir: "1.18.4", | |
| # otp: "28.1", | |
| # build_os_ver: "8", | |
| # prod_os_ver: "8" | |
| # }, | |
| # { | |
| # os: "centos7", | |
| # elixir: "1.18.4", | |
| # otp: "28.1", | |
| # build_os_ver: "7", | |
| # prod_os_ver: "7" | |
| # }, | |
| with: | |
| script: | | |
| return { | |
| include: [ | |
| { | |
| os: "debian", | |
| elixir: "1.19.5", | |
| otp: "28.5", | |
| build_os_ver: "trixie-20260421-slim", | |
| prod_os_ver: "trixie-20260421-slim" | |
| }, | |
| ] | |
| } | |
| - name: Configure deploy-matrix | |
| id: deploy-matrix | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| return { | |
| include: [ | |
| { | |
| os: "debian", | |
| elixir: "1.19.5", | |
| otp: "28.5", | |
| build_os_ver: "trixie-20260421-slim", | |
| prod_os_ver: "trixie-20260421-slim" | |
| }, | |
| ] | |
| } | |
| - name: Configure assets-matrix | |
| uses: actions/github-script@v8 | |
| id: assets-matrix | |
| with: | |
| script: | | |
| return { | |
| include: [ | |
| { | |
| os: "debian", | |
| elixir: "1.19.5", | |
| otp: "28.5", | |
| build_os_ver: "trixie-20260421-slim", | |
| prod_os_ver: "trixie-20260421-slim" | |
| }, | |
| ] | |
| } | |
| # Select GitHub Actions environment based on branch or tag name | |
| # This also implicitly selects the AWS deploy environment. | |
| # * `main` branch or `test` tag deploys to test | |
| # * `staging` or `staging-*` tag/branch deploys to staging | |
| # * `qa` or `qa-*` tag/branch deploys to qa | |
| # * `prod` tag deploys to prod and dr | |
| - name: Configure environment | |
| id: get-environment | |
| uses: actions/github-script@v8 | |
| env: | |
| GITHUB_REF_NAME: ${{ github.ref_name }} | |
| with: | |
| result-encoding: string | |
| script: | | |
| ref_name = process.env.GITHUB_REF_NAME | |
| var environments = { | |
| 'test': 'test', | |
| 'main': 'test', | |
| 'staging': 'staging', | |
| 'qa': 'qa', | |
| 'prod': 'prod', | |
| 'dr': 'prod', | |
| 'media': 'prod', | |
| } | |
| if (ref_name in environments) { | |
| result = environments[ref_name] | |
| } else { | |
| if (ref_name.startsWith('staging-')) { | |
| result = 'staging' | |
| } else if (ref_name.startsWith('qa-')) { | |
| result = 'qa' | |
| } else { | |
| result = 'test' | |
| } | |
| } | |
| return result | |
| # Get image tag for deploy based on environment | |
| # In test, staging, and qa, deploy is based on GitHub SHA | |
| # In prod, deploy is based on staging ECR repo | |
| # In dr, deploy is based on prod ECR repo in DR region | |
| - name: Configure image-tag-deploy | |
| id: get-image-tag-deploy | |
| uses: actions/github-script@v8 | |
| env: | |
| GITHUB_REF_NAME: ${{ github.ref_name }} | |
| with: | |
| result-encoding: string | |
| script: | | |
| ref_name = process.env.GITHUB_REF_NAME | |
| if (ref_name == 'prod' || ref_name == 'dr') { | |
| result = 'prod' | |
| } else { | |
| result = context.sha | |
| } | |
| return result | |
| # Determine whether to deploy to AWS based on branch or tag name | |
| - name: Configure deploy | |
| id: get-deploy | |
| uses: actions/github-script@v8 | |
| env: | |
| GITHUB_REF_NAME: ${{ github.ref_name }} | |
| with: | |
| result-encoding: string | |
| script: | | |
| ref_name = process.env.GITHUB_REF_NAME | |
| if (ref_name == 'main' || ref_name == 'staging' || ref_name.startsWith('staging-')) { | |
| result = '1' | |
| } else if (ref_name == 'qa' || ref_name.startsWith('qa-')) { | |
| result = '1' | |
| } else if (ref_name == 'testing' || ref_name.startsWith('devops-')) { | |
| result = '1' | |
| } else { | |
| result = '0' | |
| } | |
| return result | |
| - name: Configure sync-images | |
| id: get-sync-images | |
| uses: actions/github-script@v8 | |
| env: | |
| GITHUB_REF_NAME: ${{ github.ref_name }} | |
| with: | |
| result-encoding: string | |
| script: | | |
| ref_name = process.env.GITHUB_REF_NAME | |
| if (ref_name == 'main' || ref_name == 'staging' || ref_name.startsWith('staging-')) { | |
| result = '1' | |
| } else if (ref_name == 'testing' || ref_name.startsWith('devops-')) { | |
| result = '1' | |
| } else if (ref_name == 'qa' || ref_name.startsWith('qa-')) { | |
| result = '1' | |
| } else { | |
| result = '0' | |
| } | |
| return result | |
| scan: | |
| name: Security scan prod image | |
| if: needs.config.outputs.scan-prod-image == '1' | |
| needs: [config] | |
| permissions: | |
| id-token: write | |
| contents: read | |
| packages: read | |
| checks: write | |
| pull-requests: write | |
| issues: read | |
| # Upload SARIF report files | |
| security-events: write | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.config.outputs.prod-matrix) }} | |
| env: | |
| DOCKER_FILE: deploy/${{ matrix.os }}.Dockerfile | |
| VAR: ${{ matrix.elixir }}-erlang-${{ matrix.otp }}-${{ matrix.os }}-${{ matrix.build_os_ver }} | |
| GITHUB_ADVANCED_SECURITY: ${{ needs.config.outputs.github-advanced-security }} | |
| steps: | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Pull image | |
| run: docker pull "ghcr.io/${IMAGE_OWNER}/${IMAGE_NAME}:${VAR}${IMAGE_VER}" | |
| - name: Scan image with Trivy | |
| uses: aquasecurity/trivy-action@0.35.0 | |
| # https://github.com/aquasecurity/trivy-action | |
| # https://github.com/marketplace/actions/aqua-security-trivy#inputs | |
| with: | |
| image-ref: ghcr.io/${{ env.IMAGE_OWNER }}/${{ env.IMAGE_NAME }}:${{ env.VAR }}${{ env.IMAGE_VER }} | |
| # exit-code: '1' # fail build | |
| # ignore-unfixed: true | |
| # vuln-type: 'os,library' | |
| # severity: 'CRITICAL,HIGH' | |
| # cache-dir: /var/cache | |
| format: ${{ env.GITHUB_ADVANCED_SECURITY == '1' && 'sarif' || 'table' }} | |
| # output: ${{ env.GITHUB_ADVANCED_SECURITY == '1' && 'trivy-results.sarif' }} | |
| - name: Display scan results | |
| if: ${{ always() && env.GITHUB_ADVANCED_SECURITY == '1' }} | |
| run: cat trivy-results.sarif | jq . | |
| - name: Upload Trivy scan results to GitHub Security tab | |
| if: ${{ always() && env.GITHUB_ADVANCED_SECURITY == '1' }} | |
| uses: github/codeql-action/upload-sarif@v3 | |
| # Requires GitHub Advanced Security | |
| # https://docs.github.com/en/get-started/learning-about-github/about-github-advanced-security | |
| # https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning | |
| # https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/uploading-a-sarif-file-to-github | |
| with: | |
| sarif_file: 'trivy-results.sarif' | |
| category: trivy | |
| - name: Scan image with Grype | |
| uses: anchore/scan-action@v7 | |
| # https://github.com/marketplace/actions/anchore-container-scan | |
| id: scan-grype | |
| with: | |
| image: ghcr.io/${{ env.IMAGE_OWNER }}/${{ env.IMAGE_NAME }}:${{ env.VAR }}${{ env.IMAGE_VER }} | |
| # severity-cutoff: critical | |
| fail-build: false | |
| # output-format: 'sarif' | |
| output-format: table | |
| # output-format: ${{ env.GITHUB_ADVANCED_SECURITY == '1' && 'sarif' || 'table' }} | |
| - name: Display scan results | |
| if: ${{ always() && env.GITHUB_ADVANCED_SECURITY == '1' }} | |
| run: cat ${{ steps.scan-grype.outputs.sarif }} | jq . | |
| - name: Upload Grype scan results to GitHub Security tab | |
| if: ${{ always() && env.GITHUB_ADVANCED_SECURITY == '1' }} | |
| uses: github/codeql-action/upload-sarif@v3 | |
| with: | |
| sarif_file: ${{ steps.scan-grype.outputs.sarif }} | |
| category: grype | |
| # - name: Scan image with Snyk | |
| # uses: snyk/actions/docker@master | |
| # continue-on-error: true | |
| # env: | |
| # SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} | |
| # with: | |
| # command: test | |
| # image: ghcr.io/${{ env.IMAGE_OWNER }}/${{ env.IMAGE_NAME }}:${{ env.VAR }}${{ env.IMAGE_VER }} | |
| # args: --file=${{ env.DOCKER_FILE }} --project-name=api |