Some checks failed
Deploy Application / deploy (push) Has been cancelled
305 lines
12 KiB
YAML
305 lines
12 KiB
YAML
name: Security Vulnerability Scan
|
||
|
||
on:
|
||
push:
|
||
branches: [ main, staging, develop ]
|
||
pull_request:
|
||
branches: [ main, staging, develop ]
|
||
schedule:
|
||
# Daily security scan at 2 AM UTC
|
||
- cron: '0 2 * * *'
|
||
workflow_dispatch:
|
||
|
||
jobs:
|
||
check-changes:
|
||
name: Check for Dependency Changes
|
||
runs-on: ubuntu-latest
|
||
outputs:
|
||
dependencies_changed: ${{ steps.filter.outputs.dependencies_changed }}
|
||
steps:
|
||
- name: Download CI helpers
|
||
shell: bash
|
||
env:
|
||
CI_TOKEN: ${{ secrets.CI_TOKEN }}
|
||
run: |
|
||
set -euo pipefail
|
||
REF="${{ github.sha }}"
|
||
if [ -z "$REF" ]; then
|
||
REF="${{ github.ref_name }}"
|
||
fi
|
||
if [ -z "$REF" ]; then
|
||
REF="${{ github.head_ref }}"
|
||
fi
|
||
if [ -z "$REF" ]; then
|
||
REF="main"
|
||
fi
|
||
URL="https://git.michaelschiemer.de/${{ github.repository }}/raw/${REF}/scripts/ci/clone_repo.sh"
|
||
mkdir -p /tmp/ci-tools
|
||
if [ -n "$CI_TOKEN" ]; then
|
||
curl -sfL -u "$CI_TOKEN:x-oauth-basic" "$URL" -o /tmp/ci-tools/clone_repo.sh
|
||
else
|
||
curl -sfL "$URL" -o /tmp/ci-tools/clone_repo.sh
|
||
fi
|
||
chmod +x /tmp/ci-tools/clone_repo.sh
|
||
|
||
- name: Analyse changed files
|
||
id: filter
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
|
||
REF_NAME="${{ github.ref_name }}"
|
||
if [ -z "$REF_NAME" ]; then
|
||
REF_NAME="${{ github.head_ref }}"
|
||
fi
|
||
if [ -z "$REF_NAME" ]; then
|
||
REF_NAME="main"
|
||
fi
|
||
|
||
REPO="${{ github.repository }}"
|
||
WORKDIR="/workspace/repo"
|
||
|
||
export CI_REPOSITORY="$REPO"
|
||
export CI_TOKEN="${{ secrets.CI_TOKEN }}"
|
||
export CI_REF_NAME="$REF_NAME"
|
||
export CI_DEFAULT_BRANCH="main"
|
||
export CI_TARGET_DIR="$WORKDIR"
|
||
export CI_FETCH_DEPTH="2"
|
||
|
||
/tmp/ci-tools/clone_repo.sh
|
||
|
||
cd "$WORKDIR"
|
||
|
||
# For scheduled or manual runs, always run the scan
|
||
if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||
echo "dependencies_changed=true" >> "$GITHUB_OUTPUT"
|
||
echo "ℹ️ Scheduled/manual run - will scan dependencies"
|
||
exit 0
|
||
fi
|
||
|
||
CHANGED_FILES=""
|
||
EVENT_BEFORE="${{ github.event.before }}"
|
||
|
||
if [ "${{ github.event_name }}" = "push" ] && [ -n "$EVENT_BEFORE" ]; then
|
||
if git rev-parse "$EVENT_BEFORE" >/dev/null 2>&1; then
|
||
CHANGED_FILES="$(git diff --name-only "$EVENT_BEFORE" HEAD || true)"
|
||
else
|
||
git fetch origin "$EVENT_BEFORE" --depth 1 || true
|
||
if git rev-parse "$EVENT_BEFORE" >/dev/null 2>&1; then
|
||
CHANGED_FILES="$(git diff --name-only "$EVENT_BEFORE" HEAD || true)"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
if [ -z "$CHANGED_FILES" ]; then
|
||
if git rev-parse HEAD^ >/dev/null 2>&1; then
|
||
CHANGED_FILES="$(git diff --name-only HEAD^ HEAD || true)"
|
||
else
|
||
git fetch origin "$REF_NAME" --depth 50 || true
|
||
if git rev-parse HEAD^ >/dev/null 2>&1; then
|
||
CHANGED_FILES="$(git diff --name-only HEAD^ HEAD || true)"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
DEPENDENCIES_CHANGED=false
|
||
|
||
if [ -n "$CHANGED_FILES" ]; then
|
||
while IFS= read -r FILE; do
|
||
[ -z "$FILE" ] && continue
|
||
if echo "$FILE" | grep -Eq "^(composer\.json|composer\.lock)$"; then
|
||
DEPENDENCIES_CHANGED=true
|
||
break
|
||
fi
|
||
done <<< "$CHANGED_FILES"
|
||
fi
|
||
|
||
echo "dependencies_changed=$DEPENDENCIES_CHANGED" >> "$GITHUB_OUTPUT"
|
||
|
||
if [ "$DEPENDENCIES_CHANGED" = "true" ]; then
|
||
echo "ℹ️ Dependencies changed - security scan will run"
|
||
else
|
||
echo "ℹ️ No dependency changes detected - skipping security scan"
|
||
fi
|
||
|
||
security-audit:
|
||
needs: check-changes
|
||
if: needs.check-changes.outputs.dependencies_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||
name: Composer Security Audit
|
||
runs-on: php-ci # Uses pre-built PHP 8.5 CI image with Composer pre-installed
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
run: |
|
||
# For pull_request events, use the head ref (source branch)
|
||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||
REF_NAME="${{ github.head_ref || github.event.pull_request.head.ref }}"
|
||
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||
REF_NAME="${{ inputs.branch || github.ref_name }}"
|
||
else
|
||
REF_NAME="${{ github.ref_name }}"
|
||
fi
|
||
|
||
# Fallback to main if REF_NAME is still empty
|
||
if [ -z "$REF_NAME" ] || [ "$REF_NAME" = "" ]; then
|
||
REF_NAME="main"
|
||
fi
|
||
|
||
REPO="${{ github.repository }}"
|
||
|
||
echo "📋 Cloning branch: $REF_NAME"
|
||
echo "📦 Repository: $REPO"
|
||
|
||
# Use CI token if available, otherwise try public access
|
||
if [ -n "${{ secrets.CI_TOKEN }}" ]; then
|
||
git clone --depth 1 --branch "$REF_NAME" \
|
||
"https://${{ secrets.CI_TOKEN }}@git.michaelschiemer.de/${REPO}.git" \
|
||
/workspace/repo
|
||
else
|
||
# Try public HTTPS (works if repository is public)
|
||
git clone --depth 1 --branch "$REF_NAME" \
|
||
"https://git.michaelschiemer.de/${REPO}.git" \
|
||
/workspace/repo || \
|
||
# Fallback: Try to use Gitea's internal runner access
|
||
git clone --depth 1 \
|
||
"https://git.michaelschiemer.de/${REPO}.git" \
|
||
/workspace/repo
|
||
fi
|
||
|
||
cd /workspace/repo
|
||
|
||
- name: Get Composer cache directory
|
||
id: composer-cache
|
||
shell: bash
|
||
run: |
|
||
echo "dir=$(composer global config cache-dir 2>/dev/null | cut -d' ' -f3 || echo "$HOME/.composer/cache")" >> $GITHUB_OUTPUT
|
||
|
||
- name: Cache Composer dependencies
|
||
uses: actions/cache@v4
|
||
with:
|
||
path: |
|
||
${{ steps.composer-cache.outputs.dir }}
|
||
vendor/
|
||
key: ${{ runner.os }}-composer-security-${{ hashFiles('**/composer.lock') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-composer-security-
|
||
|
||
- name: Validate composer.json and composer.lock
|
||
run: |
|
||
cd /workspace/repo
|
||
# Validate composer.json (less strict - lock file might be updated during install)
|
||
composer validate --no-check-lock || echo "⚠️ composer.lock might need update, but continuing..."
|
||
# Try to update lock file if needed
|
||
composer update --lock --no-interaction || echo "⚠️ Could not update lock file, but continuing..."
|
||
|
||
- name: Install dependencies
|
||
run: |
|
||
cd /workspace/repo
|
||
# TEMPORARY WORKAROUND: Ignore PHP 8.5 platform requirement until dependencies officially support PHP 8.5
|
||
# TODO: Remove --ignore-platform-req=php when dependencies are updated (estimated: 1 month)
|
||
composer install --prefer-dist --no-progress --no-dev --ignore-platform-req=php
|
||
|
||
- name: Run Composer Security Audit
|
||
id: security-audit
|
||
run: |
|
||
cd /workspace/repo
|
||
composer audit --format=json > audit-result.json || true
|
||
cat audit-result.json
|
||
|
||
- name: Parse audit results
|
||
id: parse-audit
|
||
run: |
|
||
cd /workspace/repo
|
||
if [ -f audit-result.json ]; then
|
||
# jq is pre-installed in php-ci image
|
||
jq --version
|
||
|
||
ADVISORIES=$(jq -r '.advisories | length' audit-result.json 2>/dev/null || echo "0")
|
||
ABANDONED=$(jq -r '.abandoned | length' audit-result.json 2>/dev/null || echo "0")
|
||
|
||
echo "advisories_count=$ADVISORIES" >> $GITHUB_OUTPUT
|
||
echo "abandoned_count=$ABANDONED" >> $GITHUB_OUTPUT
|
||
|
||
echo "## Security Audit Results" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Vulnerabilities Found:** $ADVISORIES" >> $GITHUB_STEP_SUMMARY
|
||
echo "- **Abandoned Packages:** $ABANDONED" >> $GITHUB_STEP_SUMMARY
|
||
|
||
if [ "$ADVISORIES" -gt 0 ]; then
|
||
echo "has_vulnerabilities=true" >> $GITHUB_OUTPUT
|
||
echo "- **Status:** ❌ Security vulnerabilities detected - review required" >> $GITHUB_STEP_SUMMARY
|
||
|
||
# Display vulnerability details
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### Vulnerability Details" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||
jq '.advisories' audit-result.json >> $GITHUB_STEP_SUMMARY
|
||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||
|
||
exit 1
|
||
else
|
||
echo "has_vulnerabilities=false" >> $GITHUB_OUTPUT
|
||
echo "- **Status:** ✅ No security vulnerabilities detected" >> $GITHUB_STEP_SUMMARY
|
||
fi
|
||
|
||
if [ "$ABANDONED" -gt 0 ]; then
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
echo "### Abandoned Packages" >> $GITHUB_STEP_SUMMARY
|
||
echo "" >> $GITHUB_STEP_SUMMARY
|
||
jq -r '.abandoned | to_entries[] | "- **\(.key)**: \(.value // "No replacement suggested")"' audit-result.json >> $GITHUB_STEP_SUMMARY
|
||
fi
|
||
fi
|
||
|
||
- name: Save audit results
|
||
if: always()
|
||
run: |
|
||
cd /workspace/repo
|
||
if [ -f audit-result.json ]; then
|
||
mkdir -p /tmp/artifacts
|
||
cp audit-result.json /tmp/artifacts/security-audit-results-${{ github.run_number }}.json || true
|
||
echo "✅ Audit results saved"
|
||
fi
|
||
|
||
- name: Create Gitea issue on vulnerability (scheduled runs only)
|
||
if: failure() && github.event_name == 'schedule'
|
||
run: |
|
||
# Prepare issue body
|
||
ISSUE_TITLE="🚨 Security Vulnerabilities Detected in Dependencies"
|
||
ISSUE_BODY="## Security Audit Report\n\n"
|
||
ISSUE_BODY="${ISSUE_BODY}**Date:** $(date -u +"%Y-%m-%d %H:%M:%S UTC")\n"
|
||
ISSUE_BODY="${ISSUE_BODY}**Workflow Run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n\n"
|
||
|
||
if [ -f audit-result.json ]; then
|
||
# Add vulnerability details
|
||
ISSUE_BODY="${ISSUE_BODY}### Vulnerabilities Found\n\n"
|
||
ISSUE_BODY="${ISSUE_BODY}\`\`\`json\n"
|
||
ISSUE_BODY="${ISSUE_BODY}$(cat audit-result.json)\n"
|
||
ISSUE_BODY="${ISSUE_BODY}\`\`\`\n\n"
|
||
fi
|
||
|
||
ISSUE_BODY="${ISSUE_BODY}### Action Required\n\n"
|
||
ISSUE_BODY="${ISSUE_BODY}1. Review the vulnerability details above\n"
|
||
ISSUE_BODY="${ISSUE_BODY}2. Update affected packages: \`composer update <package>\`\n"
|
||
ISSUE_BODY="${ISSUE_BODY}3. Run tests: \`make test\`\n"
|
||
ISSUE_BODY="${ISSUE_BODY}4. Verify with: \`make security-check\`\n"
|
||
|
||
# Create issue using Gitea API
|
||
# Note: Requires CI_TOKEN secret to be configured
|
||
if [ -n "${{ secrets.CI_TOKEN }}" ]; then
|
||
curl -X POST \
|
||
-H "Authorization: token ${{ secrets.CI_TOKEN }}" \
|
||
-H "Content-Type: application/json" \
|
||
-d "{\"title\":\"${ISSUE_TITLE}\",\"body\":\"${ISSUE_BODY}\",\"labels\":[\"security\",\"dependencies\",\"automated\"]}" \
|
||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues"
|
||
else
|
||
echo "⚠️ CI_TOKEN not configured - skipping issue creation"
|
||
echo "Please add CI_TOKEN as repository secret for automated issue creation"
|
||
fi
|
||
|
||
- name: Notify on failure
|
||
if: failure()
|
||
run: |
|
||
echo "::error::Security vulnerabilities detected! Check the audit results for details."
|
||
echo "Run 'make security-check' locally to review vulnerabilities."
|