Skip to content

Security Scanning #8892

Security Scanning

Security Scanning #8892

Workflow file for this run

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