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 & Format 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 - 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 security: name: 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 --upgrade pip pip install -q -r requirements.txt pip install -q bandit pip-audit - name: Bandit security scan (app) run: | # -ll = medium and high severity only bandit -r app/ -ll -q - name: Bandit security scan (CLI) run: | # Scan fpaste CLI for security issues bandit fpaste -ll -q - name: Check for hardcoded secrets run: | # Simple pattern check for common secret patterns 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: | # Check for known vulnerabilities in dependencies # Fail on high/critical, warn on medium pip-audit --progress-spinner=off --strict || { echo "::warning::pip-audit found vulnerabilities - review required" # Show summary but don't fail CI for now pip-audit --progress-spinner=off 2>&1 | tail -20 } continue-on-error: true test: name: Unit 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 - name: Run unit tests run: | pytest tests/test_api.py tests/test_database.py \ tests/test_paste_*.py tests/test_metrics.py tests/test_pki.py \ -v --tb=short - name: Run tests with coverage run: | pytest tests/ --cov=app --cov-report=term-missing --cov-fail-under=70 || \ echo "::warning::Coverage below 70%" continue-on-error: true security-tests: name: Security Tests runs-on: ubuntu-latest needs: [lint, security] 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 - name: Security header tests run: pytest tests/test_security.py -v --tb=short - name: Rate limiting tests run: pytest tests/test_rate_limiting.py -v --tb=short - name: Abuse prevention tests run: pytest tests/test_abuse_prevention.py -v --tb=short - name: PoW tests run: pytest tests/test_pow.py -v --tb=short - name: CLI security tests run: pytest tests/test_cli_security.py -v --tb=short - name: Audit logging tests run: pytest tests/test_audit.py -v --tb=short security-advanced: name: Advanced Security Tests runs-on: ubuntu-latest needs: [lint, security] 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 - name: CLI security audit run: python tests/security/cli_security_audit.py - name: DoS memory exhaustion tests run: python tests/security/dos_memory_test.py - name: Race condition tests run: python tests/security/race_condition_test.py - name: Security headers audit run: python tests/security/headers_audit.py memory: name: Memory Leak Check 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 - name: Run memory leak tests run: pytest tests/test_memory.py -v --tb=short sbom: name: SBOM Generation 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 jq >/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 cyclonedx-bom - name: Generate SBOM run: | # Generate CycloneDX SBOM for supply chain transparency cyclonedx-py requirements requirements.txt -o sbom.json --of json echo "SBOM generated with $(jq '.components | length' sbom.json) components" - name: Validate SBOM run: | # Basic validation - check for expected fields if jq -e '.bomFormat == "CycloneDX"' sbom.json >/dev/null; then echo "SBOM format: CycloneDX" echo "Components: $(jq '.components | length' sbom.json)" echo "Spec version: $(jq -r '.specVersion' sbom.json)" else echo "::warning::SBOM validation failed" fi build-push: name: Build & Push Image runs-on: ubuntu-latest needs: [test, security-tests] if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - name: Checkout run: | 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 -f Containerfile -t flaskpaste:latest -t flaskpaste:sha-${SHORT_SHA} . $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" for tag in latest 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 echo "Triggering vulnerability scan..." python harbor-ctl.py --url https://harbor.mymx.me \ -u "$HARBOR_USER" -p "$HARBOR_PASS" \ scan library flaskpaste --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 echo "Checking for fixable critical/high vulnerabilities..." # Get vulnerability report python harbor-ctl.py --url https://harbor.mymx.me \ -u "$HARBOR_USER" -p "$HARBOR_PASS" \ vulns library flaskpaste -s critical -l 100 > /tmp/critical.txt 2>&1 || true python harbor-ctl.py --url https://harbor.mymx.me \ -u "$HARBOR_USER" -p "$HARBOR_PASS" \ vulns library flaskpaste -s high -l 100 > /tmp/high.txt 2>&1 || true # Check for fixable vulns (have a "Fixed" version that's not "N/A") CRITICAL_FIXABLE=$(grep -v "N/A *$" /tmp/critical.txt | grep -c "^CVE\|^GHSA" || echo 0) HIGH_FIXABLE=$(grep -v "N/A *$" /tmp/high.txt | grep -c "^CVE\|^GHSA" || echo 0) echo "Critical fixable: $CRITICAL_FIXABLE" echo "High fixable: $HIGH_FIXABLE" if [ "$CRITICAL_FIXABLE" -gt 0 ]; then echo "::error::Found $CRITICAL_FIXABLE fixable critical vulnerabilities" cat /tmp/critical.txt exit 1 fi if [ "$HIGH_FIXABLE" -gt 0 ]; then echo "::warning::Found $HIGH_FIXABLE fixable high vulnerabilities" cat /tmp/high.txt fi echo "Vulnerability scan passed"