Skip to content

Commit d3ddedd

Browse files
Add secure defaults checklist and CI linting (#10)
- Add SECURE_DEFAULTS.md with comprehensive security checklist covering: - PHP configuration best practices - Input validation patterns with php-aegis Validator - Output sanitization with php-aegis Sanitizer - HTTP security headers configuration - Authentication, database, file, and crypto guidelines - CI/CD and dependency management practices - Add phpstan.neon with maximum strictness (level 9) - Enable all strict type checks - Check for implicit mixed types - Report maybes in method signatures - Add .php-cs-fixer.dist.php with PSR-12 and security rules - Enforce strict_types declaration - Enable strict comparisons - Warn on eval() usage - Migrate to secure random functions - Add php-lint.yml CI workflow with comprehensive checks: - PHP syntax validation - PHP-CS-Fixer code style enforcement - PHPStan level 9 static analysis - SPDX license header verification - strict_types declaration check - Security pattern detection (dangerous functions, weak crypto) - Composer dependency audit - PHP 8.1-8.4 compatibility matrix - Update .gitignore for PHP tooling cache files Co-authored-by: Claude <noreply@anthropic.com>
1 parent 165b8d3 commit d3ddedd

5 files changed

Lines changed: 1133 additions & 0 deletions

File tree

.github/workflows/php-lint.yml

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
# SPDX-License-Identifier: AGPL-3.0-or-later
2+
name: PHP Lint & Analysis
3+
4+
on:
5+
push:
6+
branches: [main, master]
7+
paths:
8+
- 'src/**'
9+
- 'tests/**'
10+
- 'composer.json'
11+
- 'composer.lock'
12+
- 'phpstan.neon'
13+
- '.php-cs-fixer.dist.php'
14+
- '.github/workflows/php-lint.yml'
15+
pull_request:
16+
paths:
17+
- 'src/**'
18+
- 'tests/**'
19+
- 'composer.json'
20+
- 'composer.lock'
21+
- 'phpstan.neon'
22+
- '.php-cs-fixer.dist.php'
23+
- '.github/workflows/php-lint.yml'
24+
25+
permissions: read-all
26+
27+
env:
28+
PHP_VERSION: '8.3'
29+
30+
jobs:
31+
# ============================================================
32+
# Syntax Check - Fast fail on parse errors
33+
# ============================================================
34+
syntax:
35+
name: PHP Syntax Check
36+
runs-on: ubuntu-latest
37+
permissions:
38+
contents: read
39+
steps:
40+
- name: Checkout
41+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
42+
43+
- name: Setup PHP
44+
uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # v2.32.0
45+
with:
46+
php-version: ${{ env.PHP_VERSION }}
47+
tools: none
48+
coverage: none
49+
50+
- name: Check PHP syntax
51+
run: |
52+
echo "Checking PHP syntax..."
53+
find src -name "*.php" -print0 | xargs -0 -n1 php -l
54+
echo "✅ All PHP files have valid syntax"
55+
56+
# ============================================================
57+
# Code Style - PSR-12 compliance via PHP-CS-Fixer
58+
# ============================================================
59+
code-style:
60+
name: Code Style (PHP-CS-Fixer)
61+
runs-on: ubuntu-latest
62+
needs: syntax
63+
permissions:
64+
contents: read
65+
steps:
66+
- name: Checkout
67+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
68+
69+
- name: Setup PHP
70+
uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # v2.32.0
71+
with:
72+
php-version: ${{ env.PHP_VERSION }}
73+
tools: php-cs-fixer:3
74+
coverage: none
75+
76+
- name: Run PHP-CS-Fixer
77+
run: |
78+
php-cs-fixer fix --dry-run --diff --verbose --config=.php-cs-fixer.dist.php
79+
80+
# ============================================================
81+
# Static Analysis - PHPStan at maximum strictness
82+
# ============================================================
83+
phpstan:
84+
name: Static Analysis (PHPStan Level 9)
85+
runs-on: ubuntu-latest
86+
needs: syntax
87+
permissions:
88+
contents: read
89+
steps:
90+
- name: Checkout
91+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
92+
93+
- name: Setup PHP
94+
uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # v2.32.0
95+
with:
96+
php-version: ${{ env.PHP_VERSION }}
97+
tools: phpstan:1
98+
coverage: none
99+
100+
- name: Install Composer dependencies
101+
run: composer install --no-progress --prefer-dist --no-interaction
102+
103+
- name: Run PHPStan
104+
run: |
105+
phpstan analyse --configuration=phpstan.neon --error-format=github
106+
107+
# ============================================================
108+
# SPDX License Headers - Ensure all files have headers
109+
# ============================================================
110+
license-headers:
111+
name: SPDX License Headers
112+
runs-on: ubuntu-latest
113+
permissions:
114+
contents: read
115+
steps:
116+
- name: Checkout
117+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
118+
119+
- name: Check SPDX headers
120+
run: |
121+
MISSING=""
122+
for file in $(find src -name "*.php"); do
123+
if ! grep -q "SPDX-License-Identifier" "$file"; then
124+
MISSING="$MISSING\n - $file"
125+
fi
126+
done
127+
128+
if [ -n "$MISSING" ]; then
129+
echo "::error::Missing SPDX-License-Identifier in:$MISSING"
130+
exit 1
131+
fi
132+
echo "✅ All PHP files have SPDX license headers"
133+
134+
# ============================================================
135+
# Strict Types - Ensure all files use strict_types
136+
# ============================================================
137+
strict-types:
138+
name: Strict Types Declaration
139+
runs-on: ubuntu-latest
140+
permissions:
141+
contents: read
142+
steps:
143+
- name: Checkout
144+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
145+
146+
- name: Check strict_types
147+
run: |
148+
MISSING=""
149+
for file in $(find src -name "*.php"); do
150+
if ! grep -q "declare(strict_types=1)" "$file"; then
151+
MISSING="$MISSING\n - $file"
152+
fi
153+
done
154+
155+
if [ -n "$MISSING" ]; then
156+
echo "::error::Missing declare(strict_types=1) in:$MISSING"
157+
exit 1
158+
fi
159+
echo "✅ All PHP files declare strict_types=1"
160+
161+
# ============================================================
162+
# Security Patterns - Check for dangerous code patterns
163+
# ============================================================
164+
security-patterns:
165+
name: Security Pattern Check
166+
runs-on: ubuntu-latest
167+
permissions:
168+
contents: read
169+
steps:
170+
- name: Checkout
171+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
172+
173+
- name: Check dangerous functions
174+
run: |
175+
FOUND=0
176+
177+
# Dangerous execution functions
178+
DANGEROUS=$(grep -rEn 'eval\s*\(|exec\s*\(|system\s*\(|passthru\s*\(|shell_exec\s*\(|proc_open\s*\(|popen\s*\(' \
179+
--include="*.php" src/ 2>/dev/null || true)
180+
if [ -n "$DANGEROUS" ]; then
181+
echo "::error::Dangerous execution functions found:"
182+
echo "$DANGEROUS"
183+
FOUND=1
184+
fi
185+
186+
# Backtick execution
187+
BACKTICKS=$(grep -rEn '`[^`]*\$' --include="*.php" src/ 2>/dev/null || true)
188+
if [ -n "$BACKTICKS" ]; then
189+
echo "::error::Backtick execution with variables found:"
190+
echo "$BACKTICKS"
191+
FOUND=1
192+
fi
193+
194+
# preg_replace with /e modifier (deprecated but check anyway)
195+
PREG_E=$(grep -rEn "preg_replace\s*\([^)]*'/[^']*e[^']*'" --include="*.php" src/ 2>/dev/null || true)
196+
if [ -n "$PREG_E" ]; then
197+
echo "::error::preg_replace with /e modifier found:"
198+
echo "$PREG_E"
199+
FOUND=1
200+
fi
201+
202+
# assert() with string (can execute code)
203+
ASSERT_STR=$(grep -rEn "assert\s*\(['\"]" --include="*.php" src/ 2>/dev/null || true)
204+
if [ -n "$ASSERT_STR" ]; then
205+
echo "::error::assert() with string argument found:"
206+
echo "$ASSERT_STR"
207+
FOUND=1
208+
fi
209+
210+
# create_function (deprecated, can execute code)
211+
CREATE_FUNC=$(grep -rEn 'create_function\s*\(' --include="*.php" src/ 2>/dev/null || true)
212+
if [ -n "$CREATE_FUNC" ]; then
213+
echo "::error::create_function() found (use closures instead):"
214+
echo "$CREATE_FUNC"
215+
FOUND=1
216+
fi
217+
218+
if [ "$FOUND" -eq 0 ]; then
219+
echo "✅ No dangerous function patterns found"
220+
else
221+
exit 1
222+
fi
223+
224+
- name: Check weak cryptography
225+
run: |
226+
FOUND=0
227+
228+
# MD5 for security (allow md5_file for checksums)
229+
MD5=$(grep -rEn 'md5\s*\(' --include="*.php" src/ 2>/dev/null | grep -v 'md5_file' || true)
230+
if [ -n "$MD5" ]; then
231+
echo "::warning::MD5 usage found (ensure not used for security):"
232+
echo "$MD5"
233+
fi
234+
235+
# SHA1 for security
236+
SHA1=$(grep -rEn 'sha1\s*\(' --include="*.php" src/ 2>/dev/null || true)
237+
if [ -n "$SHA1" ]; then
238+
echo "::warning::SHA1 usage found (ensure not used for security):"
239+
echo "$SHA1"
240+
fi
241+
242+
# Insecure random functions
243+
RAND=$(grep -rEn '\brand\s*\(|\bmt_rand\s*\(|\buniqid\s*\(' --include="*.php" src/ 2>/dev/null || true)
244+
if [ -n "$RAND" ]; then
245+
echo "::warning::Potentially insecure random functions found (use random_int/random_bytes):"
246+
echo "$RAND"
247+
fi
248+
249+
echo "✅ Weak cryptography check completed"
250+
251+
- name: Check SQL injection patterns
252+
run: |
253+
# Direct variable interpolation in queries
254+
SQLI=$(grep -rEn '(mysql_query|mysqli_query|pg_query|->query)\s*\([^)]*\$' \
255+
--include="*.php" src/ 2>/dev/null || true)
256+
if [ -n "$SQLI" ]; then
257+
echo "::warning::Potential SQL injection pattern (use prepared statements):"
258+
echo "$SQLI"
259+
fi
260+
echo "✅ SQL injection pattern check completed"
261+
262+
# ============================================================
263+
# Composer Audit - Check for dependency vulnerabilities
264+
# ============================================================
265+
composer-audit:
266+
name: Dependency Audit
267+
runs-on: ubuntu-latest
268+
permissions:
269+
contents: read
270+
steps:
271+
- name: Checkout
272+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
273+
274+
- name: Setup PHP
275+
uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # v2.32.0
276+
with:
277+
php-version: ${{ env.PHP_VERSION }}
278+
tools: composer:2
279+
coverage: none
280+
281+
- name: Install dependencies
282+
run: composer install --no-progress --prefer-dist --no-interaction
283+
284+
- name: Run Composer audit
285+
run: |
286+
composer audit --format=plain || echo "::warning::Vulnerabilities found in dependencies"
287+
288+
# ============================================================
289+
# Multi-version PHP Test - Ensure compatibility
290+
# ============================================================
291+
php-compat:
292+
name: PHP ${{ matrix.php }} Compatibility
293+
runs-on: ubuntu-latest
294+
needs: [syntax, code-style, phpstan]
295+
permissions:
296+
contents: read
297+
strategy:
298+
fail-fast: false
299+
matrix:
300+
php: ['8.1', '8.2', '8.3', '8.4']
301+
steps:
302+
- name: Checkout
303+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
304+
305+
- name: Setup PHP ${{ matrix.php }}
306+
uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # v2.32.0
307+
with:
308+
php-version: ${{ matrix.php }}
309+
coverage: none
310+
311+
- name: Install dependencies
312+
run: composer install --no-progress --prefer-dist --no-interaction
313+
314+
- name: Check syntax on PHP ${{ matrix.php }}
315+
run: find src -name "*.php" -print0 | xargs -0 -n1 php -l
316+
317+
- name: Run PHPStan on PHP ${{ matrix.php }}
318+
run: vendor/bin/phpstan analyse --configuration=phpstan.neon --no-progress
319+
continue-on-error: ${{ matrix.php == '8.4' }} # Allow failures on newest PHP
320+
321+
# ============================================================
322+
# Summary - Aggregate all check results
323+
# ============================================================
324+
lint-summary:
325+
name: Lint Summary
326+
runs-on: ubuntu-latest
327+
needs: [syntax, code-style, phpstan, license-headers, strict-types, security-patterns, composer-audit, php-compat]
328+
if: always()
329+
permissions:
330+
contents: read
331+
steps:
332+
- name: Check results
333+
run: |
334+
echo "## PHP Lint & Analysis Summary" >> $GITHUB_STEP_SUMMARY
335+
echo "" >> $GITHUB_STEP_SUMMARY
336+
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
337+
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
338+
echo "| Syntax | ${{ needs.syntax.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
339+
echo "| Code Style | ${{ needs.code-style.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
340+
echo "| PHPStan | ${{ needs.phpstan.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
341+
echo "| License Headers | ${{ needs.license-headers.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
342+
echo "| Strict Types | ${{ needs.strict-types.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
343+
echo "| Security Patterns | ${{ needs.security-patterns.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
344+
echo "| Composer Audit | ${{ needs.composer-audit.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
345+
echo "| PHP Compatibility | ${{ needs.php-compat.result == 'success' && '✅ Pass' || '⚠️ Partial' }} |" >> $GITHUB_STEP_SUMMARY
346+
347+
# Fail if any critical check failed
348+
if [ "${{ needs.syntax.result }}" != "success" ] || \
349+
[ "${{ needs.code-style.result }}" != "success" ] || \
350+
[ "${{ needs.phpstan.result }}" != "success" ] || \
351+
[ "${{ needs.strict-types.result }}" != "success" ] || \
352+
[ "${{ needs.security-patterns.result }}" != "success" ]; then
353+
echo ""
354+
echo "::error::One or more critical checks failed"
355+
exit 1
356+
fi
357+
358+
echo ""
359+
echo "✅ All critical checks passed!"

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ htmlcov/
7373
*.log
7474
/logs/
7575

76+
# PHP
77+
.php-cs-fixer.cache
78+
.phpunit.cache/
79+
.phpstan.cache/
80+
7681
# Temp
7782
/tmp/
7883
*.tmp

0 commit comments

Comments
 (0)