Security Scanning #8892
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: Security Scanning | |
| on: | |
| push: | |
| branches: [develop, main] | |
| pull_request: | |
| branches: [develop, main] | |
| schedule: | |
| # Run daily security scans at 2 AM UTC | |
| - cron: '0 2 * * *' | |
| permissions: | |
| contents: read | |
| security-events: write | |
| pull-requests: write | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| govulncheck: | |
| name: Go Vulnerability Check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: '1.26.2' | |
| cache: true | |
| - name: Set up buf | |
| uses: bufbuild/buf-action@v1 | |
| with: | |
| setup_only: true | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate protobuf files | |
| run: buf generate | |
| - name: Run govulncheck | |
| run: | | |
| set -euo pipefail | |
| go install golang.org/x/vuln/cmd/govulncheck@latest | |
| # Known false positives - see ADR-0036 for context. | |
| # GO-2024-2476: Dex TLS config discarding. Not applicable because | |
| # embedded Dex serves HTTP behind the gateway (Caddy terminates TLS). | |
| # GO-2026-4883: Docker/Moby plugin privilege off-by-one. No fix available. | |
| # Only reachable via testcontainers in tests, not production code paths. | |
| # GO-2026-4887: Docker/Moby AuthZ plugin bypass with oversized request bodies. | |
| # No fix available. Only reachable via testcontainers in tests, not production. | |
| GOVULN_ALLOWLIST="$(printf '%s\n' GO-2024-2476 GO-2026-4883 GO-2026-4887)" | |
| govulncheck ./... 2>&1 | tee /tmp/govulncheck.out || true | |
| GOVULN_RC=${PIPESTATUS[0]:-$?} | |
| # If govulncheck failed but NOT due to found vulnerabilities, propagate the error | |
| if [ "$GOVULN_RC" -ne 0 ] && ! grep -q 'Your code is affected by' /tmp/govulncheck.out; then | |
| echo "::error::govulncheck execution failed (exit ${GOVULN_RC})" | |
| exit "$GOVULN_RC" | |
| fi | |
| # Filter by explicit vulnerability ID allowlist (not module name) | |
| if grep -q 'Your code is affected by' /tmp/govulncheck.out; then | |
| NON_ALLOWLISTED=$(grep -oE 'GO-[0-9]{4}-[0-9]+' /tmp/govulncheck.out | sort -u | grep -Fxv -e "$GOVULN_ALLOWLIST" || true) | |
| if [ -n "$NON_ALLOWLISTED" ]; then | |
| echo "::error::Non-allowlisted vulnerabilities detected: $NON_ALLOWLISTED" | |
| exit 1 | |
| fi | |
| echo "::warning::Suppressed allowlisted false positive(s) (Dex, Docker/Moby) — see ADR-0036" | |
| fi | |
| gosec: | |
| name: Security Scanner (Gosec) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: '1.26.2' | |
| cache: true | |
| - name: Set up buf | |
| uses: bufbuild/buf-action@v1 | |
| with: | |
| setup_only: true | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate protobuf files | |
| run: buf generate | |
| - name: Install Gosec | |
| # Pinned version for reproducibility. v2.21.4 incompatible with Go 1.25 | |
| # (golang.org/x/tools v0.25.0 issue). Using v2.22.0+ for compatibility. | |
| run: go install github.com/securego/gosec/v2/cmd/gosec@v2.22.0 | |
| - name: Run Gosec Security Scanner | |
| run: | | |
| # gosec v2.22.0 exits 1 with SARIF format even when no issues found | |
| # See: https://github.com/securego/gosec/issues/1279 | |
| # gosec may also fail with "package without types" error for packages | |
| # using type aliases from external libraries (e.g., samber/mo). | |
| # We capture the exit code and create an empty SARIF if needed. | |
| gosec -fmt sarif -out gosec-results.sarif -exclude-generated ./... || true | |
| # Ensure SARIF file exists (gosec may fail without creating it) | |
| if [ ! -f gosec-results.sarif ]; then | |
| echo '{"version":"2.1.0","$schema":"https://json.schemastore.org/sarif-2.1.0.json","runs":[{"tool":{"driver":{"name":"gosec","version":"2.22.0"}},"results":[]}]}' > gosec-results.sarif | |
| echo "⚠️ Gosec failed to generate SARIF output - created empty report" | |
| fi | |
| - name: Upload Gosec results to GitHub Security tab | |
| uses: github/codeql-action/upload-sarif@v4 | |
| if: always() | |
| with: | |
| sarif_file: gosec-results.sarif | |
| category: gosec | |
| trivy-repo: | |
| name: Trivy Repository Scan | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Run Trivy vulnerability scanner (repo scan) | |
| uses: aquasecurity/trivy-action@0.35.0 | |
| with: | |
| scan-type: 'fs' | |
| scan-ref: '.' | |
| format: 'sarif' | |
| output: 'trivy-repo-results.sarif' | |
| severity: 'CRITICAL,HIGH' | |
| exit-code: '1' # Fail on critical/high vulnerabilities | |
| trivyignores: '.trivyignore' | |
| limit-severities-for-sarif: true | |
| env: | |
| TRIVY_IGNOREFILE: '.trivyignore' | |
| - name: Upload Trivy results to GitHub Security tab | |
| uses: github/codeql-action/upload-sarif@v4 | |
| if: always() | |
| with: | |
| sarif_file: 'trivy-repo-results.sarif' | |
| category: trivy-repo | |
| trivy-image: | |
| name: Trivy Container Image Scan | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: [govulncheck, gosec] # Only scan image if code passes security checks | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set up buf | |
| uses: bufbuild/buf-action@v1 | |
| with: | |
| setup_only: true | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate protobuf files | |
| run: buf generate | |
| - name: Build Docker image | |
| run: | | |
| docker build -t meridian:security-scan \ | |
| --build-arg VERSION=security-scan \ | |
| --build-arg COMMIT=${{ github.sha }} \ | |
| --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ | |
| . | |
| - name: Run Trivy vulnerability scanner (image scan) | |
| uses: aquasecurity/trivy-action@0.35.0 | |
| with: | |
| image-ref: 'meridian:security-scan' | |
| format: 'sarif' | |
| output: 'trivy-image-results.sarif' | |
| severity: 'CRITICAL,HIGH' | |
| ignore-unfixed: true # Skip vulnerabilities without available fixes (common in Debian base images) | |
| exit-code: '1' # Fail on critical/high vulnerabilities with available fixes | |
| - name: Upload Trivy image results to GitHub Security tab | |
| uses: github/codeql-action/upload-sarif@v4 | |
| if: always() | |
| with: | |
| sarif_file: 'trivy-image-results.sarif' | |
| category: trivy-image | |
| dependency-review: | |
| name: Dependency Review | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Dependency Review | |
| uses: actions/dependency-review-action@v4 | |
| with: | |
| fail-on-severity: high | |
| # Dex go.mod false positives: module path lacks /v2 suffix, so GitHub | |
| # reports all historical CVEs against v0.0.0. Pinned commit (v2.45.1) | |
| # includes all fixes. See ADR-0036. | |
| # | |
| # NOTE: allow-ghsas is NOT package-scoped — these are globally ignored. | |
| # All four advisories are filed against github.com/dexidp/dex specifically | |
| # (not Go stdlib), so the global scope doesn't mask non-Dex issues. | |
| # GHSA-m9hp-7r99-94h5: XML encoding in Dex (advisory scoped to Dex module) | |
| # GHSA-2x32-jm95-2cpx: Auth bypass in Dex | |
| # GHSA-vh7g-p26c-j2cw: MitM ID token capture in Dex | |
| # GHSA-gr79-9v6v-gc9r: TLS config discarding in Dex | |
| allow-ghsas: GHSA-m9hp-7r99-94h5, GHSA-2x32-jm95-2cpx, GHSA-vh7g-p26c-j2cw, GHSA-gr79-9v6v-gc9r | |
| sbom: | |
| name: Generate SBOM | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set up buf | |
| uses: bufbuild/buf-action@v1 | |
| with: | |
| setup_only: true | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate protobuf files | |
| run: buf generate | |
| - name: Build Docker image | |
| run: | | |
| docker build -t meridian:sbom \ | |
| --build-arg VERSION=sbom \ | |
| --build-arg COMMIT=${{ github.sha }} \ | |
| --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ | |
| . | |
| - name: Generate SBOM with Syft | |
| uses: anchore/sbom-action@v0.24.0 | |
| with: | |
| image: meridian:sbom | |
| format: spdx-json | |
| output-file: sbom.spdx.json | |
| - name: Verify SBOM was created | |
| run: | | |
| if [ ! -f sbom.spdx.json ]; then | |
| echo "ERROR: SBOM file not created" | |
| exit 1 | |
| fi | |
| echo "✅ SBOM file created successfully" | |
| echo "File size: $(du -h sbom.spdx.json | cut -f1)" | |
| - name: Upload SBOM artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: sbom | |
| path: sbom.spdx.json | |
| retention-days: 90 | |
| secrets-scan: | |
| name: Secret Scanning with Gitleaks | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 # Full history for secret scanning | |
| - name: Run Gitleaks | |
| uses: gitleaks/gitleaks-action@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} # Optional: for Gitleaks Pro features |