name: CI on: push: branches: [main] pull_request: branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: PYTHON_VERSION: "3.11" PIP_DISABLE_PIP_VERSION_CHECK: "1" PIP_NO_CACHE_DIR: "1" jobs: lint: name: Lint & Security Scan runs-on: ubuntu-latest container: image: python:3.11-slim steps: - name: Setup and checkout run: | apt-get update -qq && apt-get install -yqq --no-install-recommends git >/dev/null git clone --depth 1 --branch "${GITHUB_REF_NAME}" \ "https://oauth2:${{ github.token }}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git" . - name: Install dependencies run: | pip install -q -r requirements.txt pip install -q ruff mypy bandit pip-audit - name: Python syntax check run: python -m py_compile run.py wsgi.py app/*.py app/**/*.py - name: Ruff lint run: ruff check app/ tests/ fpaste - name: Ruff format run: ruff format --check app/ tests/ fpaste - name: Type check run: mypy app/ tests/ fpaste --ignore-missing-imports - name: Bandit security scan (app) run: bandit -r app/ -ll -q - name: Bandit security scan (CLI) run: bandit fpaste -ll -q - name: Check for hardcoded secrets run: | echo "Checking for hardcoded secrets..." if grep -rE "(password|secret|api_key|token)\s*=\s*['\"][^'\"]+['\"]" app/ --include="*.py" | grep -v "environ.get\|config\[" | grep -v "test_\|#.*password"; then echo "::warning::Potential hardcoded secrets found" else echo "No hardcoded secrets detected" fi - name: Dependency audit run: | pip-audit --progress-spinner=off --strict || { echo "::warning::pip-audit found vulnerabilities - review required" pip-audit --progress-spinner=off 2>&1 | tail -20 } continue-on-error: true test: name: Tests runs-on: ubuntu-latest needs: [lint] container: image: python:3.11-slim steps: - name: Setup and checkout run: | apt-get update -qq && apt-get install -yqq --no-install-recommends git >/dev/null git clone --depth 1 --branch "${GITHUB_REF_NAME}" \ "https://oauth2:${{ github.token }}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git" . - name: Install dependencies run: | pip install -q -r requirements.txt pip install -q pytest pytest-cov hypothesis - name: Unit tests run: | pytest tests/test_api.py tests/test_database.py \ tests/test_paste_*.py tests/test_metrics.py tests/test_pki.py \ tests/test_url_shortener.py tests/test_scheduled_cleanup.py \ -v --tb=short - name: Security tests run: | pytest tests/test_security.py tests/test_rate_limiting.py \ tests/test_abuse_prevention.py tests/test_pow.py \ tests/test_cli_security.py tests/test_audit.py \ -v --tb=short - name: Memory leak tests run: pytest tests/test_memory.py -v --tb=short - name: Fuzz tests run: | pytest tests/test_fuzz.py -v --tb=short \ --hypothesis-seed=0 \ -x - name: Advanced security tests run: | python tests/security/cli_security_audit.py python tests/security/dos_memory_test.py python tests/security/race_condition_test.py python tests/security/headers_audit.py - name: Coverage report run: | pytest tests/ --cov=app --cov-report=term-missing --cov-fail-under=70 || \ echo "::warning::Coverage below 70%" continue-on-error: true build-push: name: Build & Push Image runs-on: ubuntu-latest needs: [test] if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - name: Checkout run: | command -v git >/dev/null 2>&1 || { apt-get update -qq && apt-get install -yqq --no-install-recommends git >/dev/null } git clone --depth 1 --branch "${GITHUB_REF_NAME}" \ "https://oauth2:${{ github.token }}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git" . - name: Build image run: | # Use docker or podman, whichever is available if command -v docker >/dev/null 2>&1; then BUILD_CMD="docker" elif command -v podman >/dev/null 2>&1; then BUILD_CMD="podman" else echo "::error::Neither docker nor podman found" exit 1 fi echo "Using: $BUILD_CMD" SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) $BUILD_CMD build --load -f Containerfile -t flaskpaste:latest -t flaskpaste:sha-${SHORT_SHA} . echo "Image built" $BUILD_CMD images | grep flaskpaste - name: Push to Harbor env: HARBOR_USER: ${{ secrets.HARBOR_USER }} HARBOR_PASS: ${{ secrets.HARBOR_PASS }} HARBOR_REGISTRY: harbor.mymx.me run: | if [ -z "$HARBOR_USER" ] || [ -z "$HARBOR_PASS" ]; then echo "::warning::Harbor credentials not configured - skipping push" echo "Configure HARBOR_USER and HARBOR_PASS secrets to enable push" exit 0 fi # Use docker or podman if command -v docker >/dev/null 2>&1; then BUILD_CMD="docker" else BUILD_CMD="podman" fi SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) $BUILD_CMD login "${HARBOR_REGISTRY}" \ -u "$HARBOR_USER" -p "$HARBOR_PASS" # Push image with multiple tags (latest, slim for backwards compat, sha) for tag in latest slim sha-${SHORT_SHA}; do $BUILD_CMD tag flaskpaste:latest "${HARBOR_REGISTRY}/library/flaskpaste:${tag}" $BUILD_CMD push "${HARBOR_REGISTRY}/library/flaskpaste:${tag}" echo "Pushed: ${HARBOR_REGISTRY}/library/flaskpaste:${tag}" done vuln-scan: name: Harbor Vulnerability Scan runs-on: ubuntu-latest needs: [build-push] if: github.event_name == 'push' && github.ref == 'refs/heads/main' container: image: python:3.11-slim steps: - name: Setup run: | apt-get update -qq && apt-get install -yqq --no-install-recommends git curl >/dev/null - name: Fetch harbor-ctl run: | curl -sL "https://git.mymx.me/username/harbor/raw/branch/master/harbor-ctl.py" -o harbor-ctl.py chmod +x harbor-ctl.py - name: Trigger and wait for scan env: HARBOR_USER: ${{ secrets.HARBOR_USER }} HARBOR_PASS: ${{ secrets.HARBOR_PASS }} run: | if [ -z "$HARBOR_USER" ] || [ -z "$HARBOR_PASS" ]; then echo "::warning::Harbor credentials not configured - skipping scan" exit 0 fi # Wait for Harbor to index the pushed images echo "Waiting for Harbor to index images..." sleep 15 echo "Triggering vulnerability scan..." python harbor-ctl.py --url https://harbor.mymx.me \ -u "$HARBOR_USER" -p "$HARBOR_PASS" \ scan library flaskpaste -d latest --wait --timeout 180 - name: Check for critical vulnerabilities env: HARBOR_USER: ${{ secrets.HARBOR_USER }} HARBOR_PASS: ${{ secrets.HARBOR_PASS }} run: | if [ -z "$HARBOR_USER" ]; then exit 0; fi check_vulns() { local tag="$1" echo "Checking fixable critical/high vulnerabilities for :${tag}..." python harbor-ctl.py --url https://harbor.mymx.me \ -u "$HARBOR_USER" -p "$HARBOR_PASS" \ vulns library flaskpaste -d ${tag} -s critical -l 100 > /tmp/critical-${tag}.txt 2>&1 || true python harbor-ctl.py --url https://harbor.mymx.me \ -u "$HARBOR_USER" -p "$HARBOR_PASS" \ vulns library flaskpaste -d ${tag} -s high -l 100 > /tmp/high-${tag}.txt 2>&1 || true CRITICAL=$(grep -v "N/A *$" /tmp/critical-${tag}.txt 2>/dev/null | grep -cE "^CVE-|^GHSA-" 2>/dev/null) || CRITICAL=0 HIGH=$(grep -v "N/A *$" /tmp/high-${tag}.txt 2>/dev/null | grep -cE "^CVE-|^GHSA-" 2>/dev/null) || HIGH=0 echo " :${tag} - Critical fixable: $CRITICAL, High fixable: $HIGH" if [ "$CRITICAL" -gt 0 ]; then echo "::error::Found $CRITICAL fixable critical vulnerabilities in :${tag}" cat /tmp/critical-${tag}.txt return 1 fi if [ "$HIGH" -gt 0 ]; then echo "::warning::Found $HIGH fixable high vulnerabilities in :${tag}" cat /tmp/high-${tag}.txt fi return 0 } check_vulns latest || exit 1 echo "Vulnerability scan passed"