Skip to content

Commit c90a246

Browse files
committed
chore: add workflow security linter (robot-cleaner)
1 parent 4a2cd50 commit c90a246

File tree

1 file changed

+155
-0
lines changed

1 file changed

+155
-0
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.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@b4ffde65f46336ab88eb53be808477a3936bae11 # 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"

0 commit comments

Comments
 (0)