Salta el contingut

PR5032 - Pipeline CI/CD amb Seguretat Integrada

Objectius

  • Crear un pipeline CI/CD amb GitHub Actions que integri seguretat
  • Configurar SAST automàtic amb Semgrep i CodeQL
  • Escanejar dependències amb Trivy i Dependency-Check
  • Configurar escaneig de secrets amb GitLeaks
  • Implementar desplegament automatitzat segur amb Docker

Prerequisits

Element Detall
Temps estimat 4 hores
Eines necessàries GitHub account (gratuït), Docker Hub account
Coneixements previs Git bàsic, Docker bàsic
Activitats relacionades AC5031, AC5032

Introducció

En aquesta pràctica construirem un pipeline de DevSecOps real usant GitHub Actions, el servei CI/CD gratuït de GitHub. Partirem d'una aplicació Python vulnerable (intencionadament) i configurarem el pipeline per detectar les vulnerabilitats automàticament.

flowchart LR
    DEV[Developer\ngit push] --> GH[GitHub]
    GH --> SAST[1. SAST\nSemgrep + CodeQL]
    GH --> SECRETS[2. Secrets\nGitLeaks]
    GH --> BUILD[3. Docker Build]
    BUILD --> SCAN[4. Image Scan\nTrivy]
    SCAN --> TEST[5. Tests\npytest + DAST]
    TEST --> DEPLOY[6. Deploy\nDocker Hub]
    DEPLOY --> NOTIFY[7. Notificació\nSlack/Email]

    SAST -->|Vulnerabilitat crítica| BLOCK[❌ BLOQUEJAT]
    SECRETS -->|Secret detectat| BLOCK
    SCAN -->|CVE crític| BLOCK

Part 1: Crear el repositori i l'aplicació

1.1 Estructura del projecte

mkdir pipeline-segur-NOMCOGNOM
cd pipeline-segur-NOMCOGNOM
git init

# Estructura de fitxers
mkdir -p .github/workflows src tests

1.2 Aplicació Python vulnerable (intencionadament)

# src/app.py - Aplicació Flask amb vulnerabilitats per a detectar
from flask import Flask, request, jsonify
import sqlite3
import hashlib
import os

app = Flask(__name__)

# ❌ VULNERABILITAT: Contrasenya hardcodejada (detectada per GitLeaks/Semgrep)
DB_PASSWORD = "admin123"
API_KEY = "sk-1234567890abcdef"

def get_db():
    return sqlite3.connect('/tmp/users.db')

@app.route('/users', methods=['GET'])
def get_users():
    user_id = request.args.get('id', '')

    # ❌ VULNERABILITAT: SQL Injection (detectada per Semgrep)
    conn = get_db()
    cursor = conn.cursor()
    query = f"SELECT * FROM users WHERE id = {user_id}"
    cursor.execute(query)
    users = cursor.fetchall()
    return jsonify(users)

@app.route('/login', methods=['POST'])
def login():
    data = request.json
    password = data.get('password', '')

    # ❌ VULNERABILITAT: Hashing insegur (MD5, detectat per Semgrep)
    hashed = hashlib.md5(password.encode()).hexdigest()

    return jsonify({'status': 'ok', 'hash': hashed})

@app.route('/health', methods=['GET'])
def health():
    return jsonify({'status': 'ok', 'version': '1.0.0'})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)  # ❌ Debug mode en producció

1.3 Dockerfile

# Dockerfile - versió millorada (segura)
FROM python:3.12-slim

# Crear usuari no-root
RUN groupadd -r appuser && useradd -r -g appuser appuser

WORKDIR /app

# Copiar dependències primer (aprofitar cache Docker)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copiar codi amb el propietari correcte
COPY --chown=appuser:appuser src/ .

# Canviar a usuari no-root
USER appuser

EXPOSE 5000

# Usar gunicorn en lloc de Flask dev server
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "app:app"]

1.4 requirements.txt

flask==3.0.3
gunicorn==22.0.0
pytest==8.2.0
pytest-flask==1.3.0

Part 2: Configuració de GitHub Actions

2.1 Workflow principal

Crea el fitxer .github/workflows/security-pipeline.yml:

# .github/workflows/security-pipeline.yml
name: Security Pipeline - NOMCOGNOM

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

env:
  IMAGE_NAME: nomcognom-app    # Canviar per el teu nom
  REGISTRY: docker.io

jobs:
  # ============================================
  # JOB 1: Escaneig de secrets
  # ============================================
  secret-scanning:
    name: Secret Scanning (GitLeaks)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Historial complet per a escaneig de tot l'historial

      - name: Executar GitLeaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        # Bloca si trobi secrets (API keys, contrasenyes, etc.)

  # ============================================
  # JOB 2: Anàlisi estàtica de codi (SAST)
  # ============================================
  sast:
    name: SAST Analysis
    runs-on: ubuntu-latest
    needs: secret-scanning
    permissions:
      security-events: write

    steps:
      - uses: actions/checkout@v4

      # Semgrep: regles OWASP
      - name: Semgrep OWASP Top 10
        uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/owasp-top-ten
            p/python
            p/flask
        env:
          SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}

      # CodeQL: anàlisi profunda de seguretat
      - name: Inicialitzar CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: python
          queries: security-and-quality

      - name: Executar anàlisi CodeQL
        uses: github/codeql-action/analyze@v3
        with:
          category: "/language:python"

  # ============================================
  # JOB 3: Build de la imatge Docker
  # ============================================
  build:
    name: Docker Build
    runs-on: ubuntu-latest
    needs: sast
    outputs:
      image-digest: ${{ steps.build.outputs.digest }}

    steps:
      - uses: actions/checkout@v4

      - name: Configurar Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login al Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build i Push
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
            ${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # ============================================
  # JOB 4: Escaneig de la imatge Docker
  # ============================================
  image-scan:
    name: Container Image Scan (Trivy)
    runs-on: ubuntu-latest
    needs: build

    steps:
      - uses: actions/checkout@v4

      - name: Escanejar imatge amb Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: '${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ github.sha }}'
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'   # Falla si hi ha vulnerabilitats crítiques

      - name: Pujar resultats a GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'

  # ============================================
  # JOB 5: Tests
  # ============================================
  tests:
    name: Tests
    runs-on: ubuntu-latest
    needs: build

    steps:
      - uses: actions/checkout@v4

      - name: Configurar Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Instal·lar dependències
        run: pip install -r requirements.txt

      - name: Executar tests
        run: |
          cd src && python -m pytest ../tests/ -v --tb=short

  # ============================================
  # JOB 6: Desplegament (únicament a main)
  # ============================================
  deploy:
    name: Deploy a Producció
    runs-on: ubuntu-latest
    needs: [image-scan, tests]
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production  # Requereix aprovació manual

    steps:
      - name: Notificar desplegament
        run: |
          echo "✅ Desplegant ${{ env.IMAGE_NAME }}:${{ github.sha }}"
          echo "Tots els checks de seguretat superats"
          # Aquí anirien les comandes de desplegament real
          # kubectl set image deployment/app app=...

Reflexió 1

Observa l'ordre dels jobs en el pipeline. Per quin motiu el job secret-scanning s'executa primer de tot, abans del build? Quines conseqüències tindria que un secret s'hagués pujat al repositori i el job de secrets el detectés DESPRES de fer el push de la imatge?

Part 3: Configurar els Secrets de GitHub

# Secrets necessaris a GitHub (Settings → Secrets and variables → Actions):
# - DOCKERHUB_USERNAME: el teu username de Docker Hub
# - DOCKERHUB_TOKEN: token d'accés de Docker Hub (no la contrasenya!)
# - SEMGREP_APP_TOKEN: token de semgrep.dev (compte gratuït)

3.1 Crear un token de Docker Hub

  1. Accedir a Docker Hub → Account Settings → Security
  2. New Access Token → Nom: github-actions-NOMCOGNOM
  3. Permisos: Read, Write (per a push d'imatges)
  4. Copiar el token i guardar-lo com a secret DOCKERHUB_TOKEN a GitHub

Part 4: Observar les Vulnerabilitats Detectades

4.1 Fer un commit i observar el pipeline

# Inicialitzar el repositori i fer el primer commit
git add .
git commit -m "Initial commit - app with intentional vulnerabilities"
git branch -M main
git remote add origin https://github.com/TU_USERNAME/pipeline-segur-NOMCOGNOM.git
git push -u origin main

# Observar el pipeline a: GitHub → Actions

4.2 Analitzar els resultats de Semgrep

El pipeline hauria de detectar almenys: - Contrasenya hardcodejada (DB_PASSWORD = "admin123") - API key hardcodejada - SQL injection - Ús de MD5 per a hashing - Flask debug mode activat

## Taula de vulnerabilitats detectades per Semgrep - NOMCOGNOM

| Regla | Fitxer | Línia | Gravetat | CWE |
|-------|--------|-------|----------|-----|
| hardcoded-credentials | src/app.py | 10 | ERROR | CWE-798 |
| sql-injection | src/app.py | 24 | ERROR | CWE-89 |
| use-of-md5 | src/app.py | 34 | WARNING | CWE-327 |
| flask-debug-enabled | src/app.py | 42 | WARNING | CWE-489 |

Part 5: Corregir les vulnerabilitats i re-executar el pipeline

5.1 Versió corregida de l'app

# src/app_secure.py - Versió corregida
from flask import Flask, request, jsonify
import sqlite3
import hashlib
import os
import bcrypt

app = Flask(__name__)

# ✅ CORRECCIÓ: Credencials des de variables d'entorn
DB_PASSWORD = os.environ.get('DB_PASSWORD')
API_KEY = os.environ.get('API_KEY')

if not DB_PASSWORD or not API_KEY:
    raise RuntimeError("Les variables d'entorn DB_PASSWORD i API_KEY són obligatòries")

def get_db():
    return sqlite3.connect('/tmp/users.db')

@app.route('/users', methods=['GET'])
def get_users():
    user_id = request.args.get('id', '')

    # ✅ CORRECCIÓ: Prepared Statement
    conn = get_db()
    cursor = conn.cursor()
    cursor.execute("SELECT id, username FROM users WHERE id = ?", (user_id,))
    users = cursor.fetchall()
    return jsonify(users)

@app.route('/login', methods=['POST'])
def login():
    data = request.json
    password = data.get('password', '').encode('utf-8')

    # ✅ CORRECCIÓ: bcrypt en lloc de MD5
    salt = bcrypt.gensalt()
    hashed = bcrypt.hashpw(password, salt)

    return jsonify({'status': 'ok'})  # No retornar el hash!

@app.route('/health', methods=['GET'])
def health():
    return jsonify({'status': 'ok', 'version': '1.0.0'})

if __name__ == '__main__':
    # ✅ CORRECCIÓ: Debug mode desactivat (controlar amb variable d'entorn)
    debug = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true'
    app.run(host='0.0.0.0', port=5000, debug=debug)
# Actualitzar requirements.txt per afegir bcrypt
echo "bcrypt==4.1.3" >> requirements.txt

# Fer commit de la correcció
git add src/app_secure.py requirements.txt
git commit -m "fix: corregir vulnerabilitats detectades pel pipeline de seguretat"
git push

# Observar que ara el pipeline passa tots els checks

Reflexió 2

Compara el pipeline de la versió vulnerable vs la versió corregida. Quants checks fallaven inicialment? Tots han passat després de la correcció? Si algun check continua fallant, per quin motiu?

Part 6: Resum de Resiliència (CA5.5)

## Avaluació de resiliència del pipeline - NOMCOGNOM

### Temps de build
- Build inicial: __ minuts
- Build amb cache: __ minuts
- Reducció: __%

### Cobertura de seguretat
| Control | Eina | Automatic | Manual |
|---------|------|-----------|--------|
| Secrets hardcodejats | GitLeaks | ✅ | - |
| Vulnerabilitats de codi | Semgrep + CodeQL | ✅ | - |
| Dependències vulnerables | Trivy | ✅ | - |
| Configuració insegura contenidor | Trivy | ✅ | - |
| Lògica de negoci | - | ❌ | ✅ |
| DAST (tests dinàmics) | ZAP | ✅ | - |

### Pla de recuperació
Si el pipeline detecta una vulnerabilitat crítica:
1. El push queda bloquejat (no arriba a main)
2. El developer rep una notificació amb la descripció de la vulnerabilitat
3. El developer crea una branca fix/ i corregeix
4. El pipeline re-executa sobre la branca fix/
5. Si passa, es crea un PR cap a main

Informe final

Crea el document informe_pr5032_NOMCOGNOM.md amb:

# Informe PR5032 - Pipeline CI/CD amb Seguretat
**Alumne**: NOMCOGNOM
**Data**: ____________________
**Repositori**: https://github.com/TU_USERNAME/pipeline-segur-NOMCOGNOM

## 1. Arquitectura del pipeline
[Diagrama o descripció dels jobs i l'ordre d'execució]

## 2. Vulnerabilitats detectades
[Taula de vulnerabilitats detectades per Semgrep/CodeQL]

## 3. Correccions implementades
[Per cada vulnerabilitat, el codi original i el codi corregit]

## 4. Captures del pipeline
[Captures de pantalla de GitHub Actions mostrant els checks verds]

## 5. Resiliència
[Taula d'avaluació de resiliència completada]

## 6. Reflexions
[Respostes a les 2 reflexions]

Rúbrica

Vegeu Rúbrica PR5032.