Repository navigation
Secure Docker Build & Scan #310
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: 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 |