Both workflow files referenced 'main' branch for push/PR triggers, but the repo default branch is 'master'. This caused security scanning and CodeQL analysis to never trigger on pushes to the default branch. Co-Authored-By: Paperclip <noreply@paperclip.ing>
313 lines
9.5 KiB
YAML
313 lines
9.5 KiB
YAML
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
|
|
security-events: write
|
|
|
|
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: Run Trivy vulnerability scanner (API)
|
|
uses: aquasecurity/trivy-action@0.28.0
|
|
with:
|
|
image-ref: "goodgo-api:scan"
|
|
format: "sarif"
|
|
output: "trivy-api-results.sarif"
|
|
severity: "CRITICAL,HIGH"
|
|
# Ignore unfixed vulns to reduce noise
|
|
ignore-unfixed: true
|
|
|
|
- name: Upload Trivy SARIF (API)
|
|
uses: github/codeql-action/upload-sarif@v3
|
|
if: always()
|
|
with:
|
|
sarif_file: "trivy-api-results.sarif"
|
|
category: "trivy-api"
|
|
|
|
- name: Trivy table output (API)
|
|
uses: aquasecurity/trivy-action@0.28.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: Run Trivy vulnerability scanner (Web)
|
|
uses: aquasecurity/trivy-action@0.28.0
|
|
with:
|
|
image-ref: "goodgo-web:scan"
|
|
format: "sarif"
|
|
output: "trivy-web-results.sarif"
|
|
severity: "CRITICAL,HIGH"
|
|
ignore-unfixed: true
|
|
|
|
- name: Upload Trivy SARIF (Web)
|
|
uses: github/codeql-action/upload-sarif@v3
|
|
if: always()
|
|
with:
|
|
sarif_file: "trivy-web-results.sarif"
|
|
category: "trivy-web"
|
|
|
|
- name: Trivy table output (Web)
|
|
uses: aquasecurity/trivy-action@0.28.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: Run Trivy vulnerability scanner (AI)
|
|
uses: aquasecurity/trivy-action@0.28.0
|
|
with:
|
|
image-ref: "goodgo-ai:scan"
|
|
format: "sarif"
|
|
output: "trivy-ai-results.sarif"
|
|
severity: "CRITICAL,HIGH"
|
|
ignore-unfixed: true
|
|
|
|
- name: Upload Trivy SARIF (AI)
|
|
uses: github/codeql-action/upload-sarif@v3
|
|
if: always()
|
|
with:
|
|
sarif_file: "trivy-ai-results.sarif"
|
|
category: "trivy-ai"
|
|
|
|
- name: Trivy table output (AI)
|
|
uses: aquasecurity/trivy-action@0.28.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: Run Trivy filesystem scanner
|
|
uses: aquasecurity/trivy-action@0.28.0
|
|
with:
|
|
scan-type: "fs"
|
|
scan-ref: "."
|
|
format: "sarif"
|
|
output: "trivy-fs-results.sarif"
|
|
severity: "CRITICAL,HIGH"
|
|
ignore-unfixed: true
|
|
scanners: "vuln,secret,misconfig"
|
|
|
|
- name: Upload Trivy SARIF (filesystem)
|
|
uses: github/codeql-action/upload-sarif@v3
|
|
if: always()
|
|
with:
|
|
sarif_file: "trivy-fs-results.sarif"
|
|
category: "trivy-filesystem"
|
|
|
|
- name: Trivy filesystem table output
|
|
uses: aquasecurity/trivy-action@0.28.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
|