/ .github / workflows / workflow-linter.yml
workflow-linter.yml
  1  # SPDX-License-Identifier: AGPL-3.0-or-later
  2  # workflow-linter.yml - Validates GitHub workflows against RSR security standards
  3  # This workflow can be copied to other repos for consistent enforcement
  4  name: Workflow Security Linter
  5  
  6  on:
  7    push:
  8      paths:
  9        - '.github/workflows/**'
 10    pull_request:
 11      paths:
 12        - '.github/workflows/**'
 13    workflow_dispatch:
 14  
 15  permissions: read-all
 16  
 17  jobs:
 18    lint-workflows:
 19      runs-on: ubuntu-latest
 20      permissions:
 21        contents: read
 22  
 23      steps:
 24        - name: Checkout
 25          uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
 26  
 27        - name: Check SPDX Headers
 28          run: |
 29            echo "=== Checking SPDX License Headers ==="
 30            failed=0
 31            for file in .github/workflows/*.yml .github/workflows/*.yaml; do
 32              [ -f "$file" ] || continue
 33              if ! head -1 "$file" | grep -q "^# SPDX-License-Identifier:"; then
 34                echo "ERROR: $file missing SPDX header"
 35                failed=1
 36              fi
 37            done
 38            if [ $failed -eq 1 ]; then
 39              echo "Add '# SPDX-License-Identifier: AGPL-3.0-or-later' as first line"
 40              exit 1
 41            fi
 42            echo "All workflows have SPDX headers"
 43  
 44        - name: Check Permissions Declaration
 45          run: |
 46            echo "=== Checking Permissions ==="
 47            failed=0
 48            for file in .github/workflows/*.yml .github/workflows/*.yaml; do
 49              [ -f "$file" ] || continue
 50              if ! grep -q "^permissions:" "$file"; then
 51                echo "ERROR: $file missing top-level 'permissions:' declaration"
 52                failed=1
 53              fi
 54            done
 55            if [ $failed -eq 1 ]; then
 56              echo "Add 'permissions: read-all' at workflow level"
 57              exit 1
 58            fi
 59            echo "All workflows have permissions declared"
 60  
 61        - name: Check SHA-Pinned Actions
 62          run: |
 63            echo "=== Checking Action Pinning ==="
 64            # Find any uses: lines that don't have @SHA format
 65            # Pattern: uses: owner/repo@<40-char-hex>
 66            unpinned=$(grep -rn "uses:" .github/workflows/ | \
 67              grep -v "@[a-f0-9]\{40\}" | \
 68              grep -v "uses: \./\|uses: docker://\|uses: actions/github-script" || true)
 69  
 70            if [ -n "$unpinned" ]; then
 71              echo "ERROR: Found unpinned actions:"
 72              echo "$unpinned"
 73              echo ""
 74              echo "Replace version tags with SHA pins, e.g.:"
 75              echo "  uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4.1.1"
 76              exit 1
 77            fi
 78            echo "All actions are SHA-pinned"
 79  
 80        - name: Check for Duplicate Workflows
 81          run: |
 82            echo "=== Checking for Duplicates ==="
 83            # Known duplicate patterns
 84            if [ -f .github/workflows/codeql.yml ] && [ -f .github/workflows/codeql-analysis.yml ]; then
 85              echo "ERROR: Duplicate CodeQL workflows found"
 86              echo "Delete codeql-analysis.yml (keep codeql.yml)"
 87              exit 1
 88            fi
 89            if [ -f .github/workflows/rust.yml ] && [ -f .github/workflows/rust-ci.yml ]; then
 90              echo "WARNING: Potential duplicate Rust workflows"
 91              echo "Consider consolidating rust.yml and rust-ci.yml"
 92            fi
 93            echo "No critical duplicates found"
 94  
 95        - name: Check CodeQL Language Matrix
 96          run: |
 97            echo "=== Checking CodeQL Configuration ==="
 98            if [ ! -f .github/workflows/codeql.yml ]; then
 99              echo "No CodeQL workflow found (optional)"
100              exit 0
101            fi
102  
103            # Detect repo languages
104            has_js=$(find . -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" -path "*/src/*" -o -path "*/lib/*" 2>/dev/null | head -1)
105            has_py=$(find . -name "*.py" -path "*/src/*" -o -path "*/lib/*" 2>/dev/null | head -1)
106            has_go=$(find . -name "*.go" -path "*/src/*" -o -path "*/cmd/*" -o -path "*/pkg/*" 2>/dev/null | head -1)
107            has_rs=$(find . -name "*.rs" -path "*/src/*" 2>/dev/null | head -1)
108            has_java=$(find . -name "*.java" -path "*/src/*" 2>/dev/null | head -1)
109            has_rb=$(find . -name "*.rb" -path "*/lib/*" -o -path "*/app/*" 2>/dev/null | head -1)
110  
111            echo "Detected languages:"
112            [ -n "$has_js" ] && echo "  - javascript-typescript"
113            [ -n "$has_py" ] && echo "  - python"
114            [ -n "$has_go" ] && echo "  - go"
115            [ -n "$has_rs" ] && echo "  - rust (note: CodeQL rust is limited)"
116            [ -n "$has_java" ] && echo "  - java-kotlin"
117            [ -n "$has_rb" ] && echo "  - ruby"
118  
119            # Check for over-reach
120            if grep -q "language:.*'go'" .github/workflows/codeql.yml && [ -z "$has_go" ]; then
121              echo "WARNING: CodeQL configured for Go but no Go files found"
122            fi
123            if grep -q "language:.*'python'" .github/workflows/codeql.yml && [ -z "$has_py" ]; then
124              echo "WARNING: CodeQL configured for Python but no Python files found"
125            fi
126            if grep -q "language:.*'java'" .github/workflows/codeql.yml && [ -z "$has_java" ]; then
127              echo "WARNING: CodeQL configured for Java but no Java files found"
128            fi
129            if grep -q "language:.*'ruby'" .github/workflows/codeql.yml && [ -z "$has_rb" ]; then
130              echo "WARNING: CodeQL configured for Ruby but no Ruby files found"
131            fi
132  
133            echo "CodeQL check complete"
134  
135        - name: Check Secrets Guards
136          run: |
137            echo "=== Checking Secrets Usage ==="
138            # Look for secrets without conditional guards in mirror workflows
139            if [ -f .github/workflows/mirror.yml ]; then
140              if grep -q "secrets\." .github/workflows/mirror.yml; then
141                if ! grep -q "if:.*vars\." .github/workflows/mirror.yml; then
142                  echo "WARNING: mirror.yml uses secrets without vars guard"
143                  echo "Add 'if: vars.FEATURE_ENABLED == true' to jobs"
144                fi
145              fi
146            fi
147            echo "Secrets check complete"
148  
149        - name: Summary
150          run: |
151            echo ""
152            echo "=== Workflow Linter Summary ==="
153            echo "All critical checks passed."
154            echo ""
155            echo "For more info, see: robot-repo-bot/ERROR-CATALOG.scm"