diff --git a/.github/.env.base b/.github/.env.base index 451d073..3e70e55 100644 --- a/.github/.env.base +++ b/.github/.env.base @@ -81,6 +81,7 @@ ENABLE_BENCHMARKS=true # Run benchmark tests ENABLE_CACHE_WARMING=true # Warm Go module and build caches ENABLE_CODE_COVERAGE=true # Generate coverage reports via go-coverage ENABLE_FUZZ_TESTING=true # Run fuzz tests (Go 1.18+) +ENABLE_GO_TESTS=true # Run Go test suite (unit, integration, matrix) ENABLE_RACE_DETECTION=true # Enable Go race detector ENABLE_STATIC_ANALYSIS=true # Run go vet analysis ENABLE_VERBOSE_TEST_OUTPUT=false # Verbose test output (can slow CI) @@ -231,14 +232,14 @@ REDIS_CACHE_FORCE_PULL=false # Force pull Redis images even when cache # πŸͺ„ MAGE-X CONFIGURATION # ================================================================================================ -MAGE_X_VERSION=v1.7.9 # https://github.com/mrz1836/mage-x/releases +MAGE_X_VERSION=v1.7.12 # https://github.com/mrz1836/mage-x/releases MAGE_X_USE_LOCAL=false # Use local version for development MAGE_X_AUTO_DISCOVER_BUILD_TAGS=true # Enable auto-discovery of build tags MAGE_X_AUTO_DISCOVER_BUILD_TAGS_EXCLUDE=race,custom # Comma-separated list of tags to exclude MAGE_X_FORMAT_EXCLUDE_PATHS=vendor,node_modules,.git,.idea # Format exclusion paths (comma-separated directories to exclude from formatting) MAGE_X_GITLEAKS_VERSION=8.28.0 # https://github.com/gitleaks/gitleaks/releases -MAGE_X_GOFUMPT_VERSION=v0.9.1 # https://github.com/mvdan/gofumpt/releases -MAGE_X_GOLANGCI_LINT_VERSION=v2.6.0 # https://github.com/golangci/golangci-lint/releases +MAGE_X_GOFUMPT_VERSION=v0.9.2 # https://github.com/mvdan/gofumpt/releases +MAGE_X_GOLANGCI_LINT_VERSION=v2.6.1 # https://github.com/golangci/golangci-lint/releases MAGE_X_GORELEASER_VERSION=v2.12.7 # https://github.com/goreleaser/goreleaser/releases MAGE_X_GOVULNCHECK_VERSION=v1.1.4 # https://pkg.go.dev/golang.org/x/vuln MAGE_X_GO_SECONDARY_VERSION=1.24.x # Secondary Go version for MAGE-X (also our secondary) @@ -247,7 +248,7 @@ MAGE_X_MOCKGEN_VERSION=v0.6.0 # https://github.c MAGE_X_NANCY_VERSION=v1.0.52 # https://github.com/sonatype-nexus-community/nancy/releases MAGE_X_STATICCHECK_VERSION=2025.1.1 # https://github.com/dominikh/go-tools/releases MAGE_X_SWAG_VERSION=v1.16.6 # https://github.com/swaggo/swag/releases -MAGE_X_YAMLFMT_VERSION=v0.17.2 # https://github.com/google/yamlfmt/releases +MAGE_X_YAMLFMT_VERSION=v0.20.0 # https://github.com/google/yamlfmt/releases # Runtime variables (set by setup-goreleaser action): # MAGE_X_GORELEASER_PATH - Path to installed goreleaser binary @@ -318,8 +319,8 @@ GO_PRE_COMMIT_MAX_FILES_OPEN=100 GO_PRE_COMMIT_ALL_FILES=true # Tool Versions -GO_PRE_COMMIT_GOLANGCI_LINT_VERSION=v2.6.0 # https://github.com/golangci/golangci-lint -GO_PRE_COMMIT_FUMPT_VERSION=v0.9.1 # https://github.com/mvdan/gofumpt +GO_PRE_COMMIT_GOLANGCI_LINT_VERSION=v2.6.1 # https://github.com/golangci/golangci-lint/releases +GO_PRE_COMMIT_FUMPT_VERSION=v0.9.2 # https://github.com/mvdan/gofumpt/releases GO_PRE_COMMIT_GOIMPORTS_VERSION=latest # https://github.com/golang/tools # Build tags for golangci-lint and other tools diff --git a/.github/workflows/fortress-code-quality.yml b/.github/workflows/fortress-code-quality.yml index 9ad730d..37ace40 100644 --- a/.github/workflows/fortress-code-quality.yml +++ b/.github/workflows/fortress-code-quality.yml @@ -206,6 +206,7 @@ jobs: # ---------------------------------------------------------------------------------- lint: name: ✨ Lint Code + timeout-minutes: 20 if: ${{ inputs.go-lint-enabled == 'true' }} runs-on: ${{ inputs.primary-runner }} outputs: diff --git a/.github/workflows/fortress-completion-finalize.yml b/.github/workflows/fortress-completion-finalize.yml index a85d71c..b857304 100644 --- a/.github/workflows/fortress-completion-finalize.yml +++ b/.github/workflows/fortress-completion-finalize.yml @@ -186,7 +186,7 @@ jobs: echo "| πŸͺ Pre-commit Checks | ${{ env.INPUT_pre-commit-result }} | $([ "${{ env.INPUT_pre-commit-result }}" = "success" ] && echo "βœ…" || echo "❌") |" echo "| πŸ”’ Security Scans | ${{ env.INPUT_security-result }} | $([ "${{ env.INPUT_security-result }}" = "success" ] && echo "βœ…" || echo "❌") |" echo "| πŸ“Š Code Quality | ${{ env.INPUT_code-quality-result }} | $([ "${{ env.INPUT_code-quality-result }}" = "success" ] && echo "βœ…" || echo "❌") |" - echo "| πŸ§ͺ Test Suite | ${{ env.INPUT_test-suite-result }} | $([ "${{ env.INPUT_test-suite-result }}" = "success" ] && echo "βœ…" || echo "❌") |" + echo "| πŸ§ͺ Test Suite | ${{ env.INPUT_test-suite-result }} | $([ "${{ env.INPUT_test-suite-result }}" = "success" ] && echo "βœ…" || ([ "${{ env.INPUT_test-suite-result }}" = "skipped" ] && echo "⏭️" || echo "❌")) |" } >> final-report.md # Only show benchmarks row if it was attempted diff --git a/.github/workflows/fortress-completion-statistics.yml b/.github/workflows/fortress-completion-statistics.yml index c6c2559..d48801a 100644 --- a/.github/workflows/fortress-completion-statistics.yml +++ b/.github/workflows/fortress-completion-statistics.yml @@ -100,7 +100,7 @@ jobs: # Download specific artifacts needed for statistics processing # -------------------------------------------------------------------- - name: πŸ“₯ Download test statistics - if: always() + if: always() && env.ENABLE_GO_TESTS == 'true' uses: ./.github/actions/download-artifact-resilient with: pattern: "test-stats-*" @@ -136,7 +136,7 @@ jobs: continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} - name: πŸ“₯ Download internal coverage statistics - if: always() && env.GO_COVERAGE_PROVIDER == 'internal' + if: always() && env.ENABLE_GO_TESTS == 'true' && env.GO_COVERAGE_PROVIDER == 'internal' uses: ./.github/actions/download-artifact-resilient with: pattern: "coverage-stats-internal" @@ -148,7 +148,7 @@ jobs: continue-on-error: true - name: πŸ“₯ Download codecov coverage statistics - if: always() && env.GO_COVERAGE_PROVIDER == 'codecov' + if: always() && env.ENABLE_GO_TESTS == 'true' && env.GO_COVERAGE_PROVIDER == 'codecov' uses: ./.github/actions/download-artifact-resilient with: pattern: "coverage-stats-codecov" diff --git a/.github/workflows/fortress-completion-tests.yml b/.github/workflows/fortress-completion-tests.yml index 4a5c7d1..e9ce49d 100644 --- a/.github/workflows/fortress-completion-tests.yml +++ b/.github/workflows/fortress-completion-tests.yml @@ -78,7 +78,7 @@ jobs: # Download specific artifacts needed for test analysis # -------------------------------------------------------------------- - name: πŸ“₯ Download test statistics - if: always() + if: always() && env.ENABLE_GO_TESTS == 'true' uses: ./.github/actions/download-artifact-resilient with: pattern: "test-stats-*" @@ -114,7 +114,7 @@ jobs: continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} - name: πŸ“₯ Download test failure artifacts - if: always() + if: always() && env.ENABLE_GO_TESTS == 'true' uses: ./.github/actions/download-artifact-resilient with: pattern: "test-results-unit-*" @@ -126,7 +126,7 @@ jobs: continue-on-error: ${{ env.ARTIFACT_DOWNLOAD_CONTINUE_ON_ERROR }} - name: πŸ“₯ Download fuzz test failure artifacts - if: always() + if: always() && env.ENABLE_GO_TESTS == 'true' && env.ENABLE_FUZZ_TESTING == 'true' uses: ./.github/actions/download-artifact-resilient with: pattern: "test-results-fuzz-*" @@ -333,7 +333,7 @@ jobs: echo "failure-metrics={\"total_failures\":$TOTAL_FAILURES,\"has_error_output\":$HAS_ERROR_OUTPUT}" >> $GITHUB_OUTPUT fi else - # No test statistics available - likely fork PR with skipped test suite + # No test statistics available - check if tests were disabled or fork PR { echo "" echo "" @@ -341,11 +341,17 @@ jobs: echo "" echo "| Status | Details |" echo "|--------|---------|" - echo "| **Test Suite** | ⚠️ Skipped - No test statistics available |" - echo "| **Reason** | Tests may have been skipped for fork PR security restrictions |" - echo "| **Note** | Repository maintainers can run full tests on merged code |" - echo "" - echo "_For security reasons, fork PRs do not have access to test execution secrets._" + if [[ "${{ env.ENABLE_GO_TESTS }}" == "false" ]]; then + echo "| **Test Suite** | ❌ Disabled - Set ENABLE_GO_TESTS=true to enable |" + echo "| **Reason** | Tests are disabled via configuration flag |" + echo "| **Note** | Enable ENABLE_GO_TESTS in .env.custom or .env.base to run tests |" + else + echo "| **Test Suite** | ⚠️ Skipped - No test statistics available |" + echo "| **Reason** | Tests may have been skipped for fork PR security restrictions |" + echo "| **Note** | Repository maintainers can run full tests on merged code |" + echo "" + echo "_For security reasons, fork PRs do not have access to test execution secrets._" + fi } >> tests-section.md fi diff --git a/.github/workflows/fortress-setup-config.yml b/.github/workflows/fortress-setup-config.yml index 53b713b..cc4ac8e 100644 --- a/.github/workflows/fortress-setup-config.yml +++ b/.github/workflows/fortress-setup-config.yml @@ -68,6 +68,9 @@ on: fuzz-testing-enabled: description: "Whether fuzz testing is enabled" value: ${{ jobs.setup-config.outputs.fuzz-testing-enabled }} + go-tests-enabled: + description: "Whether Go tests are enabled" + value: ${{ jobs.setup-config.outputs.go-tests-enabled }} go-primary-version: description: "Primary Go version" value: ${{ jobs.setup-config.outputs.go-primary-version }} @@ -190,6 +193,7 @@ jobs: coverage-provider: ${{ steps.config.outputs.coverage-provider }} cache-warming-enabled: ${{ steps.config.outputs.cache-warming-enabled }} fuzz-testing-enabled: ${{ steps.config.outputs.fuzz-testing-enabled }} + go-tests-enabled: ${{ steps.config.outputs.go-tests-enabled }} go-primary-version: ${{ steps.config.outputs.go-primary-version }} go-secondary-version: ${{ steps.config.outputs.go-secondary-version }} go-sum-file: ${{ steps.config.outputs.go-sum-file }} @@ -510,6 +514,7 @@ jobs: echo "gitleaks-enabled=${{ env.ENABLE_SECURITY_SCAN_GITLEAKS }}" >> $GITHUB_OUTPUT echo "static-analysis-enabled=${{ env.ENABLE_STATIC_ANALYSIS }}" >> $GITHUB_OUTPUT echo "fuzz-testing-enabled=${{ env.ENABLE_FUZZ_TESTING }}" >> $GITHUB_OUTPUT + echo "go-tests-enabled=${{ env.ENABLE_GO_TESTS }}" >> $GITHUB_OUTPUT echo "pre-commit-enabled=${{ env.ENABLE_GO_PRE_COMMIT }}" >> $GITHUB_OUTPUT # Detect if this is a release run @@ -680,6 +685,7 @@ jobs: echo "| **Cache Warming** | $([ "${{ env.ENABLE_CACHE_WARMING }}" == "true" ] && echo "βœ… Enabled" || echo "❌ Disabled") | Go module and build caches will $([ "${{ env.ENABLE_CACHE_WARMING }}" == "true" ] && echo "be pre-warmed for faster test execution" || echo "not be pre-warmed (saves memory)") |" >> $GITHUB_STEP_SUMMARY echo "| **Code Coverage** | $([ "${{ env.ENABLE_CODE_COVERAGE }}" == "true" ] && echo "βœ… Enabled" || echo "❌ Disabled") | Coverage will $([ "${{ env.ENABLE_CODE_COVERAGE }}" == "true" ] && echo "use $([ "${{ env.GO_COVERAGE_PROVIDER }}" == "codecov" ] && echo "**Codecov**" || echo "**go-coverage**") (${{ env.GO_COVERAGE_THRESHOLD }}% threshold)" || echo "be skipped") |" >> $GITHUB_STEP_SUMMARY echo "| **Fuzz Testing** | $([ "${{ env.ENABLE_FUZZ_TESTING }}" == "true" ] && echo "βœ… Enabled" || echo "❌ Disabled") | Fuzz tests will $([ "${{ env.ENABLE_FUZZ_TESTING }}" == "true" ] && echo "run in parallel job on Linux with primary Go version" || echo "be skipped") |" >> $GITHUB_STEP_SUMMARY + echo "| **Go Tests** | $([ "${{ env.ENABLE_GO_TESTS }}" == "true" ] && echo "βœ… Enabled" || echo "❌ Disabled") | Test suite will $([ "${{ env.ENABLE_GO_TESTS }}" == "true" ] && echo "run across matrix configurations" || echo "be skipped") |" >> $GITHUB_STEP_SUMMARY echo "| **Gitleaks (Secret Scan)** | $([ "${{ env.ENABLE_SECURITY_SCAN_GITLEAKS }}" == "true" ] && echo "βœ… Enabled" || echo "❌ Disabled") | Gitleaks will $([ "${{ env.ENABLE_SECURITY_SCAN_GITLEAKS }}" == "true" ] && echo "scan for leaked secrets" || echo "be skipped") |" >> $GITHUB_STEP_SUMMARY echo "| **Go Linting** | $([ "${{ env.ENABLE_GO_LINT }}" == "true" ] && echo "βœ… Enabled" || echo "❌ Disabled") | golangci-lint via MAGE-X will $([ "${{ env.ENABLE_GO_LINT }}" == "true" ] && echo "analyze code quality" || echo "be skipped") |" >> $GITHUB_STEP_SUMMARY echo "| **Govulncheck** | $([ "${{ env.ENABLE_SECURITY_SCAN_GOVULNCHECK }}" == "true" ] && echo "βœ… Enabled" || echo "❌ Disabled") | govulncheck via MAGE-X will $([ "${{ env.ENABLE_SECURITY_SCAN_GOVULNCHECK }}" == "true" ] && echo "scan for Go vulnerabilities" || echo "be skipped") |" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/fortress-test-suite.yml b/.github/workflows/fortress-test-suite.yml index 1d3cf98..b4cfe89 100644 --- a/.github/workflows/fortress-test-suite.yml +++ b/.github/workflows/fortress-test-suite.yml @@ -56,6 +56,10 @@ on: description: "Whether fuzz testing is enabled" required: true type: string + go-tests-enabled: + description: "Whether Go tests are enabled" + required: true + type: string redis-enabled: description: "Whether Redis service is enabled" required: false @@ -119,6 +123,7 @@ jobs: # ---------------------------------------------------------------------------------- execute-test-matrix: name: πŸ§ͺ Execute Test Matrix + if: inputs.go-tests-enabled == 'true' uses: ./.github/workflows/fortress-test-matrix.yml with: env-json: ${{ inputs.env-json }} @@ -143,6 +148,7 @@ jobs: # ---------------------------------------------------------------------------------- execute-fuzz-tests: name: 🎯 Execute Fuzz Tests + if: inputs.go-tests-enabled == 'true' && inputs.fuzz-testing-enabled == 'true' uses: ./.github/workflows/fortress-test-fuzz.yml with: env-json: ${{ inputs.env-json }} @@ -158,7 +164,7 @@ jobs: validate-test-results: name: πŸ” Validate Test Results needs: [execute-test-matrix, execute-fuzz-tests] - if: always() # Always run to validate results even if tests failed + if: always() && inputs.go-tests-enabled == 'true' # Always run to validate results even if tests failed uses: ./.github/workflows/fortress-test-validation.yml with: env-json: ${{ inputs.env-json }} @@ -171,7 +177,7 @@ jobs: process-coverage: name: πŸ“Š Process Coverage needs: [execute-test-matrix, validate-test-results] - if: inputs.code-coverage-enabled == 'true' && !startsWith(github.ref, 'refs/tags/') + if: inputs.go-tests-enabled == 'true' && inputs.code-coverage-enabled == 'true' && !startsWith(github.ref, 'refs/tags/') permissions: contents: write # Write repository content and push to gh-pages branch for coverage processing pull-requests: write # Required: Coverage workflow needs to create PR comments diff --git a/.github/workflows/fortress.yml b/.github/workflows/fortress.yml index 542b179..97f004a 100644 --- a/.github/workflows/fortress.yml +++ b/.github/workflows/fortress.yml @@ -241,7 +241,8 @@ jobs: needs.setup.result == 'success' && needs.test-magex.result == 'success' && (needs.warm-cache.result == 'success' || needs.warm-cache.result == 'skipped') && - needs.setup.outputs.is-fork-pr != 'true' + needs.setup.outputs.is-fork-pr != 'true' && + needs.setup.outputs.go-tests-enabled == 'true' permissions: contents: write # Write repository content and push to gh-pages branch for test execution pull-requests: write # Required: Coverage workflow needs to create PR comments @@ -255,6 +256,7 @@ jobs: coverage-provider: ${{ needs.setup.outputs.coverage-provider }} env-json: ${{ needs.load-env.outputs.env-json }} fuzz-testing-enabled: ${{ needs.setup.outputs.fuzz-testing-enabled }} + go-tests-enabled: ${{ needs.setup.outputs.go-tests-enabled }} go-primary-version: ${{ needs.setup.outputs.go-primary-version }} go-secondary-version: ${{ needs.setup.outputs.go-secondary-version }} primary-runner: ${{ needs.setup.outputs.primary-runner }} @@ -332,7 +334,7 @@ jobs: echo "| πŸ”’ Security | ${{ needs.security.result }} | Required |" echo "| πŸ“Š Code Quality | ${{ needs.code-quality.result }} | Required |" echo "| πŸͺ Pre-commit | ${{ needs.pre-commit.result }} | ${{ needs.setup.outputs.pre-commit-enabled == 'true' && 'Required' || 'Skipped' }} |" - echo "| πŸ§ͺ Test Suite | ${{ needs.test-suite.result }} | Required |" + echo "| πŸ§ͺ Test Suite | ${{ needs.test-suite.result }} | ${{ needs.setup.outputs.go-tests-enabled == 'true' && 'Required' || 'Skipped' }} |" echo "| πŸƒ Benchmarks | ${{ needs.benchmarks.result }} | Optional ⚠️ |" echo "" if [[ "${{ needs.benchmarks.result }}" == "failure" ]]; then @@ -376,9 +378,12 @@ jobs: FAILED=true fi - if [[ "${{ needs.test-suite.result }}" == "failure" || "${{ needs.test-suite.result }}" == "cancelled" ]]; then - echo "❌ Test suite failed or was cancelled" >&2 - FAILED=true + # Only check test-suite if it was enabled + if [[ "${{ needs.setup.outputs.go-tests-enabled }}" == "true" ]]; then + if [[ "${{ needs.test-suite.result }}" == "failure" || "${{ needs.test-suite.result }}" == "cancelled" ]]; then + echo "❌ Test suite failed or was cancelled" >&2 + FAILED=true + fi fi # Check benchmarks (currently optional - just warn if they fail) diff --git a/.github/workflows/pull-request-management-fork.yml b/.github/workflows/pull-request-management-fork.yml index fc3d24b..5578d1b 100644 --- a/.github/workflows/pull-request-management-fork.yml +++ b/.github/workflows/pull-request-management-fork.yml @@ -18,12 +18,71 @@ # # Maintainer: @mrz1836 # -# SECURITY MODEL: -# - Uses pull_request_target trigger for write permissions (required for labels/comments) -# - CRITICAL: Only checks out BASE branch code, NEVER PR head (prevents malicious code execution) -# - Fork detection uses full_name comparison for accuracy (not owner.login which fails for org members) -# - All code execution happens from trusted base repository -# - No secrets exposed to fork PRs (GITHUB_TOKEN only) +# ════════════════════════════════════════════════════════════════════════════════ +# πŸ”’ SECURITY MODEL - Two-Workflow Pattern for Safe Fork PR Handling +# ════════════════════════════════════════════════════════════════════════════════ +# +# This workflow implements the RECOMMENDED security pattern for handling fork PRs +# as documented in GitHub Security Best Practices (githubactions:S7631). +# +# β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +# β”‚ WHY pull_request_target IS SAFE HERE: β”‚ +# β”‚ β”‚ +# β”‚ βœ… Uses pull_request_target trigger for write permissions β”‚ +# β”‚ (Required for: labels, comments, assignees) β”‚ +# β”‚ β”‚ +# β”‚ βœ… CRITICAL: Only checks out BASE branch code, NEVER PR head β”‚ +# β”‚ (Prevents malicious code execution from untrusted forks) β”‚ +# β”‚ β”‚ +# β”‚ βœ… Fork detection uses full_name comparison for accuracy β”‚ +# β”‚ (Not owner.login which fails for org members) β”‚ +# β”‚ β”‚ +# β”‚ βœ… All code execution happens from trusted base repository β”‚ +# β”‚ (No code from PR is ever executed) β”‚ +# β”‚ β”‚ +# β”‚ βœ… No secrets exposed to fork PRs (GITHUB_TOKEN only) β”‚ +# β”‚ (No custom secrets accessible to malicious actors) β”‚ +# β”‚ β”‚ +# β”‚ βœ… Sparse checkout minimizes attack surface β”‚ +# β”‚ (Only config files checked out, no executable code) β”‚ +# β”‚ β”‚ +# β”‚ βœ… Least-privilege permissions model β”‚ +# β”‚ (Jobs get elevated permissions only where absolutely needed) β”‚ +# β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +# +# SECURITY PATTERN: Two-Workflow Approach +# β”œβ”€ pull-request-management.yml β†’ Same-repo PRs (uses pull_request) +# └─ pull-request-management-fork.yml β†’ Fork PRs (uses pull_request_target) +# +# WHAT COULD GO WRONG (and how we prevent it): +# ❌ Malicious fork creates PR with code that steals secrets +# βœ… PREVENTED: We never checkout or execute PR code +# +# ❌ Attacker modifies workflow files in their fork +# βœ… PREVENTED: pull_request_target runs base repo workflow only +# +# ❌ Malicious code in PR tries to access repository secrets +# βœ… PREVENTED: Only GITHUB_TOKEN exposed, no custom secrets +# +# ❌ Code injection via PR title/description into workflow +# βœ… PREVENTED: All user input properly escaped in GitHub Actions +# +# SECURITY SCANNERS: +# - GitHub Security: May flag pull_request_target + checkout (FALSE POSITIVE) +# - Semgrep: May flag dangerous-checkout pattern (FALSE POSITIVE) +# - Checkov: May flag CKV_GHA_3 (FALSE POSITIVE) +# +# These are FALSE POSITIVES because: +# 1. We explicitly checkout base branch, not PR head +# 2. This is the RECOMMENDED pattern per GitHub docs +# 3. All security controls are properly implemented +# +# REFERENCES: +# - GitHub Security Advisory: githubactions:S7631 +# - GitHub Docs: Keeping your GitHub Actions and workflows secure +# - OWASP: CI/CD Security Top 10 +# +# ════════════════════════════════════════════════════════════════════════════════ # # ------------------------------------------------------------------------------------ @@ -65,13 +124,42 @@ jobs: outputs: env-json: ${{ steps.load-env.outputs.env-json }} steps: + # β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + # β”‚ SECURITY SCANNERS: This checkout is SAFE despite pull_request_targetβ”‚ + # β”‚ β”‚ + # β”‚ Justification: β”‚ + # β”‚ - Only checks out TRUSTED base branch (ref: github.base_ref) β”‚ + # β”‚ - NEVER checks out PR head code from untrusted fork β”‚ + # β”‚ - Implements recommended two-workflow security pattern β”‚ + # β”‚ - Uses sparse checkout (minimal attack surface) β”‚ + # β”‚ - No executable code from PR is ever run β”‚ + # β”‚ β”‚ + # β”‚ Pattern: Two-workflow security model (see SECURITY.md) β”‚ + # β”‚ References: githubactions:S7631, semgrep:github-actions-checkout β”‚ + # β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + # semgrep:ignore github-actions-dangerous-checkout + # codeql:ignore GH001 + # checkov:skip=CKV_GHA_3:Base branch checkout is intentional and safe - name: πŸ“₯ Checkout base repo (sparse) uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: - # CRITICAL SECURITY: Always checkout base branch (not PR head) - # This prevents malicious code execution from fork PRs - # pull_request_target runs with write permissions, so we MUST NOT - # execute any code from the untrusted PR + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # πŸ”’ CRITICAL SECURITY CONTROL: Base Branch Checkout Only + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # This workflow uses pull_request_target for write permissions BUT + # ONLY checks out the trusted base branch code - NEVER PR head code. + # + # WHY THIS IS SAFE: + # - ref parameter explicitly set to base branch (github.base_ref) + # - Malicious fork PRs cannot inject code into this workflow + # - All code execution happens from trusted repository only + # - Sparse checkout limits to config files only (no executables) + # + # SECURITY MODEL: + # - pull_request_target = write permissions needed for labels/comments + # - Base branch checkout = prevents malicious code execution + # - This is the RECOMMENDED pattern for fork PR automation + # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ref: ${{ github.base_ref }} fetch-depth: 1 sparse-checkout: |