name: Security Scanning on: push: branches: [master] pull_request: branches: [master] schedule: # Run daily at 05:43 UTC — catch new CVEs early - cron: "43 5 * * *" concurrency: group: security-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: # ── Dependency Audit ───────────────────────────────────────────── dependency-audit: name: Dependency Audit (pnpm) runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile - name: Run pnpm audit run: | echo "## Dependency Audit Report" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY # Run audit, capture output and exit code set +e AUDIT_OUTPUT=$(pnpm audit --json 2>&1) AUDIT_EXIT=$? set -e # Parse and display summary echo "$AUDIT_OUTPUT" | jq -r ' if .metadata then "| Severity | Count |\n|----------|-------|\n" + "| Critical | \(.metadata.vulnerabilities.critical // 0) |\n" + "| High | \(.metadata.vulnerabilities.high // 0) |\n" + "| Moderate | \(.metadata.vulnerabilities.moderate // 0) |\n" + "| Low | \(.metadata.vulnerabilities.low // 0) |\n" + "| Info | \(.metadata.vulnerabilities.info // 0) |" else "No vulnerabilities found ✅" end ' >> $GITHUB_STEP_SUMMARY 2>/dev/null || echo "Audit completed" >> $GITHUB_STEP_SUMMARY # Also run human-readable output pnpm audit 2>&1 || true # Fail on critical or high vulnerabilities only pnpm audit --audit-level=critical continue-on-error: false # ── Container Scanning (Trivy) ─────────────────────────────────── trivy-api: name: Trivy Scan — API Image runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build API image for scanning uses: docker/build-push-action@v6 with: context: . file: apps/api/Dockerfile push: false load: true tags: goodgo-api:scan cache-from: type=gha,scope=api-scan cache-to: type=gha,mode=max,scope=api-scan - name: Trivy table output (API) uses: aquasecurity/trivy-action@v0.36.0 with: image-ref: "goodgo-api:scan" format: "table" severity: "CRITICAL,HIGH,MEDIUM" ignore-unfixed: true trivy-web: name: Trivy Scan — Web Image runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build Web image for scanning uses: docker/build-push-action@v6 with: context: . file: apps/web/Dockerfile push: false load: true tags: goodgo-web:scan cache-from: type=gha,scope=web-scan cache-to: type=gha,mode=max,scope=web-scan - name: Trivy table output (Web) uses: aquasecurity/trivy-action@v0.36.0 with: image-ref: "goodgo-web:scan" format: "table" severity: "CRITICAL,HIGH,MEDIUM" ignore-unfixed: true trivy-ai: name: Trivy Scan — AI Services Image runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build AI Services image for scanning uses: docker/build-push-action@v6 with: context: ./libs/ai-services file: libs/ai-services/Dockerfile push: false load: true tags: goodgo-ai:scan cache-from: type=gha,scope=ai-scan cache-to: type=gha,mode=max,scope=ai-scan - name: Trivy table output (AI) uses: aquasecurity/trivy-action@v0.36.0 with: image-ref: "goodgo-ai:scan" format: "table" severity: "CRITICAL,HIGH,MEDIUM" ignore-unfixed: true # ── Filesystem / IaC Scanning ──────────────────────────────────── trivy-fs: name: Trivy Filesystem Scan runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v4 - name: Trivy filesystem table output uses: aquasecurity/trivy-action@v0.36.0 with: scan-type: "fs" scan-ref: "." format: "table" severity: "CRITICAL,HIGH,MEDIUM" scanners: "vuln,secret,misconfig" # ── Summary Gate ───────────────────────────────────────────────── security-gate: name: Security Gate runs-on: ubuntu-latest needs: [dependency-audit, trivy-api, trivy-web, trivy-ai, trivy-fs] if: always() steps: - name: Check security scan results run: | echo "## Security Scan Summary" >> $GITHUB_STEP_SUMMARY # Check each job result FAILED=false if [ "${{ needs.dependency-audit.result }}" != "success" ]; then echo "❌ Dependency audit: ${{ needs.dependency-audit.result }}" >> $GITHUB_STEP_SUMMARY FAILED=true else echo "✅ Dependency audit: passed" >> $GITHUB_STEP_SUMMARY fi if [ "${{ needs.trivy-api.result }}" != "success" ]; then echo "❌ Trivy API scan: ${{ needs.trivy-api.result }}" >> $GITHUB_STEP_SUMMARY FAILED=true else echo "✅ Trivy API scan: passed" >> $GITHUB_STEP_SUMMARY fi if [ "${{ needs.trivy-web.result }}" != "success" ]; then echo "❌ Trivy Web scan: ${{ needs.trivy-web.result }}" >> $GITHUB_STEP_SUMMARY FAILED=true else echo "✅ Trivy Web scan: passed" >> $GITHUB_STEP_SUMMARY fi if [ "${{ needs.trivy-ai.result }}" != "success" ]; then echo "❌ Trivy AI scan: ${{ needs.trivy-ai.result }}" >> $GITHUB_STEP_SUMMARY FAILED=true else echo "✅ Trivy AI scan: passed" >> $GITHUB_STEP_SUMMARY fi if [ "${{ needs.trivy-fs.result }}" != "success" ]; then echo "❌ Trivy filesystem scan: ${{ needs.trivy-fs.result }}" >> $GITHUB_STEP_SUMMARY FAILED=true else echo "✅ Trivy filesystem scan: passed" >> $GITHUB_STEP_SUMMARY fi if [ "$FAILED" = true ]; then echo "" echo "⚠️ One or more security scans failed. Review the Security tab for details." >> $GITHUB_STEP_SUMMARY exit 1 fi echo "" echo "🎉 All security scans passed!" >> $GITHUB_STEP_SUMMARY