Skip to content

Phase 7 — Pipeline DevSecOps

Pipeline CI/CD complet avec validation de sécurité. Chaque étape est bloquante.


Vue d'ensemble

Commit GitLab
    │
    ├── [Test]      pytest + lint
    ├── [SAST]      Semgrep + Bandit → DefectDojo
    ├── [Build]     Docker build
    ├── [SCAN]      Trivy → DefectDojo
    ├── [Push]      Harbor (harbor.mounik.ovh)
    ├── [DAST]      OWASP ZAP → DefectDojo
    └── [Deploy]    ArgoCD sync → k3s

Chaque étape de sécurité est bloquante : si le SAST échoue, le pipeline s'arrête. Aucune image vulnérable n'atteint la production.


Structure du pipeline

# .gitlab-ci.yml (dépôt applicatif, ex: fastapi)
stages:
  - test
  - sast
  - build
  - scan
  - push
  - dast
  - deploy

variables:
  DOCKER_IMAGE: harbor.mounik.ovh/fastapi/app
  DD_URL: https://defectdojo.mounik.ovh

Étape 1 — Test

test:
  stage: test
  image: python:3.13
  script:
    - pip install -r requirements.txt
    - pytest --cov=. --junitxml=report.xml
    - ruff check .
    - mypy .
  artifacts:
    reports:
      junit: report.xml

Étape 2 — SAST

sast:
  stage: sast
  image: python:3.13
  script:
    # Semgrep
    - pip install semgrep
    - semgrep --config=auto --output=semgrep-report.json --json .
    - |
      curl -X POST "${DD_URL}/api/v2/import-scan/" \
        -H "Authorization: Token ${DD_API_TOKEN}" \
        -H "Content-Type: application/json" \
        -d '{
          "product_name": "fastapi",
          "engagement_name": "Pipeline CI/CD",
          "scan_type": "Semgrep JSON Report",
          "file": "$(cat semgrep-report.json)"
        }'

    # Bandit
    - pip install bandit
    - bandit -r . -f json -o bandit-report.json
    - |
      curl -X POST "${DD_URL}/api/v2/import-scan/" \
        -H "Authorization: Token ${DD_API_TOKEN}" \
        -H "Content-Type: application/json" \
        -d '{
          "product_name": "fastapi",
          "scan_type": "Bandit Report",
          "file": "$(cat bandit-report.json)"
        }'
  artifacts:
    paths:
      - semgrep-report.json
      - bandit-report.json

Étape 3 — Build

build:
  stage: build
  image: docker:27
  services:
    - docker:27-dind
  script:
    - docker build -t ${DOCKER_IMAGE}:${CI_COMMIT_SHORT_SHA} .
    - docker tag ${DOCKER_IMAGE}:${CI_COMMIT_SHORT_SHA} ${DOCKER_IMAGE}:latest

Étape 4 — Scan (Trivy)

scan:
  stage: scan
  image: docker:27
  services:
    - docker:27-dind
  script:
    # Installer Trivy
    - apk add --no-cache curl
    - curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh

    # Scanner l'image
    - trivy image --format json --output trivy-report.json ${DOCKER_IMAGE}:${CI_COMMIT_SHORT_SHA}

    # Envoyer le rapport à DefectDojo
    - |
      curl -X POST "${DD_URL}/api/v2/import-scan/" \
        -H "Authorization: Token ${DD_API_TOKEN}" \
        -H "Content-Type: application/json" \
        -d '{
          "product_name": "fastapi",
          "scan_type": "Trivy Scan",
          "file": "$(cat trivy-report.json)"
        }'

    # Bloquer si vulnérabilités critiques
    - trivy image --severity CRITICAL --exit-code 1 ${DOCKER_IMAGE}:${CI_COMMIT_SHORT_SHA}
  artifacts:
    paths:
      - trivy-report.json

Étape 5 — Push

push:
  stage: push
  image: docker:27
  services:
    - docker:27-dind
  script:
    - docker login harbor.mounik.ovh -u admin -p ${HARBOR_PASSWORD}
    - docker push ${DOCKER_IMAGE}:${CI_COMMIT_SHORT_SHA}
    - docker push ${DOCKER_IMAGE}:latest

Étape 6 — DAST

dast:
  stage: dast
  image: ghcr.io/zaproxy/zaproxy:stable
  script:
    # Lancer ZAP en mode headless
    - zap-full-scan.py -t https://fastapi.mounik.ovh \
        -r zap-report.html \
        -w zap-report.json

    # Envoyer à DefectDojo
    - |
      curl -X POST "${DD_URL}/api/v2/import-scan/" \
        -H "Authorization: Token ${DD_API_TOKEN}" \
        -H "Content-Type: application/json" \
        -d '{
          "product_name": "fastapi",
          "scan_type": "OWASP ZAP Report",
          "file": "$(cat zap-report.json)"
        }'
  artifacts:
    paths:
      - zap-report.html
      - zap-report.json

Étape 7 — Déploiement

deploy:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache curl git
    - curl -LO https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
    - chmod +x argocd-linux-amd64 && mv argocd-linux-amd64 /usr/local/bin/argocd
  script:
    # Cloner le dépôt d'infrastructure (contient les manifests)
    - git clone https://gitlab.com/Mounicou/homelab-proxmox.git infra
    - cd infra/manifests/fastapi

    # Mettre à jour l'image dans le manifest
    - sed -i "s|image:.*|image: ${DOCKER_IMAGE}:${CI_COMMIT_SHORT_SHA}|" deployment.yaml

    # Commit et push du nouveau manifest
    - git config user.email "pipeline@mounik.ovh"
    - git config user.name "GitLab CI Pipeline"
    - git add deployment.yaml
    - git commit -m "chore: update fastapi image to ${CI_COMMIT_SHORT_SHA}" || true
    - git push "https://oauth2:${GITLAB_TOKEN}@gitlab.com/Mounicou/homelab-proxmox.git" HEAD:main

    # Sync ArgoCD
    - argocd login argocd.mounik.ovh --grpc-web --username admin --password ${ARGOCD_PASSWORD}
    - argocd app sync fastapi --grpc-web

Pipeline complet

# .gitlab-ci.yml (structure complète)
stages:
  - test
  - sast
  - build
  - scan
  - push
  - dast
  - deploy

variables:
  DOCKER_IMAGE: harbor.mounik.ovh/fastapi/app
  DD_URL: https://defectdojo.mounik.ovh

# Règles : exécuter sur toutes les branches, déployer uniquement sur main
workflow:
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

include:
  - local: /.gitlab/ci/test.yml
  - local: /.gitlab/ci/sast.yml
  - local: /.gitlab/ci/build.yml
  - local: /.gitlab/ci/scan.yml
  - local: /.gitlab/ci/push.yml
  - local: /.gitlab/ci/dast.yml
  - local: /.gitlab/ci/deploy.yml

Variables CI/CD requises

Variable Source Description
HARBOR_PASSWORD Vault (homelab/harbor) Mot de passe admin Harbor
DD_API_TOKEN Interface DefectDojo Token API pour push des rapports
DD_URL Défini dans GitLab URL de DefectDojo
GITLAB_TOKEN GitLab.com Token pour push des manifests sur homelab-proxmox
ARGOCD_PASSWORD Vault (homelab/argocd) Mot de passe admin ArgoCD
VAULT_ADDR Défini dans GitLab Adresse du serveur Vault
VAULT_TOKEN Vault Token d'accès aux secrets

Vérification

# Lancer un pipeline manuellement depuis GitLab.com
# Vérifier chaque étape dans l'interface CI/CD

# Vérifier les rapports dans DefectDojo
curl -X GET https://defectdojo.mounik.ovh/api/v2/findings/ \
  -H "Authorization: Token ${DD_API_TOKEN}"

Pour aller plus loin