Repository navigation
ECR vulnerability detection #656
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: ECR vulnerability detection | |
| on: | |
| schedule: | |
| - cron: '0 9 * * *' | |
| workflow_call: | |
| inputs: | |
| trigger_source: | |
| type: string | |
| required: false | |
| default: 'schedule' | |
| outputs: | |
| scan_type: | |
| description: "Type of scan (scheduled or workflow)" | |
| value: ${{ inputs.trigger_source }} | |
| jobs: | |
| vulnerability-detection: | |
| if: | | |
| inputs.trigger_source == 'workflow' || | |
| (github.event_name == 'schedule') | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - image_tag: "8.2-alpine3.21" | |
| - image_tag: "8.3-alpine3.21" | |
| - image_tag: "8.4-alpine3.21" | |
| - image_tag: "8.2-alpine3.22" | |
| - image_tag: "8.3-alpine3.22" | |
| - image_tag: "8.4-alpine3.22" | |
| - image_tag: "8.2-alpine3.23" | |
| - image_tag: "8.3-alpine3.23" | |
| - image_tag: "8.4-alpine3.23" | |
| - image_tag: "8.2-alpine3.24" | |
| - image_tag: "8.3-alpine3.24" | |
| - image_tag: "8.4-alpine3.24" | |
| - image_tag: "8.5-alpine3.24" | |
| steps: | |
| - name: Login to DockerHub | |
| uses: docker/login-action@v1 | |
| with: | |
| username: ${{ secrets.DOCKER_USERNAME }} | |
| password: ${{ secrets.DOCKER_PASSWORD }} | |
| - name: Pull the latest image from the DockerHub | |
| run: docker pull "spryker/php:${{ matrix.image_tag }}" | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| aws-region: ${{ secrets.AWS_REGION }} | |
| - name: Login to AWS ECR | |
| id: login-ecr | |
| uses: aws-actions/amazon-ecr-login@v2 | |
| - name: Ensure ECR repo exists (scanOnPush on) | |
| run: | | |
| aws ecr describe-repositories --repository-names "${{ secrets.AWS_ECR_REPO }}" >/dev/null 2>&1 || \ | |
| aws ecr create-repository --repository-name "${{ secrets.AWS_ECR_REPO }}" | |
| - name: Tag & push to ECR | |
| id: push | |
| env: | |
| ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} | |
| run: | | |
| ECR_URI="${ECR_REGISTRY}/${{ secrets.AWS_ECR_REPO }}:${{ matrix.image_tag }}" | |
| docker tag "spryker/php:${{ matrix.image_tag }}" "$ECR_URI" | |
| docker push "$ECR_URI" | |
| echo "ecr_uri=$ECR_URI" >> $GITHUB_ENV | |
| - name: Wait for enhanced scan results | |
| run: | | |
| set -euo pipefail | |
| REPO="${{ secrets.AWS_ECR_REPO }}" | |
| TAG="${{ matrix.image_tag }}" | |
| REG="${{ secrets.AWS_REGION }}" | |
| for i in {1..20}; do | |
| echo "Attempt $i of 20: Checking scan status..." | |
| out=$(aws ecr describe-image-scan-findings \ | |
| --repository-name "$REPO" \ | |
| --image-id imageTag="$TAG" \ | |
| --region "$REG" 2>/dev/null || true) | |
| status=$(echo "$out" | jq -r '.imageScanStatus.status // ""') | |
| has_obj=$(echo "$out" | jq -r '.imageScanFindings != null') | |
| echo "Status: ${status:-<none>} | has findings obj: $has_obj" | |
| if [[ ("$status" == "COMPLETE" || "$status" == "ACTIVE") && "$has_obj" == "true" ]]; then | |
| echo "Scan is ready to proceed!" | |
| break | |
| fi | |
| if [[ "$i" == "20" ]]; then | |
| echo "Timeout waiting for scan to become ready" | |
| exit 1 | |
| fi | |
| echo "Waiting 30 seconds before next attempt..." | |
| sleep 30 | |
| done | |
| - name: Scan Docker image (ECR) | |
| id: docker-scan | |
| uses: alexjurkiewicz/ecr-scan-image@v3.0.0 | |
| with: | |
| repository: ${{ secrets.AWS_ECR_REPO }} | |
| tag: ${{ matrix.image_tag }} | |
| - name: Fix vulnerability counts | |
| id: fixed-counts | |
| run: | | |
| CRITICAL=${{ steps.docker-scan.outputs.critical }} | |
| HIGH=${{ steps.docker-scan.outputs.high }} | |
| MEDIUM=${{ steps.docker-scan.outputs.medium }} | |
| LOW=${{ steps.docker-scan.outputs.low }} | |
| INFO=${{ steps.docker-scan.outputs.informational }} | |
| SCAN_RESULT=$(aws ecr describe-image-scan-findings \ | |
| --repository-name "${{ secrets.AWS_ECR_REPO }}" \ | |
| --image-id imageTag="${{ matrix.image_tag }}" \ | |
| --output json 2>/dev/null) || SCAN_RESULT="{}" | |
| if ! echo "$SCAN_RESULT" | jq empty 2>/dev/null; then | |
| SCAN_RESULT="{}" | |
| fi | |
| UNDEFINED=$(echo "$SCAN_RESULT" | jq ' | |
| [ | |
| (.imageScanFindings.enhancedFindings // [])[] | | |
| select( | |
| .packageVulnerabilityDetails.vendorSeverity == null or | |
| .packageVulnerabilityDetails.vendorSeverity == "" or | |
| (.packageVulnerabilityDetails.vendorSeverity | ascii_downcase) == "undefined" | |
| ) | |
| ] | length | |
| ') | |
| TOTAL=$(( ${CRITICAL:-0} + ${HIGH:-0} + ${MEDIUM:-0} + ${LOW:-0} + ${INFO:-0} + ${UNDEFINED:-0} )) | |
| echo "total=$TOTAL" >> $GITHUB_OUTPUT | |
| echo "undefined=$UNDEFINED" >> $GITHUB_OUTPUT | |
| - name: Report | |
| if: always() | |
| run: | | |
| echo "Scan type: ${{ inputs.trigger_source }}" | |
| echo "${{ steps.fixed-counts.outputs.total }} total vulnerabilities." | |
| echo "Critical: ${{ steps.docker-scan.outputs.critical }}, High: ${{ steps.docker-scan.outputs.high }}, Medium: ${{ steps.docker-scan.outputs.medium }}, Low: ${{ steps.docker-scan.outputs.low }}, Informational: ${{ steps.docker-scan.outputs.informational }}, Undefined: ${{ steps.fixed-counts.outputs.undefined }}" | |
| echo "Details of findings:" | |
| cat <<'JSON' | |
| ${{ steps.docker-scan.outputs.findingsDetails }} | |
| JSON | |
| - name: Delete image from ECR (optional cleanup) | |
| if: always() | |
| run: | | |
| aws ecr batch-delete-image \ | |
| --repository-name "${{ secrets.AWS_ECR_REPO }}" \ | |
| --image-ids imageTag="${{ matrix.image_tag }}" || true | |
| - name: Set Date and Time | |
| id: set-date | |
| if: ${{ always() && github.event_name == 'schedule' }} | |
| run: echo "current_datetime=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT | |
| - name: Set Color | |
| id: set-color | |
| if: ${{ always() && github.event_name == 'schedule' }} | |
| shell: bash | |
| run: | | |
| if [ "${{ steps.fixed-counts.outputs.total }}" = "0" ]; then | |
| echo "color=#008000" >> $GITHUB_OUTPUT | |
| else | |
| echo "color=#ff0000" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Build link to scan step | |
| id: scan-step-link | |
| if: ${{ always() && github.event_name == 'schedule' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RUN_ID: ${{ github.run_id }} | |
| TAG: ${{ matrix.image_tag }} | |
| STEP_NAME: Scan Docker image (ECR) | |
| run: | | |
| set -euo pipefail | |
| RES="$(curl -sS -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer $GH_TOKEN" \ | |
| "https://api.github.com/repos/${{ github.repository }}/actions/runs/$RUN_ID/jobs?per_page=100")" | |
| JOB="$(jq -c --arg tag "$TAG" ' | |
| .jobs | |
| | (map(select(.name | endswith("(" + $tag + ")"))) | .[0]) | |
| // .[0] | |
| ' <<<"$RES")" | |
| JOB_URL="$(jq -r '.html_url' <<<"$JOB")" | |
| STEP_NUM="$(jq -r --arg n "$STEP_NAME" ' | |
| .steps | map(select(.name == $n)) | .[0].number | |
| ' <<<"$JOB")" | |
| if [ -n "$STEP_NUM" ] && [ "$STEP_NUM" != "null" ]; then | |
| STEP_URL="${JOB_URL}#step:${STEP_NUM}:1" | |
| else | |
| STEP_URL="$JOB_URL" | |
| fi | |
| echo "job_url=$JOB_URL" >> "$GITHUB_OUTPUT" | |
| echo "step_url=$STEP_URL" >> "$GITHUB_OUTPUT" | |
| - name: Send GitHub Action trigger data to Slack | |
| id: slack | |
| if: ${{ always() && github.event_name == 'schedule' }} | |
| uses: slackapi/slack-github-action@v1.24.0 | |
| with: | |
| payload: | | |
| { | |
| "attachments": [ | |
| { | |
| "color": "${{ steps.set-color.outputs.color }}", | |
| "blocks": [ | |
| { | |
| "type": "header", | |
| "text": { "type": "plain_text", "text": "ECR scan: ${{ matrix.image_tag }}", "emoji": true } | |
| }, | |
| { | |
| "type": "context", | |
| "elements": [ | |
| { "type": "mrkdwn", "text": "*Scan time:* ${{ steps.set-date.outputs.current_datetime }}" }, | |
| { "type": "mrkdwn", "text": "<${{ steps.scan-step-link.outputs.step_url }}|Open scan report>" } | |
| ] | |
| }, | |
| { "type": "divider" }, | |
| { | |
| "type": "section", | |
| "fields": [ | |
| { "type": "mrkdwn", "text": "*Total*\n*${{ steps.fixed-counts.outputs.total }}*" }, | |
| { "type": "mrkdwn", "text": "*Critical*\n${{ steps.docker-scan.outputs.critical }}" }, | |
| { "type": "mrkdwn", "text": "*High*\n${{ steps.docker-scan.outputs.high }}" }, | |
| { "type": "mrkdwn", "text": "*Medium*\n${{ steps.docker-scan.outputs.medium }}" }, | |
| { "type": "mrkdwn", "text": "*Low*\n${{ steps.docker-scan.outputs.low }}" }, | |
| { "type": "mrkdwn", "text": "*Info*\n${{ steps.docker-scan.outputs.informational }}" }, | |
| { "type": "mrkdwn", "text": "*Undefined*\n${{ steps.fixed-counts.outputs.undefined }}" } | |
| ] | |
| }, | |
| { | |
| "type": "actions", | |
| "elements": [ | |
| { | |
| "type": "button", | |
| "text": { "type": "plain_text", "text": "Open scan report" }, | |
| "url": "${{ steps.scan-step-link.outputs.step_url }}" | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK |