name: Container CVE Scan (development) on: push: branches: ["development"] workflow_dispatch: jobs: cve-scan: name: Scan backend/frontend images for CVEs runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Docker Hub login (for Scout) if: ${{ secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }} uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build backend image (local) uses: docker/build-push-action@v6 with: context: ./backend file: ./backend/Dockerfile push: false load: true tags: nexapg-backend:dev-scan provenance: false sbom: false - name: Build frontend image (local) uses: docker/build-push-action@v6 with: context: ./frontend file: ./frontend/Dockerfile push: false load: true tags: nexapg-frontend:dev-scan build-args: | VITE_API_URL=/api/v1 provenance: false sbom: false - name: Trivy scan (backend) uses: aquasecurity/trivy-action@0.24.0 with: image-ref: nexapg-backend:dev-scan format: json output: trivy-backend.json severity: UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL ignore-unfixed: false exit-code: 0 - name: Trivy scan (frontend) uses: aquasecurity/trivy-action@0.24.0 with: image-ref: nexapg-frontend:dev-scan format: json output: trivy-frontend.json severity: UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL ignore-unfixed: false exit-code: 0 - name: Summarize Trivy severities run: | python - <<'PY' import json from collections import Counter def summarize(path): c = Counter() with open(path, "r", encoding="utf-8") as f: data = json.load(f) for result in data.get("Results", []): for v in result.get("Vulnerabilities", []) or []: c[v.get("Severity", "UNKNOWN")] += 1 for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN"]: c.setdefault(sev, 0) return c for label, path in [("backend", "trivy-backend.json"), ("frontend", "trivy-frontend.json")]: s = summarize(path) print(f"===== Trivy {label} =====") print(f"CRITICAL={s['CRITICAL']} HIGH={s['HIGH']} MEDIUM={s['MEDIUM']} LOW={s['LOW']} UNKNOWN={s['UNKNOWN']}") print() PY - name: Docker Scout scan (backend) run: | if [ -z "${{ secrets.DOCKERHUB_USERNAME }}" ] || [ -z "${{ secrets.DOCKERHUB_TOKEN }}" ]; then echo "Docker Hub Scout scan skipped: DOCKERHUB_USERNAME/DOCKERHUB_TOKEN not set." > scout-backend.txt exit 0 fi if [ ! -f "$HOME/.docker/config.json" ]; then echo "Docker Hub Scout scan skipped: docker login config not found in runner." > scout-backend.txt exit 0 fi docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v "$HOME/.docker:/root/.docker:ro" \ docker/scout-cli:latest cves nexapg-backend:dev-scan \ --only-severity critical,high,medium,low > scout-backend.txt - name: Docker Scout scan (frontend) run: | if [ -z "${{ secrets.DOCKERHUB_USERNAME }}" ] || [ -z "${{ secrets.DOCKERHUB_TOKEN }}" ]; then echo "Docker Hub Scout scan skipped: DOCKERHUB_USERNAME/DOCKERHUB_TOKEN not set." > scout-frontend.txt exit 0 fi if [ ! -f "$HOME/.docker/config.json" ]; then echo "Docker Hub Scout scan skipped: docker login config not found in runner." > scout-frontend.txt exit 0 fi docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v "$HOME/.docker:/root/.docker:ro" \ docker/scout-cli:latest cves nexapg-frontend:dev-scan \ --only-severity critical,high,medium,low > scout-frontend.txt - name: Print scan summary run: | echo "===== Docker Scout backend =====" test -f scout-backend.txt && cat scout-backend.txt || echo "scout-backend.txt not available" echo echo "===== Docker Scout frontend =====" test -f scout-frontend.txt && cat scout-frontend.txt || echo "scout-frontend.txt not available" - name: Upload scan reports uses: actions/upload-artifact@v3 with: name: container-cve-scan-reports path: | trivy-backend.json trivy-frontend.json scout-backend.txt scout-frontend.txt