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
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
- Accedir a Docker Hub → Account Settings → Security
- New Access Token → Nom:
github-actions-NOMCOGNOM - Permisos:
Read, Write(per a push d'imatges) - Copiar el token i guardar-lo com a secret
DOCKERHUB_TOKENa 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.