fix: solve bundler mismatch #466
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 Scan | |
| on: | |
| push: | |
| branches: [ master, develop ] | |
| pull_request: | |
| branches: [ master, develop ] | |
| schedule: | |
| # Run weekly on Monday at 9am UTC | |
| - cron: '0 9 * * 1' | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| jobs: | |
| brakeman: | |
| name: Brakeman Security Scan | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Set up Ruby | |
| uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1 | |
| with: | |
| ruby-version: 3.4.5 | |
| bundler-cache: true | |
| - name: Run Brakeman | |
| run: | | |
| bundle exec brakeman --rails7 \ | |
| --format json \ | |
| --output brakeman-report.json \ | |
| --no-exit-on-warn \ | |
| --no-exit-on-error | |
| - name: Parse Results | |
| id: parse | |
| run: | | |
| WARNINGS=$(jq '.warnings | length' brakeman-report.json) | |
| HIGH=$(jq '[.warnings[] | select(.confidence == "High")] | length' brakeman-report.json) | |
| echo "warnings=$WARNINGS" >> "$GITHUB_OUTPUT" | |
| echo "high=$HIGH" >> "$GITHUB_OUTPUT" | |
| - name: Upload Report | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: brakeman-report | |
| path: brakeman-report.json | |
| - name: Comment PR | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6 | |
| with: | |
| script: | | |
| const warnings = '${{ steps.parse.outputs.warnings }}'; | |
| const high = '${{ steps.parse.outputs.high }}'; | |
| const body = `## 🔒 Brakeman Security Scan | |
| - Total warnings: ${warnings} | |
| - High confidence: ${high} | |
| ${high > 0 ? '⚠️ High confidence issues found! Please review.' : '✅ No high confidence issues found.'} | |
| `; | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body, | |
| }); | |
| - name: Fail on High Confidence Issues | |
| if: steps.parse.outputs.high > 0 | |
| run: | | |
| echo "::error::Found ${{ steps.parse.outputs.high }} high confidence security issues!" | |
| exit 1 | |
| dependency-check: | |
| name: Dependency Vulnerability Check | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Set up Ruby | |
| uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1 | |
| with: | |
| ruby-version: 3.4.5 | |
| bundler-cache: true | |
| - name: Update Vulnerability Database | |
| run: bundle exec bundler-audit update | |
| - name: Run Bundle Audit | |
| id: audit | |
| run: | | |
| if ! bundle exec bundler-audit check; then | |
| echo "vulnerabilities=true" >> "$GITHUB_OUTPUT" | |
| bundle exec bundler-audit check > bundle-audit.txt || true | |
| else | |
| echo "vulnerabilities=false" >> "$GITHUB_OUTPUT" | |
| bundle exec bundler-audit check > bundle-audit.txt | |
| fi | |
| - name: Upload Report | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: bundle-audit-report | |
| path: bundle-audit.txt | |
| - name: Comment PR | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const report = fs.existsSync('bundle-audit.txt') | |
| ? fs.readFileSync('bundle-audit.txt', 'utf8') | |
| : 'No report generated.'; | |
| const hasVulns = '${{ steps.audit.outputs.vulnerabilities }}' === 'true'; | |
| const body = `## 📦 Dependency Security Check | |
| ${hasVulns ? '⚠️ Vulnerabilities found in dependencies!' : '✅ No known vulnerabilities found.'} | |
| <details> | |
| <summary>View Report</summary> | |
| \`\`\` | |
| ${report} | |
| \`\`\` | |
| </details> | |
| `; | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body, | |
| }); | |
| - name: Fail on Vulnerabilities | |
| if: steps.audit.outputs.vulnerabilities == 'true' | |
| run: | | |
| echo "::error::Vulnerable dependencies found!" | |
| exit 1 | |
| semgrep: | |
| name: Semgrep Static Analysis | |
| runs-on: ubuntu-latest | |
| container: | |
| image: returntocorp/semgrep | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Run Semgrep | |
| run: | | |
| semgrep scan \ | |
| --config=auto \ | |
| --json \ | |
| --output=semgrep-report.json \ | |
| --exclude 'scripts/' \ | |
| --exclude 'load_tests/' \ | |
| --exclude 'security_tests/' \ | |
| --exclude 'vendor/' \ | |
| --verbose \ | |
| || true | |
| - name: Parse Results | |
| id: parse | |
| run: | | |
| TOTAL=$(jq '.results | length' semgrep-report.json) | |
| ERRORS=$(jq '.results | map(select(.extra.severity == "ERROR")) | length' semgrep-report.json) | |
| WARNINGS=$(jq '.results | map(select(.extra.severity == "WARNING")) | length' semgrep-report.json) | |
| CRITICAL=$(jq '.results | map(select(.extra.metadata.confidence == "HIGH" and (.extra.metadata.subcategory // "vuln") != "audit")) | length' semgrep-report.json) | |
| echo "errors=$ERRORS" >> "$GITHUB_OUTPUT" | |
| echo "warnings=$WARNINGS" >> "$GITHUB_OUTPUT" | |
| echo "critical=$CRITICAL" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Total findings: $TOTAL — errors: $ERRORS, warnings: $WARNINGS, critical: $CRITICAL" | |
| if [ "$ERRORS" -gt 0 ]; then | |
| echo "::group::ERROR Severity Issues" | |
| jq -r '.results[] | select(.extra.severity == "ERROR") | " - \(.path):\(.start.line) — \(.check_id)"' semgrep-report.json | |
| echo "::endgroup::" | |
| fi | |
| - name: Upload Report | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: semgrep-report | |
| path: semgrep-report.json | |
| - name: Comment PR | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6 | |
| with: | |
| script: | | |
| const errors = '${{ steps.parse.outputs.errors }}'; | |
| const warnings = '${{ steps.parse.outputs.warnings }}'; | |
| const critical = '${{ steps.parse.outputs.critical }}'; | |
| const body = `## 🔍 Semgrep Static Analysis | |
| | Severity | Count | | |
| |----------|-------| | |
| | Errors | ${errors} | | |
| | Critical (HIGH confidence) | ${critical} | | |
| | Warnings | ${warnings} | | |
| ${errors > 0 ? '❌ Security errors found! Please fix before merging.' | |
| : critical > 0 ? '⚠️ High confidence issues found. Please review.' | |
| : warnings > 0 ? '⚠️ Warnings found (non-blocking).' | |
| : '✅ No issues found.'} | |
| `; | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body, | |
| }); | |
| - name: Fail on Critical Errors | |
| if: steps.parse.outputs.errors > 0 | |
| run: | | |
| echo "::error::Semgrep found ${{ steps.parse.outputs.errors }} ERROR severity issues." | |
| exit 1 | |
| secret-scan: | |
| name: Secret Detection | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: TruffleHog Secret Scan | |
| uses: trufflesecurity/trufflehog@6961f2bace57ab32b23b3ba40f8f420f6bc7e004 # main | |
| with: | |
| path: ./ | |
| extra_args: --only-verified | |
| # Dynamic Application Security Testing (DAST) | |
| ssrf-protection: | |
| name: SSRF Protection Test | |
| runs-on: ubuntu-latest | |
| services: | |
| postgres: | |
| image: postgres:15-alpine | |
| env: | |
| POSTGRES_PASSWORD: postgres | |
| POSTGRES_DB: prostaff_test | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 5432:5432 | |
| redis: | |
| image: redis:7-alpine | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 6379:6379 | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Set up Ruby | |
| uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1 | |
| with: | |
| ruby-version: 3.4.5 | |
| bundler-cache: true | |
| - name: Setup Database | |
| env: | |
| RAILS_ENV: test | |
| DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/prostaff_test | |
| REDIS_URL: redis://127.0.0.1:6379/0 | |
| run: bundle exec rails db:create db:migrate RAILS_ENV=test | |
| - name: Start Rails Server | |
| env: | |
| RAILS_ENV: test | |
| DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/prostaff_test | |
| REDIS_URL: redis://127.0.0.1:6379/0 | |
| JWT_SECRET_KEY: test_jwt_secret_key_for_ci | |
| RIOT_API_KEY: ${{ secrets.RIOT_API_KEY || 'dummy_key' }} | |
| run: | | |
| bundle exec rails server -p 3333 -d | |
| timeout 60 bash -c 'until curl -sf http://localhost:3333/up; do sleep 2; done' | |
| - name: Run SSRF Protection Tests | |
| run: | | |
| chmod +x .pentest/test-ssrf-quick.sh | |
| .pentest/test-ssrf-quick.sh | |
| - name: Upload Results | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: ssrf-test-results | |
| path: security_tests/reports/ssrf/ | |
| authentication-test: | |
| name: Authentication Security Test | |
| runs-on: ubuntu-latest | |
| services: | |
| postgres: | |
| image: postgres:15-alpine | |
| env: | |
| POSTGRES_PASSWORD: postgres | |
| POSTGRES_DB: prostaff_test | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 5432:5432 | |
| redis: | |
| image: redis:7-alpine | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 6379:6379 | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Set up Ruby | |
| uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1 | |
| with: | |
| ruby-version: 3.4.5 | |
| bundler-cache: true | |
| - name: Setup Database | |
| env: | |
| RAILS_ENV: test | |
| DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/prostaff_test | |
| REDIS_URL: redis://127.0.0.1:6379/0 | |
| run: bundle exec rails db:create db:migrate RAILS_ENV=test | |
| - name: Start Rails Server | |
| env: | |
| RAILS_ENV: test | |
| DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/prostaff_test | |
| REDIS_URL: redis://127.0.0.1:6379/0 | |
| JWT_SECRET_KEY: test_jwt_secret_key_for_ci | |
| RIOT_API_KEY: ${{ secrets.RIOT_API_KEY || 'dummy_key' }} | |
| run: | | |
| bundle exec rails server -p 3333 -d | |
| timeout 60 bash -c 'until curl -sf http://localhost:3333/up; do sleep 2; done' | |
| - name: Run Authentication Tests | |
| run: | | |
| chmod +x .pentest/test-authentication-quick.sh | |
| .pentest/test-authentication-quick.sh | |
| sql-injection-test: | |
| name: SQL Injection Protection Test | |
| runs-on: ubuntu-latest | |
| services: | |
| postgres: | |
| image: postgres:15-alpine | |
| env: | |
| POSTGRES_PASSWORD: postgres | |
| POSTGRES_DB: prostaff_test | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 5432:5432 | |
| redis: | |
| image: redis:7-alpine | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 6379:6379 | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Set up Ruby | |
| uses: ruby/setup-ruby@09a7688d3b55cf0e976497ff046b70949eeaccfd # v1 | |
| with: | |
| ruby-version: 3.4.5 | |
| bundler-cache: true | |
| - name: Setup Database | |
| env: | |
| RAILS_ENV: test | |
| DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/prostaff_test | |
| REDIS_URL: redis://127.0.0.1:6379/0 | |
| run: bundle exec rails db:create db:migrate RAILS_ENV=test | |
| - name: Start Rails Server | |
| env: | |
| RAILS_ENV: test | |
| DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/prostaff_test | |
| REDIS_URL: redis://127.0.0.1:6379/0 | |
| JWT_SECRET_KEY: test_jwt_secret_key_for_ci | |
| RIOT_API_KEY: ${{ secrets.RIOT_API_KEY || 'dummy_key' }} | |
| run: | | |
| bundle exec rails server -p 3333 -d | |
| timeout 60 bash -c 'until curl -sf http://localhost:3333/up; do sleep 2; done' | |
| - name: Run SQL Injection Tests | |
| run: | | |
| chmod +x .pentest/test-sql-injection-quick.sh | |
| .pentest/test-sql-injection-quick.sh | |
| secrets-scan-enhanced: | |
| name: Secrets Scan (Enhanced) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Run Secrets Check | |
| run: | | |
| chmod +x .pentest/test-secrets-quick.sh | |
| .pentest/test-secrets-quick.sh | |
| security-summary: | |
| name: Security Summary | |
| runs-on: ubuntu-latest | |
| needs: | |
| - brakeman | |
| - dependency-check | |
| - semgrep | |
| - ssrf-protection | |
| - authentication-test | |
| - sql-injection-test | |
| - secrets-scan-enhanced | |
| if: always() | |
| steps: | |
| - name: Check Results | |
| run: | | |
| echo "Brakeman: ${{ needs.brakeman.result }}" | |
| echo "Dependency Check: ${{ needs.dependency-check.result }}" | |
| echo "Semgrep: ${{ needs.semgrep.result }}" | |
| echo "SSRF Protection: ${{ needs.ssrf-protection.result }}" | |
| echo "Authentication: ${{ needs.authentication-test.result }}" | |
| echo "SQL Injection: ${{ needs.sql-injection-test.result }}" | |
| echo "Secrets Scan: ${{ needs.secrets-scan-enhanced.result }}" | |
| - name: Write Step Summary | |
| run: | | |
| status() { | |
| case "$1" in | |
| success) echo "✅" ;; | |
| failure) echo "❌" ;; | |
| *) echo "⚠️" ;; | |
| esac | |
| } | |
| cat >> "$GITHUB_STEP_SUMMARY" << EOF | |
| ## 🔐 Security Scan Summary | |
| ### Static Analysis (SAST) | |
| | Check | Status | | |
| |-------|--------| | |
| | Brakeman | $(status "${{ needs.brakeman.result }}") ${{ needs.brakeman.result }} | | |
| | Dependencies | $(status "${{ needs.dependency-check.result }}") ${{ needs.dependency-check.result }} | | |
| | Semgrep | $(status "${{ needs.semgrep.result }}") ${{ needs.semgrep.result }} | | |
| | Secrets | $(status "${{ needs.secrets-scan-enhanced.result }}") ${{ needs.secrets-scan-enhanced.result }} | | |
| ### Dynamic Analysis (DAST) | |
| | Check | Status | | |
| |-------|--------| | |
| | SSRF Protection | $(status "${{ needs.ssrf-protection.result }}") ${{ needs.ssrf-protection.result }} | | |
| | Authentication | $(status "${{ needs.authentication-test.result }}") ${{ needs.authentication-test.result }} | | |
| | SQL Injection | $(status "${{ needs.sql-injection-test.result }}") ${{ needs.sql-injection-test.result }} | | |
| EOF | |
| - name: Comment PR | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6 | |
| with: | |
| script: | | |
| const s = (r) => ({ success: '✅', failure: '❌' }[r] ?? '⚠️'); | |
| const brakeman = '${{ needs.brakeman.result }}'; | |
| const deps = '${{ needs.dependency-check.result }}'; | |
| const semgrep = '${{ needs.semgrep.result }}'; | |
| const ssrf = '${{ needs.ssrf-protection.result }}'; | |
| const auth = '${{ needs.authentication-test.result }}'; | |
| const sqli = '${{ needs.sql-injection-test.result }}'; | |
| const secrets = '${{ needs.secrets-scan-enhanced.result }}'; | |
| const allPassed = [brakeman, deps, semgrep, ssrf, auth, sqli, secrets] | |
| .every(r => r === 'success'); | |
| const body = `## 🔐 Security Scan Summary | |
| ### Static Analysis (SAST) | |
| | Check | Status | | |
| |-------|--------| | |
| | Brakeman | ${s(brakeman)} ${brakeman} | | |
| | Dependencies | ${s(deps)} ${deps} | | |
| | Semgrep | ${s(semgrep)} ${semgrep} | | |
| | Secrets | ${s(secrets)} ${secrets} | | |
| ### Dynamic Analysis (DAST) | |
| | Check | Status | | |
| |-------|--------| | |
| | SSRF Protection | ${s(ssrf)} ${ssrf} | | |
| | Authentication | ${s(auth)} ${auth} | | |
| | SQL Injection | ${s(sqli)} ${sqli} | | |
| ${allPassed ? '✅ All security checks passed!' : '⚠️ Some checks failed — review the details above.'} | |
| `; | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body, | |
| }); |