Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
359 changes: 359 additions & 0 deletions .github/workflows/php-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
name: PHP Lint & Analysis

on:
push:
branches: [main, master]
paths:
- 'src/**'
- 'tests/**'
- 'composer.json'
- 'composer.lock'
- 'phpstan.neon'
- '.php-cs-fixer.dist.php'
- '.github/workflows/php-lint.yml'
pull_request:
paths:
- 'src/**'
- 'tests/**'
- 'composer.json'
- 'composer.lock'
- 'phpstan.neon'
- '.php-cs-fixer.dist.php'
- '.github/workflows/php-lint.yml'

permissions: read-all

env:
PHP_VERSION: '8.3'

jobs:
# ============================================================
# Syntax Check - Fast fail on parse errors
# ============================================================
syntax:
name: PHP Syntax Check
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Setup PHP
uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # v2.32.0
with:
php-version: ${{ env.PHP_VERSION }}
tools: none
coverage: none

- name: Check PHP syntax
run: |
echo "Checking PHP syntax..."
find src -name "*.php" -print0 | xargs -0 -n1 php -l
echo "✅ All PHP files have valid syntax"

# ============================================================
# Code Style - PSR-12 compliance via PHP-CS-Fixer
# ============================================================
code-style:
name: Code Style (PHP-CS-Fixer)
runs-on: ubuntu-latest
needs: syntax
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Setup PHP
uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # v2.32.0
with:
php-version: ${{ env.PHP_VERSION }}
tools: php-cs-fixer:3
coverage: none

- name: Run PHP-CS-Fixer
run: |
php-cs-fixer fix --dry-run --diff --verbose --config=.php-cs-fixer.dist.php

# ============================================================
# Static Analysis - PHPStan at maximum strictness
# ============================================================
phpstan:
name: Static Analysis (PHPStan Level 9)
runs-on: ubuntu-latest
needs: syntax
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Setup PHP
uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # v2.32.0
with:
php-version: ${{ env.PHP_VERSION }}
tools: phpstan:1
coverage: none

- name: Install Composer dependencies
run: composer install --no-progress --prefer-dist --no-interaction

- name: Run PHPStan
run: |
phpstan analyse --configuration=phpstan.neon --error-format=github

# ============================================================
# SPDX License Headers - Ensure all files have headers
# ============================================================
license-headers:
name: SPDX License Headers
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Check SPDX headers
run: |
MISSING=""
for file in $(find src -name "*.php"); do
if ! grep -q "SPDX-License-Identifier" "$file"; then
MISSING="$MISSING\n - $file"
fi
done

if [ -n "$MISSING" ]; then
echo "::error::Missing SPDX-License-Identifier in:$MISSING"
exit 1
fi
echo "✅ All PHP files have SPDX license headers"

# ============================================================
# Strict Types - Ensure all files use strict_types
# ============================================================
strict-types:
name: Strict Types Declaration
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Check strict_types
run: |
MISSING=""
for file in $(find src -name "*.php"); do
if ! grep -q "declare(strict_types=1)" "$file"; then
MISSING="$MISSING\n - $file"
fi
done

if [ -n "$MISSING" ]; then
echo "::error::Missing declare(strict_types=1) in:$MISSING"
exit 1
fi
echo "✅ All PHP files declare strict_types=1"

# ============================================================
# Security Patterns - Check for dangerous code patterns
# ============================================================
security-patterns:
name: Security Pattern Check
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Check dangerous functions
run: |
FOUND=0

# Dangerous execution functions
DANGEROUS=$(grep -rEn 'eval\s*\(|exec\s*\(|system\s*\(|passthru\s*\(|shell_exec\s*\(|proc_open\s*\(|popen\s*\(' \
--include="*.php" src/ 2>/dev/null || true)
if [ -n "$DANGEROUS" ]; then
echo "::error::Dangerous execution functions found:"
echo "$DANGEROUS"
FOUND=1
fi

# Backtick execution
BACKTICKS=$(grep -rEn '`[^`]*\$' --include="*.php" src/ 2>/dev/null || true)
if [ -n "$BACKTICKS" ]; then
echo "::error::Backtick execution with variables found:"
echo "$BACKTICKS"
FOUND=1
fi

# preg_replace with /e modifier (deprecated but check anyway)
PREG_E=$(grep -rEn "preg_replace\s*\([^)]*'/[^']*e[^']*'" --include="*.php" src/ 2>/dev/null || true)
if [ -n "$PREG_E" ]; then
echo "::error::preg_replace with /e modifier found:"
echo "$PREG_E"
FOUND=1
fi

# assert() with string (can execute code)
ASSERT_STR=$(grep -rEn "assert\s*\(['\"]" --include="*.php" src/ 2>/dev/null || true)
if [ -n "$ASSERT_STR" ]; then
echo "::error::assert() with string argument found:"
echo "$ASSERT_STR"
FOUND=1
fi

# create_function (deprecated, can execute code)
CREATE_FUNC=$(grep -rEn 'create_function\s*\(' --include="*.php" src/ 2>/dev/null || true)
if [ -n "$CREATE_FUNC" ]; then
echo "::error::create_function() found (use closures instead):"
echo "$CREATE_FUNC"
FOUND=1
fi

if [ "$FOUND" -eq 0 ]; then
echo "✅ No dangerous function patterns found"
else
exit 1
fi

- name: Check weak cryptography
run: |
FOUND=0

# MD5 for security (allow md5_file for checksums)
MD5=$(grep -rEn 'md5\s*\(' --include="*.php" src/ 2>/dev/null | grep -v 'md5_file' || true)
if [ -n "$MD5" ]; then
echo "::warning::MD5 usage found (ensure not used for security):"
echo "$MD5"
fi

# SHA1 for security
SHA1=$(grep -rEn 'sha1\s*\(' --include="*.php" src/ 2>/dev/null || true)
if [ -n "$SHA1" ]; then
echo "::warning::SHA1 usage found (ensure not used for security):"
echo "$SHA1"
fi

# Insecure random functions
RAND=$(grep -rEn '\brand\s*\(|\bmt_rand\s*\(|\buniqid\s*\(' --include="*.php" src/ 2>/dev/null || true)
if [ -n "$RAND" ]; then
echo "::warning::Potentially insecure random functions found (use random_int/random_bytes):"
echo "$RAND"
fi

echo "✅ Weak cryptography check completed"

- name: Check SQL injection patterns
run: |
# Direct variable interpolation in queries
SQLI=$(grep -rEn '(mysql_query|mysqli_query|pg_query|->query)\s*\([^)]*\$' \
--include="*.php" src/ 2>/dev/null || true)
if [ -n "$SQLI" ]; then
echo "::warning::Potential SQL injection pattern (use prepared statements):"
echo "$SQLI"
fi
echo "✅ SQL injection pattern check completed"

# ============================================================
# Composer Audit - Check for dependency vulnerabilities
# ============================================================
composer-audit:
name: Dependency Audit
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Setup PHP
uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # v2.32.0
with:
php-version: ${{ env.PHP_VERSION }}
tools: composer:2
coverage: none

- name: Install dependencies
run: composer install --no-progress --prefer-dist --no-interaction

- name: Run Composer audit
run: |
composer audit --format=plain || echo "::warning::Vulnerabilities found in dependencies"

# ============================================================
# Multi-version PHP Test - Ensure compatibility
# ============================================================
php-compat:
name: PHP ${{ matrix.php }} Compatibility
runs-on: ubuntu-latest
needs: [syntax, code-style, phpstan]
permissions:
contents: read
strategy:
fail-fast: false
matrix:
php: ['8.1', '8.2', '8.3', '8.4']
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 # v2.32.0
with:
php-version: ${{ matrix.php }}
coverage: none

- name: Install dependencies
run: composer install --no-progress --prefer-dist --no-interaction

- name: Check syntax on PHP ${{ matrix.php }}
run: find src -name "*.php" -print0 | xargs -0 -n1 php -l

- name: Run PHPStan on PHP ${{ matrix.php }}
run: vendor/bin/phpstan analyse --configuration=phpstan.neon --no-progress
continue-on-error: ${{ matrix.php == '8.4' }} # Allow failures on newest PHP

# ============================================================
# Summary - Aggregate all check results
# ============================================================
lint-summary:
name: Lint Summary
runs-on: ubuntu-latest
needs: [syntax, code-style, phpstan, license-headers, strict-types, security-patterns, composer-audit, php-compat]
if: always()
permissions:
contents: read
steps:
- name: Check results
run: |
echo "## PHP Lint & Analysis Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Syntax | ${{ needs.syntax.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Code Style | ${{ needs.code-style.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| PHPStan | ${{ needs.phpstan.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| License Headers | ${{ needs.license-headers.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Strict Types | ${{ needs.strict-types.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Security Patterns | ${{ needs.security-patterns.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Composer Audit | ${{ needs.composer-audit.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| PHP Compatibility | ${{ needs.php-compat.result == 'success' && '✅ Pass' || '⚠️ Partial' }} |" >> $GITHUB_STEP_SUMMARY

# Fail if any critical check failed
if [ "${{ needs.syntax.result }}" != "success" ] || \
[ "${{ needs.code-style.result }}" != "success" ] || \
[ "${{ needs.phpstan.result }}" != "success" ] || \
[ "${{ needs.strict-types.result }}" != "success" ] || \
[ "${{ needs.security-patterns.result }}" != "success" ]; then
echo ""
echo "::error::One or more critical checks failed"
exit 1
fi

echo ""
echo "✅ All critical checks passed!"
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ htmlcov/
*.log
/logs/

# PHP
.php-cs-fixer.cache
.phpunit.cache/
.phpstan.cache/

# Temp
/tmp/
*.tmp
Expand Down
Loading
Loading