Skip to content

Secure Docker Build & Scan #310

Secure Docker Build & Scan

Secure Docker Build & Scan #310

name: Secure Docker Build & Scan
# πŸš€ CI/CD OPTIMIZATION: Docker builds moved to post-merge only
# ================================================================
# WHY: PR feedback time reduced from 18-22 min to 3-4 min (85% faster)
# SECURITY: Dependency scanning still runs on PRs via Trivy filesystem scans (02-security.yml)
# WHEN: Builds run after merge to main, weekly scans, or manual trigger
# REF: docs/development/CI_CD_OPTIMIZATION_ANALYSIS.md
on:
push:
branches: [main]
# Always build and scan on merge to main
schedule:
# Weekly CVE scan every Tuesday at 6:17 PM UTC
- cron: "17 18 * * 2"
workflow_dispatch: # Manual trigger option
# Cancel outdated workflow runs for the same PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
security-events: write # For SARIF uploads
actions: read
jobs:
security-scan:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- service: backend
dockerfile: backend/Dockerfile.backend
context: .
image_name: rag-modulo-backend
ghcr_image: ghcr.io/manavgup/rag_modulo/backend
- service: frontend
dockerfile: frontend/Dockerfile.frontend
context: frontend
image_name: rag-modulo-frontend
ghcr_image: ghcr.io/manavgup/rag_modulo/frontend
name: πŸ”’ Security Scan - ${{ matrix.service }}
steps:
- name: πŸ“₯ Checkout code
uses: actions/checkout@v5
- name: 🧹 Free Up Disk Space
run: |
# Always cleanup for Docker builds - they need significant space
# Backend build alone can use 6-8GB with layers
echo "Initial: $(df -h / | awk 'NR==2 {print $4}') available"
# Run removals in parallel for speed
sudo rm -rf /usr/share/dotnet &
sudo rm -rf /opt/ghc &
sudo rm -rf /usr/local/share/boost &
sudo rm -rf "$AGENT_TOOLSDIRECTORY" &
sudo rm -rf /usr/local/lib/android &
sudo rm -rf /usr/share/swift &
wait
docker system prune -af --volumes || true
echo "After cleanup: $(df -h / | awk 'NR==2 {print $4}') available"
- name: 🐳 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# ===== STAGE 1: Dockerfile Security (Hadolint) =====
- name: πŸ” Hadolint - Dockerfile Security Scan
id: hadolint
continue-on-error: true
run: |
# Use hadolint directly instead of the action (which has Docker issues)
docker run --rm -i hadolint/hadolint:latest hadolint \
--format sarif \
--no-fail \
- < ${{ matrix.dockerfile }} > hadolint-${{ matrix.service }}.sarif || true
# Check if file was created and has content
if [ -f "hadolint-${{ matrix.service }}.sarif" ] && [ -s "hadolint-${{ matrix.service }}.sarif" ]; then
echo "βœ… Hadolint scan completed"
echo "hadolint_success=true" >> $GITHUB_OUTPUT
else
echo "⚠️ Hadolint scan failed or produced no output"
echo "hadolint_success=false" >> $GITHUB_OUTPUT
fi
- name: πŸ“€ Upload Hadolint SARIF
uses: github/codeql-action/upload-sarif@v4
if: always() && steps.hadolint.outputs.hadolint_success == 'true'
with:
sarif_file: hadolint-${{ matrix.service }}.sarif
category: hadolint-${{ matrix.service }}
# ===== STAGE 2: Build Docker Image with Optimizations =====
- name: πŸ”„ Restore BuildKit Cache
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ matrix.service }}-${{ hashFiles(format('{0}/poetry.lock', matrix.context),
format('{0}/package-lock.json', matrix.context)) }}
restore-keys: |
${{ runner.os }}-buildx-${{ matrix.service }}-
${{ runner.os }}-buildx-
- name: πŸ—οΈ Build Docker Image
uses: docker/build-push-action@v6
with:
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
push: false
load: true
tags: ${{ matrix.image_name }}:${{ github.sha }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
build-args: |
BUILDKIT_INLINE_CACHE=1
${{ matrix.service == 'backend' && format('BACKEND_CACHE_BUST={0}', hashFiles('backend/**/*.py', 'backend/Dockerfile.backend', 'pyproject.toml', 'poetry.lock')) || '' }}
# Move cache to optimize for next run (temp workaround for actions/cache#828)
- name: πŸ’Ύ Save BuildKit Cache
if: always()
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache || true
# Clean up build cache immediately to free space
- name: 🧹 Clean Build Cache
if: always()
run: |
docker builder prune -af --filter "until=1h" || true
echo "Build cache cleaned"
# ===== STAGE 3: Container Security (Dockle) =====
- name: πŸ›‘οΈ Dockle - Container Security Scan
id: dockle
continue-on-error: true
uses: erzz/dockle-action@v1
with:
image: ${{ matrix.image_name }}:${{ github.sha }}
exit-code: "0" # Don't fail, just report
failure-threshold: warn # Correct parameter name
report-format: sarif
report-name: dockle-${{ matrix.service }}
- name: Check Dockle Output
id: check-dockle
if: always()
run: |
if [ -f "dockle-${{ matrix.service }}.sarif" ] && [ -s "dockle-${{ matrix.service }}.sarif" ]; then
echo "dockle_success=true" >> $GITHUB_OUTPUT
else
echo "dockle_success=false" >> $GITHUB_OUTPUT
fi
- name: πŸ“€ Upload Dockle SARIF
uses: github/codeql-action/upload-sarif@v4
if: always() && steps.check-dockle.outputs.dockle_success == 'true'
with:
sarif_file: dockle-${{ matrix.service }}.sarif
category: dockle-${{ matrix.service }}
# ===== STAGE 4: Vulnerability Scan (Trivy) =====
- name: πŸ”Ž Trivy - Vulnerability Scan
id: trivy
continue-on-error: true
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 pinned to SHA
with:
image-ref: ${{ matrix.image_name }}:${{ github.sha }}
format: "sarif"
output: "trivy-${{ matrix.service }}.sarif"
severity: "CRITICAL,HIGH,MEDIUM"
exit-code: "0" # Report but don't fail on vulnerabilities
ignore-unfixed: true
- name: Check Trivy Output
id: check-trivy
if: always()
run: |
if [ -f "trivy-${{ matrix.service }}.sarif" ] && [ -s "trivy-${{ matrix.service }}.sarif" ]; then
echo "trivy_success=true" >> $GITHUB_OUTPUT
else
echo "trivy_success=false" >> $GITHUB_OUTPUT
fi
- name: πŸ“€ Upload Trivy SARIF
uses: github/codeql-action/upload-sarif@v4
if: always() && steps.check-trivy.outputs.trivy_success == 'true'
with:
sarif_file: trivy-${{ matrix.service }}.sarif
category: trivy-${{ matrix.service }}
- name: πŸ”Ž Trivy - Critical CVE Check
id: trivy-critical
continue-on-error: true
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 pinned to SHA
with:
image-ref: ${{ matrix.image_name }}:${{ github.sha }}
format: "table"
severity: "CRITICAL"
exit-code: "0" # Don't fail the build, just report
ignore-unfixed: true
# ===== STAGE 4.5: Grype Vulnerability Scan (Backup Scanner) =====
# IBM uses both Trivy + Grype for comprehensive coverage
# Grype provides better fix recommendations and broader CVE database
- name: πŸ“₯ Install Grype CLI
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
- name: πŸ” Grype - Vulnerability Scan
continue-on-error: true
run: |
grype ${{ matrix.image_name }}:${{ github.sha }} \
--scope all-layers \
--only-fixed \
--output table
- name: πŸ“„ Grype - Generate SARIF Report
continue-on-error: true
run: |
grype ${{ matrix.image_name }}:${{ github.sha }} \
--scope all-layers \
--output sarif \
--file grype-${{ matrix.service }}.sarif
- name: πŸ“€ Upload Grype SARIF
uses: github/codeql-action/upload-sarif@v4
if: always()
continue-on-error: true
with:
sarif_file: grype-${{ matrix.service }}.sarif
category: grype-${{ matrix.service }}
# ===== STAGE 5: SBOM Generation (Syft) =====
- name: πŸ“‹ Syft - Generate SBOM
id: syft
continue-on-error: true
uses: anchore/sbom-action@v0
with:
image: ${{ matrix.image_name }}:${{ github.sha }}
format: spdx-json
output-file: sbom-${{ matrix.service }}.spdx.json
- name: Check SBOM Output
id: check-sbom
if: always()
run: |
if [ -f "sbom-${{ matrix.service }}.spdx.json" ] && [ -s "sbom-${{ matrix.service }}.spdx.json" ]; then
echo "sbom_success=true" >> $GITHUB_OUTPUT
else
echo "sbom_success=false" >> $GITHUB_OUTPUT
fi
- name: πŸ“€ Upload SBOM Artifact
uses: actions/upload-artifact@v4
if: always() && steps.check-sbom.outputs.sbom_success == 'true'
with:
name: sbom-${{ matrix.service }}
path: sbom-${{ matrix.service }}.spdx.json
retention-days: 90
# ===== STAGE 6: Additional Trivy Scans =====
- name: πŸ” Trivy - Filesystem Scan
id: trivy-fs
continue-on-error: true
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 pinned to SHA
with:
scan-type: "fs"
scan-ref: ${{ matrix.context }}
format: "sarif"
output: "trivy-fs-${{ matrix.service }}.sarif"
severity: "CRITICAL,HIGH"
exit-code: "0"
- name: Check Trivy Filesystem Output
id: check-trivy-fs
if: always()
run: |
if [ -f "trivy-fs-${{ matrix.service }}.sarif" ] && [ -s "trivy-fs-${{ matrix.service }}.sarif" ]; then
echo "trivy_fs_success=true" >> $GITHUB_OUTPUT
else
echo "trivy_fs_success=false" >> $GITHUB_OUTPUT
fi
- name: πŸ“€ Upload Trivy Filesystem SARIF
uses: github/codeql-action/upload-sarif@v4
if: always() && steps.check-trivy-fs.outputs.trivy_fs_success == 'true'
with:
sarif_file: trivy-fs-${{ matrix.service }}.sarif
category: trivy-fs-${{ matrix.service }}
security-summary:
runs-on: ubuntu-latest
needs: security-scan
if: always()
steps:
- name: πŸ“Š Security Scan Summary
run: |
echo "## πŸ”’ Security Scan Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "All security scans completed for backend and frontend services." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Scan Coverage" >> $GITHUB_STEP_SUMMARY
echo "- βœ… **Hadolint**: Dockerfile best practices and security" >> $GITHUB_STEP_SUMMARY
echo "- βœ… **Dockle**: Container image security checks" >> $GITHUB_STEP_SUMMARY
echo "- βœ… **Trivy**: CVE vulnerability scanning (image + filesystem)" >> $GITHUB_STEP_SUMMARY
echo "- βœ… **Syft**: SBOM generation for supply chain security" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Results" >> $GITHUB_STEP_SUMMARY
echo "πŸ“‹ Check the **Security** tab for detailed findings" >> $GITHUB_STEP_SUMMARY
echo "πŸ“¦ SBOM artifacts available in workflow artifacts" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Next Steps" >> $GITHUB_STEP_SUMMARY
echo "1. Review SARIF results in GitHub Security tab" >> $GITHUB_STEP_SUMMARY
echo "2. Download and verify SBOM artifacts" >> $GITHUB_STEP_SUMMARY
echo "3. Address any CRITICAL vulnerabilities before merging" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "---" >> $GITHUB_STEP_SUMMARY
echo "πŸ”— **Documentation**:" \
"[CI/CD Security Pipeline](../../docs/development/ci-cd-security.md)" \
>> $GITHUB_STEP_SUMMARY