Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 279 additions & 0 deletions .github/workflows/03-build-secure.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
name: Secure Docker Build & Scan

on:
pull_request:
branches: [main]
paths:
# Only run on PR if Dockerfiles or dependencies change
- 'backend/Dockerfile.backend'
- 'backend/pyproject.toml'
- 'backend/poetry.lock'
- 'frontend/Dockerfile.frontend'
- 'frontend/package*.json'
- 'docker-compose*.yml'
- '.github/workflows/03-build-secure.yml'
push:
branches: [main]
# Always 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

Check warning on line 22 in .github/workflows/03-build-secure.yml

View workflow job for this annotation

GitHub Actions / YAML Lint

22:5 [comments-indentation] comment not indented like content

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: backend
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@v4

- 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@v3
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: πŸ—οΈ Build Docker Image
uses: docker/build-push-action@v5
with:
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
push: false
load: true
tags: ${{ matrix.image_name }}:${{ github.sha }}
# No external cache to avoid slow export - rely on BuildKit's internal cache
# BuildKit cache mounts in Dockerfile are sufficient for speed
build-args: |
BUILDKIT_INLINE_CACHE=1

# 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@v3
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@master
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@v3
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@master
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 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@master
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@v3
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

Check warning on line 279 in .github/workflows/03-build-secure.yml

View workflow job for this annotation

GitHub Actions / YAML Lint

279:121 [line-length] line too long (129 > 120 characters)
69 changes: 10 additions & 59 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,76 +137,27 @@ jobs:
--tb=short
continue-on-error: true # Don't fail on coverage threshold initially

# Build images once
build:
runs-on: ubuntu-latest
outputs:
backend-image: ${{ steps.build.outputs.backend-image }}
frontend-image: ${{ steps.build.outputs.frontend-image }}
steps:
- uses: actions/checkout@v4

- name: Free Up Disk Space
run: |
echo "Freeing up disk space before Docker builds..."
df -h

# Remove unnecessary packages and files (~14GB)
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/share/boost
sudo rm -rf "$AGENT_TOOLSDIRECTORY"

# Clean Docker build cache
docker system prune -af --volumes || true

df -h
echo "βœ… Disk space freed"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-

- name: Build images (no push on PRs)
id: build
run: |
# Build with commit SHA for uniqueness
BACKEND_TAG="ghcr.io/manavgup/rag_modulo/backend:${{ github.sha }}"
FRONTEND_TAG="ghcr.io/manavgup/rag_modulo/frontend:${{ github.sha }}"

echo "Building backend image..."
docker build -t $BACKEND_TAG -f ./backend/Dockerfile.backend ./backend

echo "Building frontend image..."
docker build -t $FRONTEND_TAG -f ./frontend/Dockerfile.frontend ./frontend

echo "Images built successfully (not pushing on PRs)"
echo "backend-image=$BACKEND_TAG" >> $GITHUB_OUTPUT
echo "frontend-image=$FRONTEND_TAG" >> $GITHUB_OUTPUT

# NOTE: Build job removed - now handled by 03-build-secure.yml workflow
# This eliminates duplicate builds (was building in both ci.yml and 03-build-secure.yml)
# Security scanning workflow (03-build-secure.yml) now handles both building AND scanning

# Simple reporting without complex XML parsing
report:
needs: [lint-and-unit, build]
needs: [lint-and-unit]
runs-on: ubuntu-latest
if: always()
steps:
- name: Report results
run: |
echo "## CI/CD Results"
echo "- Lint and Unit Tests: ${{ needs.lint-and-unit.result }}"
echo "- Build: ${{ needs.build.result }}"
echo ""
echo "Note: Docker builds handled by 'Secure Docker Build & Scan' workflow"
echo "This eliminates duplicate builds and ensures all images are security scanned"

if [[ "${{ needs.lint-and-unit.result }}" == "failure" || "${{ needs.build.result }}" == "failure" ]]; then
echo "❌ Critical jobs failed"
if [[ "${{ needs.lint-and-unit.result }}" == "failure" ]]; then
echo "❌ Lint/unit tests failed"
exit 1
else
echo "βœ… Core jobs passed"
echo "βœ… Lint and unit tests passed"
fi
Loading
Loading