Skip to content

Container Security Scan #25

Container Security Scan

Container Security Scan #25

Workflow file for this run

name: Container Security Scan
on:
workflow_run:
workflows: ["Build and Push Docker Image"]
types:
- completed
branches:
- main
push:
tags:
- 'v*'
pull_request:
branches:
- main
paths:
- 'Dockerfile'
- 'entrypoint.sh'
- '**/*.sh'
workflow_dispatch:
inputs:
image_ref:
description: 'Image reference to scan (e.g., ghcr.io/azdolinski/devcoder:v0.6.12)'
required: false
type: string
severity:
description: 'Minimum severity level to report'
required: false
default: 'medium'
type: choice
options:
- critical
- high
- medium
- low
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
extract-version:
runs-on: ubuntu-latest
if: |
(github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
(github.event.workflow_run.outputs.run_security_scan != 'false' || github.event.workflow_run.outputs.run_security_scan == '')) ||
github.event_name == 'push' ||
github.event_name == 'pull_request' ||
github.event_name == 'workflow_dispatch'
outputs:
version: ${{ steps.version.outputs.version }}
image-ref: ${{ steps.version.outputs.image-ref }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get tag from workflow_run or current event
id: get_tag
run: |
if [ "${{ github.event_name }}" == "workflow_run" ]; then
# Get the tag that was created in the build workflow
echo "Triggered by workflow_run, fetching tag..."
TAG=$(git -c 'versionsort.suffix=-alpha' -c 'versionsort.suffix=-beta' \
ls-remote --tags --sort=-v:refname origin \
| grep 'refs/tags/v' \
| head -1 \
| awk '{print $2}' \
| sed 's/refs\/tags\///' \
| tr -d '{}^')
echo "TAG=$TAG" >> $GITHUB_OUTPUT
elif [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == refs/tags/* ]]; then
# Normal tag push event
TAG="${{ github.ref_name }}"
echo "TAG=$TAG" >> $GITHUB_OUTPUT
elif [[ "${{ github.event.inputs.image_ref }}" != "" ]]; then
# Manual workflow dispatch with image reference
TAG=""
echo "TAG=" >> $GITHUB_OUTPUT
else
# Pull request or other event
VERSION=$(node -e "console.log(require('./version.json').version)" 2>/dev/null || echo "latest")
TAG="$VERSION"
echo "TAG=$TAG" >> $GITHUB_OUTPUT
fi
echo "Using tag: $TAG"
- name: Extract version and image reference
id: version
run: |
TAG="${{ steps.get_tag.outputs.TAG }}"
if [ -n "$TAG" ]; then
# Strip 'v' prefix from Git tag for Docker image reference
# Git: v0.6.13 → Docker: 0.6.13
VERSION=${TAG#v}
IMAGE_REF="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION}"
elif [[ "${{ github.event.inputs.image_ref }}" != "" ]]; then
IMAGE_REF=${{ github.event.inputs.image_ref }}
VERSION="manual"
else
VERSION=$(node -e "console.log(require('./version.json').version)" 2>/dev/null || echo "latest")
IMAGE_REF="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION}"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "image-ref=$IMAGE_REF" >> $GITHUB_OUTPUT
echo "Using image reference: $IMAGE_REF"
trivy-scan:
runs-on: ubuntu-latest
needs: extract-version
permissions:
contents: read
security-events: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build test image (for PRs)
if: ${{ github.event_name == 'pull_request' }}
run: |
docker build -t devcoder-test .
echo "IMAGE_REF=devcoder-test" >> $GITHUB_ENV
- name: Build image for scanning (for tags)
if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_run') && needs.extract-version.outputs.image-ref != '' }}
run: |
docker build -t ${{ needs.extract-version.outputs.image-ref }} .
echo "IMAGE_REF=${{ needs.extract-version.outputs.image-ref }}" >> $GITHUB_ENV
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
continue-on-error: true
with:
image-ref: ${{ env.IMAGE_REF || needs.extract-version.outputs.image-ref }}
format: 'sarif'
output: 'trivy-results.sarif'
exit-code: '0'
ignore-unfixed: true
severity: ${{ github.event.inputs.severity || 'CRITICAL,HIGH,MEDIUM' }}
vuln-type: 'os,library'
- name: Upload Trivy scan results to GitHub Security tab
if: hashFiles('trivy-results.sarif') != ''
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: 'trivy-results.sarif'
snyk-container:
runs-on: ubuntu-latest
needs: extract-version
name: Snyk Container - Docker Image Scanning
environment: prod
permissions:
contents: read
security-events: write
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build Docker image
if: ${{ env.SNYK_TOKEN != '' }}
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
IMAGE_REF=devcoder-test
else
IMAGE_REF="${{ needs.extract-version.outputs.image-ref }}"
fi
if [ -z "$IMAGE_REF" ]; then
echo "Missing image reference" >&2
exit 1
fi
echo "IMAGE_REF=$IMAGE_REF" >> $GITHUB_ENV
docker build -t "$IMAGE_REF" .
- name: Run Snyk Container scan
if: ${{ env.SNYK_TOKEN != '' }}
continue-on-error: true
uses: snyk/actions/docker@master
env:
SNYK_TOKEN: ${{ env.SNYK_TOKEN }}
with:
image: ${{ env.IMAGE_REF }}
args: --severity-threshold=${{ github.event.inputs.severity || 'medium' }} --file=Dockerfile
sarif: true
- name: Debug SARIF structure
if: ${{ always() && env.SNYK_TOKEN != '' && hashFiles('snyk.sarif') != '' }}
run: |
echo "📊 SARIF file structure:"
echo " Runs count: $(jq '.runs | length' snyk.sarif)"
echo " Tools: $(jq -r '.runs[].tool.driver.name' snyk.sarif | paste -sd ',' -)"
echo " Results per run: $(jq '.runs[].results | length' snyk.sarif | paste -sd ',' -)"
- name: Install SARIF SDK
if: ${{ always() && env.SNYK_TOKEN != '' && hashFiles('snyk.sarif') != '' }}
run: dotnet tool install --global Sarif.Multitool
- name: Merge SARIF runs using official SARIF SDK
if: ${{ always() && env.SNYK_TOKEN != '' && hashFiles('snyk.sarif') != '' }}
run: |
sarif merge snyk.sarif \
--merge-runs \
--output-file snyk-container.sarif \
--log "Optimize;Minify"
echo "✅ Merged SARIF runs → snyk-container.sarif"
echo " Final runs count: $(jq '.runs | length' snyk-container.sarif)"
echo " Final results count: $(jq '.runs[0].results | length' snyk-container.sarif)"
- name: Upload Snyk Container results to GitHub Security tab
if: ${{ always() && env.SNYK_TOKEN != '' && hashFiles('snyk-container.sarif') != '' }}
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: snyk-container.sarif
category: snyk-container
dockle-scan:
runs-on: ubuntu-latest
needs: extract-version
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build test image (for PRs)
if: ${{ github.event_name == 'pull_request' }}
run: |
docker build -t devcoder-test .
- name: Build image for Dockle scanning (for tags)
if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_run') && needs.extract-version.outputs.image-ref != '' }}
run: |
docker build -t ${{ needs.extract-version.outputs.image-ref }} .
- name: Install Dockle
run: |
curl -sL https://github.com/goodwithtech/dockle/releases/download/v0.4.14/dockle_0.4.14_Linux-64bit.tar.gz | tar -xz
sudo mv dockle /usr/local/bin/
- name: Run Dockle container image linter
run: |
if [ "${{ github.event_name }}" == "pull_request" ]; then
dockle --exit-code 1 --exit-level WARN devcoder-test || echo "Dockle scan completed with warnings"
else
dockle --exit-code 1 --exit-level WARN ${{ needs.extract-version.outputs.image-ref }} || echo "Dockle scan completed with warnings"
fi
generate-report:
runs-on: ubuntu-latest
needs: [extract-version, trivy-scan, snyk-container, dockle-scan]
if: ${{ always() && needs.trivy-scan.result == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Generate security report
run: |
echo "# Security Scan Report for ${{ needs.extract-version.outputs.image-ref }}" > security-report.md
echo "" >> security-report.md
echo "Generated on: $(date)" >> security-report.md
echo "Version: ${{ needs.extract-version.outputs.version }}" >> security-report.md
echo "" >> security-report.md
if [ -f "trivy-results.sarif" ]; then
echo "## Trivy Vulnerability Scan Results" >> security-report.md
echo "Trivy scan completed. Check the 'Security' tab for detailed results." >> security-report.md
echo "" >> security-report.md
fi
if [ -f "snyk-container.sarif" ]; then
echo "## Snyk Container Security Scan Results" >> security-report.md
echo "Snyk container scan completed. Check the 'Security' tab for detailed results." >> security-report.md
echo "" >> security-report.md
fi
echo "## Dockle Container Linting Results" >> security-report.md
echo "Container image linting completed." >> security-report.md
echo "" >> security-report.md
echo "## Summary" >> security-report.md
echo "- All security scans have been completed" >> security-report.md
echo "- Review the Security tab in this repository for detailed findings" >> security-report.md
echo "- Address any CRITICAL or HIGH severity vulnerabilities found" >> security-report.md
- name: Upload security report
uses: actions/upload-artifact@v4
with:
name: security-report-${{ needs.extract-version.outputs.version }}
path: security-report.md