feat: add slide generation script for CESIZen project, covering deployment and security aspects

This commit is contained in:
Azmog
2026-07-09 16:41:44 +02:00
parent 37a1079201
commit 867d744242
14 changed files with 1648 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
name: "✨ Demande d'évolution"
about: "Proposer une nouvelle fonctionnalité ou une amélioration (maintenance évolutive)"
title: "[ÉVOLUTION] "
labels:
- "type/évolution"
body:
- type: markdown
attributes:
value: |
Toute demande d'évolution fait l'objet d'une analyse par le prestataire
(documentation, délai de mise en œuvre, coût associé) avant planification.
- type: dropdown
id: module
attributes:
label: Module concerné
options:
- "Comptes utilisateurs"
- "Informations"
- "Tracker d'émotions"
- "Administration"
- "Nouveau module (Diagnostic, Cohérence cardiaque, Activités de détente)"
- "Transverse (sécurité, RGPD, performance)"
validations:
required: true
- type: textarea
id: besoin
attributes:
label: Besoin / problème à résoudre
description: "En tant que … je souhaite … afin de …"
validations:
required: true
- type: textarea
id: solution
attributes:
label: Solution proposée
validations:
required: false
- type: dropdown
id: priorite
attributes:
label: Priorité souhaitée
options:
- "Haute"
- "Moyenne"
- "Basse"
validations:
required: true
- type: textarea
id: criteres
attributes:
label: Critères d'acceptation
value: |
- [ ]
- [ ]
validations:
required: false
+74
View File
@@ -0,0 +1,74 @@
name: "🐞 Signalement d'incident / anomalie"
about: "Déclarer un dysfonctionnement de l'application (maintenance corrective)"
title: "[INCIDENT] "
labels:
- "type/incident"
body:
- type: markdown
attributes:
value: |
Merci de renseigner ce formulaire le plus précisément possible.
Le délai de prise en compte dépend de la **sévérité** déclarée (voir SLA du contrat).
- type: dropdown
id: severite
attributes:
label: Sévérité
description: "Bloquant = service inutilisable · Majeur = service dégradé avec contournement · Mineur = altération mineure"
options:
- "Bloquant — priorité critique (prise en compte 1 h / correction 3 h)"
- "Bloquant — priorité forte (prise en compte 2 h / correction 6 h)"
- "Majeur (prise en compte 7 h / correction 16 h)"
- "Mineur (prise en compte 1 j / correction 40 h)"
validations:
required: true
- type: dropdown
id: module
attributes:
label: Module concerné
options:
- "Comptes utilisateurs / Authentification"
- "Informations (contenus)"
- "Tracker d'émotions"
- "Administration"
- "Infrastructure / Déploiement"
- "Autre"
validations:
required: true
- type: input
id: environnement
attributes:
label: Environnement
placeholder: "Production (cesizen.cleanows.fr) · web ou mobile · navigateur/OS + version"
validations:
required: true
- type: textarea
id: description
attributes:
label: Description du problème
description: "Ce qui ne fonctionne pas."
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Étapes de reproduction
value: |
1.
2.
3.
validations:
required: true
- type: textarea
id: attendu
attributes:
label: Résultat attendu vs. obtenu
placeholder: "Attendu : … / Obtenu : …"
validations:
required: true
- type: textarea
id: contexte
attributes:
label: Captures d'écran / logs
description: "Capture, message d'erreur, extrait de journal (sans donnée personnelle)."
validations:
required: false
+25
View File
@@ -0,0 +1,25 @@
<!-- Modèle de Pull Request CESIZen — la CI (Gitea Actions) doit être verte avant merge. -->
## Objet
<!-- Décrire brièvement le changement. -->
## Ticket lié
<!-- Ex. : Closes #12 -->
Closes #
## Type de changement
- [ ] 🐞 Correctif (maintenance corrective)
- [ ] ✨ Évolution (maintenance évolutive)
- [ ] ♻️ Refactorisation / dette technique
- [ ] 🔒 Sécurité
- [ ] 📖 Documentation
## Vérifications
- [ ] Les tests automatisés passent en local (`php artisan test`, `npm test`)
- [ ] Le style de code est respecté (`vendor/bin/pint`, `npm run lint`)
- [ ] La CI Gitea Actions est verte
- [ ] Pas de secret ni de donnée personnelle commitée
- [ ] Documentation mise à jour si nécessaire
## Impact sécurité / données personnelles
<!-- Le changement touche-t-il à l'authentification, aux données de santé, au RGPD ? -->
+85
View File
@@ -0,0 +1,85 @@
# ─────────────────────────────────────────────────────────────────────────
# Intégration continue CESIZen — Gitea Actions
# Déclenchée à chaque push et pull request. Lance les 29 tests automatisés
# (backend PHPUnit + frontend Vitest + mobile Jest) et le lint.
# Un déploiement Coolify est déclenché par webhook après un merge sur main
# (voir docker-compose.prod.yaml + MIGRATION_GITEA.md).
# ─────────────────────────────────────────────────────────────────────────
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
# ── Backend Laravel : PHPUnit sur base SQLite en mémoire (cf. backend/phpunit.xml) ──
backend:
name: Backend · PHPUnit + Pint
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v4
- name: Installer PHP 8.4
uses: shivammathur/setup-php@v2
with:
php-version: "8.4"
extensions: pdo, pdo_sqlite, bcmath, mbstring
coverage: none
- name: Cache des dépendances Composer
uses: actions/cache@v4
with:
path: backend/vendor
key: composer-${{ hashFiles('backend/composer.lock') }}
- name: Installer les dépendances
run: composer install --no-interaction --prefer-dist --no-progress
- name: Vérifier le style (Laravel Pint, PSR-12)
run: vendor/bin/pint --test
- name: Lancer les tests (Unit + Feature)
run: php artisan test
# ── Frontend web Next.js : ESLint + Vitest ──
frontend:
name: Frontend · ESLint + Vitest
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
cache-dependency-path: frontend/package-lock.json
- run: npm ci
- name: Lint
run: npm run lint
- name: Tests unitaires
run: npm test
# ── Mobile Expo / React Native : Jest ──
mobile:
name: Mobile · Jest
runs-on: ubuntu-latest
defaults:
run:
working-directory: mobile
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
cache-dependency-path: mobile/package-lock.json
- run: npm ci
- name: Tests unitaires
run: npm test
Binary file not shown.
Binary file not shown.
+97
View File
@@ -0,0 +1,97 @@
# Migration vers Gitea & mise en place des outils (Bloc 3)
Ce guide décrit la mise en place de **Gitea** comme plateforme unique de **versioning**, de
**gestion des tickets** (maintenance) et d'**intégration continue** (Gitea Actions), ainsi que le
raccordement au déploiement continu **Coolify**. Il sert de support à la démonstration de soutenance.
> Instance utilisée : Gitea auto-hébergée. Dépôt cible : `https://<instance-gitea>/adam/cesizen`.
---
## 1. Migrer le dépôt GitHub → Gitea
### Option A — Migration assistée (recommandée, conserve issues & historique)
1. Se connecter à Gitea → **+ → Nouvelle migration → GitHub**.
2. URL source : `https://github.com/AzmogEx/CesiZen.git`.
3. Cocher : *Éléments à migrer* → dépôt Git, **Issues**, **Pull requests**, **Releases**, **Labels**, **Milestones**.
4. Renseigner un jeton d'accès GitHub (lecture) si le dépôt est privé. Lancer.
### Option B — Push manuel (miroir du code seul)
```bash
cd /chemin/vers/CesiZen
git remote add gitea https://<instance-gitea>/adam/cesizen.git
git push gitea --all # toutes les branches
git push gitea --tags # tous les tags/releases
# Faire de Gitea le dépôt de référence :
git remote set-url origin https://<instance-gitea>/adam/cesizen.git
```
Vérifier ensuite que `.gitea/` (workflows + gabarits d'issues) est bien présent à la racine.
---
## 2. Activer l'intégration continue (Gitea Actions)
1. **Côté serveur Gitea** : Actions doit être activé (`app.ini` → `[actions] ENABLED = true`).
2. **Enregistrer un runner** (une fois) :
```bash
# Récupérer le jeton dans Gitea → Administration → Actions → Runners
act_runner register --instance https://<instance-gitea> --token <TOKEN> \
--name cesizen-runner --labels ubuntu-latest:docker://node:20-bookworm
act_runner daemon
```
3. Le workflow **`.gitea/workflows/ci.yml`** se déclenche alors à chaque `push` / `pull request` et
exécute les 3 jobs : **backend** (PHPUnit + Pint), **frontend** (ESLint + Vitest), **mobile** (Jest).
4. Une pastille verte/rouge s'affiche sur chaque commit et chaque PR (démo).
---
## 3. Configurer le ticketing (maintenance)
### Labels à créer (Issues → Labels → « Créer un label »)
| Label | Couleur | Usage |
|---|---|---|
| `type/incident` | rouge | Anomalie (maintenance corrective) — posé automatiquement par le gabarit |
| `type/évolution` | bleu | Nouvelle fonctionnalité (maintenance évolutive) |
| `gravité/bloquant` | #C8102E | Service inutilisable |
| `gravité/majeur` | #E4801C | Service dégradé, contournement possible |
| `gravité/mineur` | #F0C808 | Altération mineure |
| `priorité/critique` · `priorité/forte` · `priorité/normale` | dégradé | Ordonnancement |
| `statut/à-qualifier` · `statut/en-cours` · `statut/en-revue` · `statut/résolu` | gris→vert | Suivi Kanban |
| `module/comptes` · `module/informations` · `module/tracker` · `module/infra` | vert | Domaine |
Les **gabarits d'issues** sont déjà versionnés dans `.gitea/issue_template/` (`incident.yaml`,
`evolution.yaml`) : ils apparaissent automatiquement au clic sur **« Nouvelle issue »**.
### Milestones (= versions)
Créer un jalon par version livrée : `v1.0`, `v1.1`, … Chaque issue/PR est rattachée à un jalon → suivi
de l'avancement par version.
### Tableau de bord Kanban (pilotage)
Onglet **Projects → Nouveau projet** (type *Board*) avec les colonnes :
`À qualifier` → `À faire` → `En cours` → `En revue (PR)` → `Résolu`.
Les issues glissent d'une colonne à l'autre ; les labels de statut sont synchronisés.
---
## 4. Raccorder le déploiement continu (Coolify)
Coolify build les images depuis le dépôt Git et redéploie à chaque nouveau commit sur `main` :
1. Dans Coolify → application CESIZen → **Source** : pointer sur le dépôt Gitea (`main`).
2. Activer le **webhook de déploiement** : Coolify fournit une URL de type
`https://<coolify>/api/v1/deploy?uuid=…&force=false`.
3. Dans Gitea → dépôt → **Paramètres → Webhooks → Ajouter** : coller l'URL, événement *Push*, branche `main`.
4. Résultat : `merge sur main` → CI verte → webhook → Coolify build + déploie en production
(`cesizen.cleanows.fr` / `api.cesizen.cleanows.fr`), avec migrations, seed et génération des
secrets automatisés par `backend/docker/entrypoint.sh`.
---
## 5. Script de démonstration (soutenance)
1. **Versioning** : montrer l'arbre des commits Gitea, les branches, un tag de version.
2. **CI** : ouvrir une PR triviale → montrer les 3 jobs qui tournent → pastille verte.
3. **Ticketing** : créer une issue via le gabarit *incident*, la qualifier (labels + jalon),
la déplacer sur le board Kanban.
4. **CD** : merger la PR → montrer le déploiement Coolify déclenché → rafraîchir le site en ligne.
+62
View File
@@ -0,0 +1,62 @@
# Plan d'oral — Soutenance CESIZen · Activité 3
**Apprenant :** Adam Marzuk · **Bloc :** INFCDAAL3 — Déployer et sécuriser les applications informatiques
**Format :** 20 min de présentation + ~10 min de questions/réponses
**Support :** `CESIZen_Soutenance_Bloc3.pptx` (14 slides) + **démo live des outils** (Gitea + Coolify)
**Dossier :** `Dossier_Bloc3_CESIZen.docx` · **Guide outils :** `MIGRATION_GITEA.md`
**En ligne :** https://cesizen.cleanows.fr · API https://api.cesizen.cleanows.fr
> Objectif du jury : évaluer le **plan de déploiement**, le **plan de maintenance** et le **plan de sécurisation**, ainsi que la **démonstration** des outils de versioning et de ticketing réellement configurés.
---
## Déroulé minuté (20 min)
| Temps | Slide(s) | Sujet | Messages clés à dire |
|---|---|---|---|
| **0:00 – 1:30** | 1 — 2 | **Introduction** | Se présenter. Rappeler CESIZen (plateforme de santé mentale, Ministère). Annoncer que l'Activité 3 porte sur le **déploiement** et la **sécurisation**, avec 3 livrables + une démo d'outils. Présenter le sommaire. |
| **1:30 – 3:00** | 3 | **Contexte & périmètre** | Situer après Act. 1 (conception) et Act. 2 (dev/test). Prototype = API Laravel + web Next.js + mobile Expo + PostgreSQL. Les 3 livrables : plan de déploiement, de maintenance, de sécurisation. |
| **3:00 – 4:30** | 4 | **Architecture technique** | Architecture **découplée** : 1 API REST, 2 clients. Une seule logique métier → testabilité, évolutivité. Conteneurisation Docker = environnements reproductibles. |
| **4:30 – 7:00** | 5 — 6 | **Plan de déploiement + CI/CD** | Les **3 environnements** (dev local / test = CI / prod = Coolify). Détailler le schéma. Puis **Gitea** (versioning souverain) + **Gitea Actions** (29 tests à chaque push) + **Coolify** (déploiement continu sur merge, HTTPS, migrations auto). Insister : « la prod est **réellement en ligne**. » |
| **7:00 – 9:30** | 7 — 8 | **Plan de maintenance** | Ticketing **Gitea Issues** : gabarits incident/évolution, labels, jalons, Kanban. Cycle de vie d'un ticket + **SLA du contrat** (bloquant/majeur/mineur). Maintenance évolutive (analyse : doc, délai, coût). **Veille** : sécurité (CVE/CERT-FR), dépendances (audit en CI), réglementation (CNIL). |
| **9:30 – 13:00** | 9 — 10 — 11 | **Plan de sécurisation** ⭐ | (Partie la plus notée — 8 pts.) Vulnérabilités **OWASP** avec mesures en place. **Matrice de criticité** (gravité × probabilité) → R1 fuite de données, R7 chiffrement au repos, R2 injection prioritaires. Actions **préventives / correctives** + chiffrement (TLS, bcrypt, AES, JWT). Assumer les axes d'amélioration (chiffrement colonne, headers de sécurité). |
| **13:00 – 14:30** | 12 — 13 | **RGPD & gestion de crise** | Données de santé = **sensibles (art. 9)**. Consentement, droits (export/anonymisation), soft delete, audit immuable, hébergement UE/HDS. **Escalade** N1→N4, notification **CNIL < 72 h**. |
| **14:30 – 18:30** | démo | **🎬 Démo des outils** | Sur Gitea : commits/branches/tags → ouvrir une **PR** → 3 jobs de CI **verts** → créer une **issue** (gabarit incident) + labels + Kanban → **merge** → montrer **Coolify** qui déploie → rafraîchir le site en ligne. 🛟 Si un outil plante : montrer les captures du dossier / `MIGRATION_GITEA.md`. |
| **18:30 – 20:00** | 14 | **Conclusion** | Synthèse : chaîne de déploiement complète et en prod, maintenance cadrée, sécurité par les risques + RGPD. Remercier, ouvrir les questions. |
**Repère de rythme :** checkpoint à **14:30** (début démo). Si retard : raccourcir la maintenance (garder SLA + veille) et garder 4 min de démo + 1 min de conclusion. ⚠️ **Prépare les onglets AVANT d'entrer** : Gitea (dépôt + une PR prête), Coolify (tableau de bord), le site en ligne.
---
## Préparation aux questions / réponses (~10 min)
### Déploiement
**Q. Pourquoi trois environnements alors qu'un seul est réellement déployé ?**
> Le sujet demande de décrire dev/test/prod et d'en déployer **un** réellement : c'est la production, en ligne sur Coolify. Le dev tourne en local (docker-compose), le test correspond au runner de CI (base SQLite en mémoire). Chacun a un rôle distinct dans le cycle de vie.
**Q. Qu'est-ce qui est automatisé exactement ?**
> Deux choses : l'**intégration continue** (Gitea Actions lance les 29 tests + le lint à chaque push) et le **déploiement continu** (un merge sur main déclenche, via webhook, le build des images et le redéploiement Coolify — migrations, seed et génération des secrets étant faits par l'entrypoint au démarrage).
**Q. Comment gérez-vous les secrets et le rollback ?**
> Les secrets de prod (APP_KEY, JWT_SECRET) ne sont **pas versionnés** : ils sont générés au premier démarrage par l'entrypoint. Le rollback consiste à redéployer le tag de la version précédente depuis Coolify.
### Maintenance
**Q. Pourquoi Gitea plutôt que GitHub ou Jira ?**
> Gitea réunit dépôt Git, ticketing et CI/CD dans un outil **souverain et auto-hébergé**, cohérent avec un projet ministériel (données maîtrisées, pas de dépendance SaaS hors UE), léger et gratuit.
**Q. Comment respectez-vous les délais de correction ?**
> La sévérité déclarée dans le gabarit d'issue déclenche le SLA correspondant (bloquant critique = prise en compte 1 h / correction 3 h, etc.). Le Kanban et les jalons donnent au Ministère une visibilité temps réel.
### Sécurisation
**Q. Quelles sont les vraies faiblesses de sécurité aujourd'hui ?**
> Honnêtement : pas encore de chiffrement applicatif au repos des notes de santé, pas d'en-têtes HSTS/CSP, et une politique de mot de passe limitée à 8 caractères. Ce sont mes actions correctives prioritaires — elles figurent dans le plan (R7 notamment).
**Q. Comment calculez-vous la criticité ?**
> Criticité = gravité × probabilité, sur une échelle de 1 à 25. Ça hiérarchise les actions : R7 (chiffrement au repos, 12) et R2 (injection, 9) passent avant R8 (erreur de déploiement, 2).
**Q. Que se passe-t-il en cas de fuite de données de santé ?**
> Escalade N3 : le chef de projet et le DPO sont mobilisés, notification à la **CNIL sous 72 h** (art. 33 RGPD), information des personnes concernées, isolation et préservation des preuves, puis retour d'expérience.
### Général
**Q. Qu'est-ce qui prouve que ce n'est pas que de la théorie ?**
> La production est **en ligne** (cesizen.cleanows.fr), les outils sont configurés (je les montre en démo), les 29 tests tournent en CI, et l'audit de sécurité est réellement alimenté dans l'application.
Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

+219
View File
@@ -0,0 +1,219 @@
#!/usr/bin/env python3
"""
Génère les schémas du dossier Bloc 3 (Déployer et sécuriser) — CESIZen.
Sortie : ./assets_bloc3/*.png (réutilisés dans le .docx et le .pptx)
"""
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
import matplotlib.font_manager as fm
# ─── Charte CESIZen ───────────────────────────────────────────────
GREEN = "#06C656"
GREEN_D = "#049641"
BLUE = "#004682"
BLUE_L = "#E6F0FA"
GREY_D = "#232323"
GREY = "#6E6E6E"
WHITE = "#FFFFFF"
INK = "#23232D"
RED = "#C8102E"
ORANGE = "#E4801C"
YELLOW_A = "#F0C808"
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "assets_bloc3")
os.makedirs(OUT, exist_ok=True)
plt.rcParams["font.family"] = "DejaVu Sans"
def box(ax, x, y, w, h, text, fc, ec=None, tc="white", fs=10, bold=True, radius=0.02):
ec = ec or fc
p = FancyBboxPatch((x, y), w, h, boxstyle=f"round,pad=0.005,rounding_size={radius}",
fc=fc, ec=ec, lw=1.4, mutation_aspect=1)
ax.add_patch(p)
ax.text(x + w / 2, y + h / 2, text, ha="center", va="center",
color=tc, fontsize=fs, fontweight="bold" if bold else "normal", zorder=5)
def arrow(ax, x1, y1, x2, y2, color=GREY_D, style="-|>", lw=1.8, ls="-"):
a = FancyArrowPatch((x1, y1), (x2, y2), arrowstyle=style, mutation_scale=16,
color=color, lw=lw, linestyle=ls, zorder=1)
ax.add_patch(a)
# ══════════════════════════════════════════════════════════════════
# 1. ARCHITECTURE DE DÉPLOIEMENT + CHAÎNE CI/CD
# ══════════════════════════════════════════════════════════════════
def diagram_deploiement():
fig, ax = plt.subplots(figsize=(12, 6.6))
ax.set_xlim(0, 12); ax.set_ylim(0, 6.6); ax.axis("off")
ax.text(6, 6.35, "Chaîne de déploiement CESIZen — du poste développeur à la production",
ha="center", fontsize=13, fontweight="bold", color=BLUE)
# --- Bandeau des 3 environnements ---
envs = [
(0.3, "DÉVELOPPEMENT", "Poste local", GREEN,
["Docker Compose (dev)", "PostgreSQL 16 + Redis 7", "Hot-reload back & front", "APP_DEBUG=true"]),
(4.15, "TEST / INTÉGRATION", "Gitea Actions (CI)", ORANGE,
["Runner conteneurisé", "Base SQLite en mémoire", "29 tests automatisés", "Lint (Pint / ESLint)"]),
(8.0, "PRODUCTION", "Coolify (PaaS)", BLUE,
["Docker Compose (prod)", "HTTPS / TLS (reverse proxy)", "Secrets générés hors dépôt", "Health checks + restart"]),
]
for x, titre, sous, col, items in envs:
box(ax, x, 3.4, 3.55, 2.2, "", BLUE_L, ec=col, tc=INK)
box(ax, x, 5.0, 3.55, 0.6, titre, col, fs=11)
ax.text(x + 1.775, 4.75, sous, ha="center", fontsize=9.5, style="italic", color=GREY_D)
for i, it in enumerate(items):
ax.text(x + 0.15, 4.45 - i * 0.27, "• " + it, ha="left", fontsize=8.5, color=INK)
arrow(ax, 3.95, 4.5, 4.1, 4.5, color=GREY_D)
arrow(ax, 7.8, 4.5, 7.95, 4.5, color=GREY_D)
# --- Chaîne CI/CD (bas) ---
ax.text(6, 2.9, "Flux d'intégration & de livraison continues", ha="center",
fontsize=11, fontweight="bold", color=GREEN_D)
steps = [
(0.3, "git push\n(Gitea)", GREY_D),
(2.55, "Gitea Actions\ndéclenché", ORANGE),
(4.8, "Build + Tests\n(29 tests)", ORANGE),
(7.05, "Webhook\nCoolify", BLUE),
(9.3, "Build images\n+ déploiement", BLUE),
]
for x, t, col in steps:
box(ax, x, 1.3, 2.05, 1.05, t, col, fs=9.5)
for i in range(len(steps) - 1):
arrow(ax, steps[i][0] + 2.05, 1.82, steps[i + 1][0], 1.82, color=GREEN_D, lw=2)
box(ax, 9.3, 0.15, 2.05, 0.8, "cesizen.cleanows.fr\napi.cesizen.cleanows.fr", GREEN, fs=8)
arrow(ax, 10.32, 1.3, 10.32, 0.95, color=GREEN_D, lw=2)
ax.text(0.3, 0.55, "Rollback : redéploiement de la version\nGit précédente (tag) en 1 clic Coolify.",
ha="left", fontsize=8, color=GREY, style="italic")
plt.tight_layout()
fig.savefig(os.path.join(OUT, "archi_deploiement.png"), dpi=170, bbox_inches="tight")
plt.close(fig)
# ══════════════════════════════════════════════════════════════════
# 2. MATRICE DE RISQUES (criticité = gravité × probabilité)
# ══════════════════════════════════════════════════════════════════
def diagram_matrice_risques():
fig, ax = plt.subplots(figsize=(11, 6.8))
n = 5
for g in range(n): # gravité (y)
for p in range(n): # probabilité (x)
crit = (g + 1) * (p + 1)
if crit <= 4:
c = "#2E9E4F"
elif crit <= 9:
c = "#F0C808"
elif crit <= 14:
c = "#E4801C"
else:
c = "#C8102E"
ax.add_patch(plt.Rectangle((p, g), 1, 1, fc=c, ec="white", lw=2, alpha=0.9))
ax.text(p + 0.5, g + 0.5, str(crit), ha="center", va="center",
fontsize=9, color="white", fontweight="bold", alpha=0.6)
# Positionnement des risques réels du projet (P sur 1..5, G sur 1..5)
risques = [
("R1", 2, 4), # Fuite données de santé (Art.9)
("R2", 3, 3), # Injection / XSS
("R3", 2, 3), # Compromission compte admin
("R4", 3, 2), # DDoS / indisponibilité
("R5", 2, 2), # Perte de données (sauvegarde)
("R6", 4, 2), # Dépendances vulnérables
("R7", 3, 4), # Absence de chiffrement au repos
("R8", 2, 1), # Erreur humaine déploiement
]
for nom, p, g in risques:
ax.plot(p - 0.5, g - 0.5, "o", ms=22, mfc="white", mec=INK, mew=1.8, zorder=6)
ax.text(p - 0.5, g - 0.5, nom, ha="center", va="center", fontsize=8.5,
fontweight="bold", color=INK, zorder=7)
ax.set_xlim(0, n); ax.set_ylim(0, n)
ax.set_xticks([i + 0.5 for i in range(n)])
ax.set_yticks([i + 0.5 for i in range(n)])
ax.set_xticklabels(["Très faible", "Faible", "Moyenne", "Forte", "Très forte"], fontsize=9)
ax.set_yticklabels(["Mineure", "Modérée", "Grave", "Critique", "Catastroph."], fontsize=9)
ax.set_xlabel("Probabilité →", fontsize=11, fontweight="bold", color=BLUE)
ax.set_ylabel("Gravité →", fontsize=11, fontweight="bold", color=BLUE)
ax.set_title("Matrice de criticité des risques CESIZen (criticité = gravité × probabilité)",
fontsize=12.5, fontweight="bold", color=BLUE, pad=12)
for s in ax.spines.values():
s.set_visible(False)
ax.tick_params(length=0)
legende = ("R1 Fuite de données de santé R2 Injection SQL/XSS R3 Compromission compte admin "
"R4 DDoS / indisponibilité\nR5 Perte de données R6 Dépendances vulnérables "
"R7 Absence de chiffrement au repos R8 Erreur humaine de déploiement")
fig.text(0.5, -0.02, legende, ha="center", fontsize=8.3, color=GREY_D)
plt.tight_layout()
fig.savefig(os.path.join(OUT, "matrice_risques.png"), dpi=170, bbox_inches="tight")
plt.close(fig)
# ══════════════════════════════════════════════════════════════════
# 3. CYCLE DE VIE D'UN TICKET (maintenance corrective)
# ══════════════════════════════════════════════════════════════════
def diagram_ticket():
fig, ax = plt.subplots(figsize=(12, 5.2))
ax.set_xlim(0, 12); ax.set_ylim(0, 5.2); ax.axis("off")
ax.text(6, 4.95, "Cycle de vie d'un ticket sur Gitea (prestataire ↔ Ministère)",
ha="center", fontsize=13, fontweight="bold", color=BLUE)
etapes = [
("1. Signalement\nIssue créée\n(gabarit incident)", GREY_D),
("2. Qualification\nLabels : gravité,\npriorité, module", BLUE),
("3. Priorisation\nSLA appliqué\n(bloquant/majeur/mineur)", ORANGE),
("4. Correction\nbranche fix/*\n+ tests", GREEN_D),
("5. Revue & CI\nPull Request\n29 tests verts", GREEN_D),
("6. Livraison\nmerge → Coolify\ndéploie", GREEN),
("7. Clôture\nvalidation client\nIssue fermée", GREEN),
]
x = 0.15
w = 1.58
gap = 0.11
y = 2.5
for i, (t, col) in enumerate(etapes):
box(ax, x, y, w, 1.3, t, col, fs=8.3)
if i < len(etapes) - 1:
arrow(ax, x + w, y + 0.65, x + w + gap, y + 0.65, color=GREY_D, lw=1.6)
x += w + gap
# Bandeau SLA
ax.text(6, 1.9, "Délais contractuels (jours ouvrés) — repris du cahier des charges",
ha="center", fontsize=10, fontweight="bold", color=GREEN_D)
sla = [
("Bloquant critique", "Prise en compte 1 h", "Correction 3 h", RED),
("Bloquant fort", "Prise en compte 2 h", "Correction 6 h", ORANGE),
("Majeur", "Prise en compte 7 h", "Correction 16 h", YELLOW_A),
("Mineur (par lots)", "Prise en compte 1 j", "Correction 40 h", GREEN),
]
bx = 0.3
bw = 2.85
for titre, d1, d2, col in sla:
box(ax, bx, 0.35, bw, 1.15, "", BLUE_L, ec=col, tc=INK)
box(ax, bx, 1.12, bw, 0.38, titre, col, fs=9, tc="white")
ax.text(bx + bw / 2, 0.92, d1, ha="center", fontsize=8.3, color=INK)
ax.text(bx + bw / 2, 0.62, d2, ha="center", fontsize=8.3, color=INK)
bx += bw + 0.1
plt.tight_layout()
fig.savefig(os.path.join(OUT, "cycle_ticket.png"), dpi=170, bbox_inches="tight")
plt.close(fig)
if __name__ == "__main__":
diagram_deploiement()
diagram_matrice_risques()
diagram_ticket()
print("Schémas Bloc 3 générés dans :", OUT)
for f in sorted(os.listdir(OUT)):
print(" -", f)
+613
View File
@@ -0,0 +1,613 @@
#!/usr/bin/env python3
"""
Génère le dossier écrit du Bloc 3 CESIZen — « Déployer et sécuriser les applications
informatiques » (INFCDAAL3) au format Word éditable.
Sortie : ./Dossier_Bloc3_CESIZen.docx
Schémas : ./assets_bloc3/*.png (générés par generate_diagrams_bloc3.py)
"""
import os
from docx import Document
from docx.shared import Pt, Inches, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.enum.section import WD_SECTION
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
# ─── Charte CESIZen ───────────────────────────────────────────────
BLUE = RGBColor(0x00, 0x46, 0x82)
GREEN = RGBColor(0x04, 0x96, 0x41)
GREEN_V = RGBColor(0x06, 0xC6, 0x56)
INK = RGBColor(0x23, 0x23, 0x2D)
GREY = RGBColor(0x6E, 0x6E, 0x6E)
RED = RGBColor(0xC8, 0x10, 0x2E)
ORANGE = RGBColor(0xE4, 0x80, 0x1C)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
BLUE_L = "E6F0FA"
GREY_L = "F2F2F2"
HERE = os.path.dirname(os.path.abspath(__file__))
ASSETS = os.path.join(HERE, "assets_bloc3")
OUT = os.path.join(HERE, "Dossier_Bloc3_CESIZen.docx")
doc = Document()
# ─── Styles de base ───────────────────────────────────────────────
normal = doc.styles["Normal"]
normal.font.name = "Calibri"
normal.font.size = Pt(10.5)
normal.font.color.rgb = INK
normal.paragraph_format.space_after = Pt(6)
normal.paragraph_format.line_spacing = 1.12
for name, size, color in [("Heading 1", 17, BLUE), ("Heading 2", 13.5, GREEN),
("Heading 3", 11.5, BLUE)]:
st = doc.styles[name]
st.font.name = "Calibri"
st.font.size = Pt(size)
st.font.color.rgb = color
st.font.bold = True
st.paragraph_format.space_before = Pt(12)
st.paragraph_format.space_after = Pt(4)
st.paragraph_format.keep_with_next = True
def _shade(cell, hexcolor):
tcPr = cell._tc.get_or_add_tcPr()
shd = OxmlElement("w:shd")
shd.set(qn("w:val"), "clear")
shd.set(qn("w:fill"), hexcolor)
tcPr.append(shd)
def _set_cell_text(cell, text, bold=False, color=INK, size=9.5, white=False,
align=WD_ALIGN_PARAGRAPH.LEFT):
cell.text = ""
p = cell.paragraphs[0]
p.alignment = align
p.paragraph_format.space_after = Pt(1)
p.paragraph_format.space_before = Pt(1)
r = p.add_run(text)
r.font.size = Pt(size)
r.font.bold = bold
r.font.color.rgb = WHITE if white else color
r.font.name = "Calibri"
def h1(text):
doc.add_paragraph(text, style="Heading 1")
def h2(text):
doc.add_paragraph(text, style="Heading 2")
def h3(text):
doc.add_paragraph(text, style="Heading 3")
def para(text, size=10.5, italic=False, color=INK, align=WD_ALIGN_PARAGRAPH.JUSTIFY,
bold=False, after=6):
p = doc.add_paragraph()
p.alignment = align
p.paragraph_format.space_after = Pt(after)
r = p.add_run(text)
r.font.size = Pt(size)
r.italic = italic
r.bold = bold
r.font.color.rgb = color
return p
def rich(segments, align=WD_ALIGN_PARAGRAPH.JUSTIFY, after=6, size=10.5):
"""segments = list of (text, bold)"""
p = doc.add_paragraph()
p.alignment = align
p.paragraph_format.space_after = Pt(after)
for text, bold in segments:
r = p.add_run(text)
r.font.size = Pt(size)
r.bold = bold
r.font.color.rgb = INK
return p
def bullet(text, level=0, bold_prefix=None):
p = doc.add_paragraph(style="List Bullet" if level == 0 else "List Bullet 2")
p.paragraph_format.space_after = Pt(3)
if bold_prefix:
r = p.add_run(bold_prefix)
r.font.bold = True
r.font.size = Pt(10.5)
r.font.color.rgb = INK
r = p.add_run(text)
r.font.size = Pt(10.5)
r.font.color.rgb = INK
return p
def image(name, width=6.3, caption=None):
path = os.path.join(ASSETS, name)
doc.add_picture(path, width=Inches(width))
doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER
if caption:
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run("Figure — " + caption)
r.font.size = Pt(9)
r.italic = True
r.font.color.rgb = GREY
p.paragraph_format.space_after = Pt(8)
def callout(title, text, color=BLUE, fill=BLUE_L):
"""Encadré une-cellule."""
t = doc.add_table(rows=1, cols=1)
t.alignment = WD_TABLE_ALIGNMENT.CENTER
cell = t.rows[0].cells[0]
_shade(cell, fill)
cell.width = Inches(6.4)
p = cell.paragraphs[0]
p.paragraph_format.space_after = Pt(2)
r = p.add_run(title + " ")
r.font.bold = True
r.font.size = Pt(10)
r.font.color.rgb = color
r2 = p.add_run(text)
r2.font.size = Pt(9.5)
r2.font.color.rgb = INK
doc.add_paragraph().paragraph_format.space_after = Pt(4)
return t
def table(headers, rows, widths=None, header_fill="004682", zebra=True, size=9.3,
fonts_bold_col0=False):
t = doc.add_table(rows=1, cols=len(headers))
t.style = "Table Grid"
t.alignment = WD_TABLE_ALIGNMENT.CENTER
hdr = t.rows[0].cells
for i, htxt in enumerate(headers):
_set_cell_text(hdr[i], htxt, bold=True, white=True, size=size,
align=WD_ALIGN_PARAGRAPH.CENTER)
_shade(hdr[i], header_fill)
for ri, row in enumerate(rows):
cells = t.add_row().cells
for ci, val in enumerate(row):
_set_cell_text(cells[ci], str(val), size=size,
bold=(fonts_bold_col0 and ci == 0))
if zebra and ri % 2 == 1:
_shade(cells[ci], GREY_L)
if widths:
for row in t.rows:
for i, w in enumerate(widths):
row.cells[i].width = Inches(w)
doc.add_paragraph().paragraph_format.space_after = Pt(2)
return t
def page_break():
doc.add_page_break()
def add_toc():
p = doc.add_paragraph()
run = p.add_run()
fldSimple = OxmlElement("w:fldSimple")
fldSimple.set(qn("w:instr"), r'TOC \o "1-3" \h \z \u')
r = OxmlElement("w:r")
t = OxmlElement("w:t")
t.text = "Faites un clic droit ici puis « Mettre à jour les champs » pour générer le sommaire."
r.append(t)
fldSimple.append(r)
p._p.append(fldSimple)
# ══════════════════════════════════════════════════════════════════
# PAGE DE GARDE
# ══════════════════════════════════════════════════════════════════
for _ in range(3):
doc.add_paragraph()
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run("CESIZen"); r.font.size = Pt(46); r.font.bold = True; r.font.color.rgb = GREEN_V
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run("L'application de votre santé mentale"); r.font.size = Pt(16); r.font.color.rgb = GREY
doc.add_paragraph()
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run("Dossier — Activité 3"); r.font.size = Pt(22); r.font.bold = True; r.font.color.rgb = BLUE
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run("Déployer et sécuriser les applications informatiques")
r.font.size = Pt(15); r.italic = True; r.font.color.rgb = INK
for _ in range(6):
doc.add_paragraph()
for line, sz, col, bold in [
("Plan de déploiement · Plan de maintenance · Plan de sécurisation", 12, GREEN, True),
("", 6, INK, False),
("Adam Marzuk — CESI École d'Ingénieurs", 12, INK, False),
("Titre Concepteur Développeur d'Applications (CDA)", 11, GREY, False),
("Bloc INFCDAAL3 — Commanditaire : Ministère des Solidarités et de la Santé", 11, GREY, False),
]:
p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER
if line:
r = p.add_run(line); r.font.size = Pt(sz); r.font.color.rgb = col; r.font.bold = bold
page_break()
# ══════════════════════════════════════════════════════════════════
# SOMMAIRE
# ══════════════════════════════════════════════════════════════════
h1("Sommaire")
add_toc()
page_break()
# ══════════════════════════════════════════════════════════════════
# 1. INTRODUCTION
# ══════════════════════════════════════════════════════════════════
h1("1. Introduction")
para("CESIZen est une plateforme grand public de santé mentale simulée pour le Ministère des "
"Solidarités et de la Santé. Elle aide chacun à mieux comprendre sa santé mentale et à agir sur "
"son stress à travers trois modules : la gestion des comptes utilisateurs, la diffusion "
"d'informations et un tracker d'émotions. La solution repose sur une API REST Laravel, un site "
"web Next.js et une application mobile React Native, adossés à une base PostgreSQL.")
para("Après la conception (Activité 1) et le développement/test du prototype (Activité 2), la présente "
"Activité 3 traite du déploiement et de la sécurisation de l'application. Ce dossier présente les "
"trois livrables attendus :")
bullet("l'architecture des environnements, le versioning et les automatisations de déploiement ;",
bold_prefix="Un plan de déploiement — ")
bullet("un outil de ticketing configuré et la méthodologie de traitement des incidents et évolutions ;",
bold_prefix="Un plan de maintenance — ")
bullet("l'analyse des vulnérabilités, les actions préventives/correctives, la protection des données "
"personnelles et la gestion de crise.", bold_prefix="Un plan de sécurisation — ")
para("Les outils de versioning, d'intégration continue et de ticketing retenus (Gitea et Coolify) sont "
"réellement configurés et font l'objet d'une démonstration lors de la soutenance.")
callout("Périmètre.", "Un environnement de déploiement (la production) est réellement mis en place et "
"accessible en ligne sur cesizen.cleanows.fr. Les trois modules du prototype (Comptes, "
"Informations, Tracker d'émotions) y sont déployés.")
# ══════════════════════════════════════════════════════════════════
# 2. PLAN DE DÉPLOIEMENT
# ══════════════════════════════════════════════════════════════════
h1("2. Plan de déploiement")
h2("2.1 Architecture technique de la solution")
para("L'application suit une architecture découplée : une API REST centrale expose la logique métier et "
"les données, consommée par deux clients distincts (web et mobile). Ce découpage garantit la "
"non-duplication de la logique et facilite l'ajout de nouveaux clients.")
table(
["Composant", "Technologie", "Rôle"],
[
["API / Back-end", "Laravel 12 (PHP 8.4)", "API REST JSON, authentification JWT, logique métier"],
["Base de données", "PostgreSQL 16", "Persistance ACID des données"],
["Cache / file (dev)", "Redis 7", "Cache, sessions, files d'attente (environnement de dev)"],
["Front-end web", "Next.js 16 / React 19", "Site public et back-office, design system DSFR de l'État"],
["Application mobile", "React Native / Expo 54", "Application iOS & Android (Mobile First)"],
["Conteneurisation", "Docker / Docker Compose", "Isolation et reproductibilité des environnements"],
["Plateforme de déploiement", "Coolify (PaaS auto-hébergé)", "Build, orchestration, HTTPS, supervision"],
],
widths=[1.7, 1.9, 2.8],
)
para("En production, le cache, les sessions et les files d'attente basculent sur la base de données "
"(pilote « database »), ce qui évite d'exploiter Redis et simplifie l'infrastructure.", size=9.7,
italic=True, color=GREY)
h2("2.2 Les trois environnements")
para("Le cycle de vie du logiciel s'appuie sur trois environnements distincts, du poste du développeur "
"jusqu'à la production exposée aux utilisateurs.")
table(
["", "Développement", "Test / Intégration", "Production"],
[
["Objectif", "Coder et tester en local", "Valider automatiquement chaque modification", "Servir les utilisateurs finaux"],
["Hébergement", "Poste du développeur", "Runner Gitea Actions (CI)", "Serveur Coolify (VPS)"],
["Orchestration", "docker-compose.yml", "Conteneur de CI éphémère", "docker-compose.prod.yaml"],
["Base de données", "PostgreSQL 16 + Redis", "SQLite en mémoire", "PostgreSQL 16 (réseau interne)"],
["Configuration", "APP_DEBUG=true, secrets en dur (non sensibles)", "APP_ENV=testing", "APP_DEBUG=false, secrets générés hors dépôt"],
["Accès", "localhost:3000 / :8000", "Journaux de la CI", "HTTPS : cesizen.cleanows.fr"],
],
widths=[1.3, 1.85, 1.7, 1.9], size=8.8,
)
h2("2.3 Conteneurisation et images Docker")
para("Chaque service est livré sous forme d'image Docker, garantissant un fonctionnement identique "
"d'un environnement à l'autre. Les images de développement privilégient le rechargement à chaud ; "
"les images de production sont optimisées.")
bullet("l'image back-end (php:8.4-cli) installe les extensions PostgreSQL/bcmath et n'embarque que les "
"dépendances de production (composer install --no-dev) ;", bold_prefix="Back-end — ")
bullet("l'image front-end utilise un build multi-étapes (builder → runner) et la sortie « standalone » "
"de Next.js, produisant une image d'exécution minimale ;", bold_prefix="Front-end — ")
bullet("un script d'entrée (entrypoint.sh) automatise au démarrage la génération des clés (APP_KEY, "
"JWT_SECRET), les migrations, le seed idempotent et la mise en cache des routes/vues.",
bold_prefix="Démarrage — ")
h2("2.4 Automatisations et intégration continue")
para("Deux automatisations complémentaires structurent la chaîne de livraison :")
bullet("à chaque push ou pull request, le pipeline .gitea/workflows/ci.yml exécute les 29 tests "
"automatisés (19 back-end PHPUnit, 6 web Vitest, 4 mobile Jest) et les contrôles de style "
"(Laravel Pint, ESLint). Un changement dont les tests échouent ne peut pas être livré.",
bold_prefix="Intégration continue (Gitea Actions) — ")
bullet("un merge sur la branche main déclenche, via un webhook, la reconstruction des images et le "
"redéploiement par Coolify, avec migrations et seed joués automatiquement, health checks et "
"redémarrage automatique des conteneurs.", bold_prefix="Déploiement continu (Coolify) — ")
image("archi_deploiement.png", width=6.4,
caption="Chaîne de déploiement : trois environnements et flux d'intégration/livraison continues.")
h2("2.5 Étapes de déploiement et ressources")
para("Le déploiement en production se déroule selon les étapes suivantes, entièrement automatisées "
"après le déclenchement :")
table(
["Étape", "Action", "Automatisation"],
[
["1. Récupération", "Coolify clone la révision de main", "Webhook Gitea → Coolify"],
["2. Build", "Construction des images back & front", "Dockerfile.prod (multi-étapes)"],
["3. Secrets", "Génération APP_KEY / JWT_SECRET si absents", "entrypoint.sh"],
["4. Base de données", "Migrations + seed idempotent", "artisan migrate --force / db:seed"],
["5. Démarrage", "Lancement des conteneurs + health checks", "docker-compose.prod.yaml"],
["6. Exposition", "Routage HTTPS/TLS et certificat", "Reverse proxy Coolify"],
["7. Supervision", "Vérification /up et redémarrage auto", "healthcheck + restart: unless-stopped"],
],
widths=[1.4, 2.6, 2.4], size=9,
)
para("Ressources et dimensionnement — l'application grand public est dimensionnée pour un VPS unique "
"(2 vCPU, 4 Go de RAM, 40 Go SSD) hébergeant les trois conteneurs, suffisant pour la phase de "
"test de 12 mois. La montée en charge se fait horizontalement (réplication des conteneurs back "
"et front derrière le reverse proxy, base de données managée) sans refonte, l'API étant stateless.")
page_break()
# ══════════════════════════════════════════════════════════════════
# 3. VERSIONING
# ══════════════════════════════════════════════════════════════════
h1("3. Gestion des sources et versioning")
h2("3.1 Outil retenu : Gitea")
para("Le versioning s'appuie sur Git, hébergé sur une instance Gitea auto-hébergée. Gitea a été retenu "
"car il réunit dans un seul outil, souverain et gratuit, le dépôt Git, le suivi des tickets, "
"l'intégration continue (Gitea Actions) et la gestion des versions — cohérent avec un projet porté "
"par un ministère (hébergement maîtrisé, pas de dépendance à un service tiers hors UE).")
table(
["Critère", "Gitea", "GitHub", "GitLab"],
[
["Souveraineté / auto-hébergement", "Oui (léger)", "Non (SaaS US)", "Oui (lourd)"],
["Ticketing intégré", "Oui", "Oui", "Oui"],
["CI/CD intégrée", "Oui (Actions)", "Oui", "Oui"],
["Coût", "Gratuit", "Gratuit/payant", "Gratuit/payant"],
["Ressources serveur", "Faibles", "—", "Élevées"],
],
widths=[2.4, 1.4, 1.4, 1.4], size=9,
)
h2("3.2 Stratégie de branches et conventions")
bullet("main : code en production, toujours déployable et protégé (merge via pull request + CI verte) ;")
bullet("develop : intégration des développements en cours ;")
bullet("feature/*, fix/*, hotfix/* : une branche par fonctionnalité ou correctif, fusionnée par PR.")
para("Les messages de commit suivent la convention Conventional Commits déjà appliquée sur le projet "
"(feat, fix, docs, chore, refactor…) avec un scope (back, web, mobile, deploy), ce qui rend "
"l'historique lisible et permet de générer automatiquement les notes de version. Les versions "
"livrées sont marquées par des tags (v1.0, v1.1…) reliés aux jalons Gitea.")
# ══════════════════════════════════════════════════════════════════
# 4. PLAN DE MAINTENANCE
# ══════════════════════════════════════════════════════════════════
h1("4. Plan de maintenance")
h2("4.1 Outil de gestion des corrections et évolutions")
para("Le suivi des demandes s'appuie sur le module Issues de Gitea, complété par un tableau de bord "
"Kanban (Projects) et des jalons (Milestones). Deux gabarits d'issues versionnés dans le dépôt "
"structurent la saisie et posent automatiquement les bons libellés :")
bullet("gabarit incident (maintenance corrective) : sévérité, module, environnement, étapes de "
"reproduction, résultat attendu/obtenu ;", bold_prefix="🐞 ")
bullet("gabarit demande d'évolution (maintenance évolutive) : besoin, solution proposée, priorité, "
"critères d'acceptation.", bold_prefix="✨ ")
para("Les libellés couvrent le type, la gravité, la priorité, le statut et le module ; les jalons "
"correspondent aux versions. Le tableau Kanban (À qualifier → À faire → En cours → En revue → "
"Résolu) offre une vision temps réel de l'avancement, partagée entre le prestataire et le client.")
h2("4.2 Maintenance corrective : méthodologie et engagements de service")
para("La maintenance corrective démarre dès la prise en compte d'un ticket d'incident. La sévérité "
"conditionne les délais contractuels de prise en compte et de correction, distinguant incident "
"bloquant, majeur et mineur.")
table(
["Sévérité", "Définition", "Prise en compte", "Correction"],
[
["Bloquant — critique", "Service inutilisable, aucun contournement", "1 h ouvrée", "3 h ouvrées"],
["Bloquant — fort", "Service inutilisable, contournement partiel", "2 h ouvrées", "6 h ouvrées"],
["Majeur", "Service dégradé / intermittent avec contournement", "7 h ouvrées", "16 h ouvrées"],
["Mineur (par lots)", "Altération mineure, service opérationnel", "1 j ouvré", "40 h ouvrées"],
],
widths=[1.5, 2.5, 1.2, 1.2], size=9,
)
para("Le traitement suit un cycle standardisé et traçable, du signalement à la clôture validée par le "
"client, en passant par la correction sur une branche dédiée et la revue par pull request "
"(fusion conditionnée à une CI verte).")
image("cycle_ticket.png", width=6.4,
caption="Cycle de vie d'un ticket sur Gitea et rappel des engagements de service (SLA).")
h2("4.3 Maintenance évolutive")
para("Chaque demande d'évolution fait l'objet d'un ticket puis d'une analyse par le prestataire "
"proposant : la documentation fonctionnelle et technique, le délai de mise en œuvre et le coût "
"associé. Après validation du client, l'évolution est planifiée dans un jalon de version. "
"L'architecture découplée et le référentiel de données extensible permettent d'activer les "
"modules écartés (Diagnostic de stress, Cohérence cardiaque, Activités de détente) sans réécrire "
"le modèle existant.")
h2("4.4 Pilotage")
para("Le pilotage de la maintenance s'appuie sur les tableaux de bord natifs de Gitea : filtres par "
"libellé/jalon/assigné, vue Kanban et suivi d'avancement par version. Ces indicateurs (nombre de "
"tickets ouverts/fermés, respect des délais, répartition par module) permettent au Ministère de "
"suivre la qualité de service.")
h2("4.5 Veille technologique et pérennité")
para("La pérennité de l'application dépend du suivi des évolutions de son écosystème. Une méthodologie "
"de veille structurée est mise en place :")
table(
["Axe de veille", "Sources", "Rythme", "Action"],
[
["Sécurité / vulnérabilités", "CVE, bulletins CERT-FR, GitHub Advisories, OWASP",
"Continu (automatisé)", "Alertes de dépendances + correctifs prioritaires"],
["Frameworks (Laravel, Next.js, Expo)", "Blogs officiels, notes de version, roadmaps",
"Mensuel", "Planifier les montées de version LTS"],
["Dépendances", "npm audit, composer audit, Renovate/Dependabot",
"Hebdomadaire", "PR automatiques de mise à jour, testées par la CI"],
["Réglementation (RGPD, HDS)", "CNIL, ANSSI, Legifrance",
"Trimestriel", "Adapter les mesures de conformité"],
],
widths=[1.9, 2.3, 1.1, 1.9], size=8.7,
)
para("La veille sécurité est en partie automatisée : les outils d'analyse de dépendances (composer "
"audit, npm audit) sont intégrables à la CI et signalent immédiatement toute dépendance "
"vulnérable, tandis qu'un robot de mise à jour propose des pull requests validées par les tests.")
page_break()
# ══════════════════════════════════════════════════════════════════
# 5. PLAN DE SÉCURISATION
# ══════════════════════════════════════════════════════════════════
h1("5. Plan de sécurisation")
para("S'agissant d'une application portée par un ministère et manipulant des données de santé mentale "
"(données sensibles au sens de l'article 9 du RGPD), la sécurité est traitée comme une exigence de "
"premier ordre. Ce plan identifie les vulnérabilités et risques, en évalue la criticité, puis "
"présente les mesures déjà en place et les actions correctives et préventives planifiées.")
h2("5.1 Analyse des vulnérabilités")
para("L'analyse s'appuie sur le référentiel OWASP Top 10. Le tableau ci-dessous confronte chaque risque "
"aux mesures réellement implémentées dans le prototype et aux axes de renforcement.")
table(
["Vulnérabilité (OWASP)", "Mesure en place", "Renforcement prévu"],
[
["A01 Contrôle d'accès défaillant", "RBAC par middleware (role:administrateur), garde-fous (un admin ne peut se désactiver/supprimer)", "Revue périodique des droits"],
["A02 Défaillances cryptographiques", "HTTPS/TLS, bcrypt (coût 12), mots de passe masqués et exclus des logs", "Chiffrement applicatif au repos des notes de santé (cast chiffré)"],
["A03 Injection", "ORM Eloquent (requêtes paramétrées), Form Requests de validation", "Purification/échappement HTML côté API"],
["A05 Mauvaise configuration", "APP_DEBUG=false en prod, secrets hors dépôt, CORS en liste blanche", "En-têtes de sécurité HSTS/CSP/X-Frame-Options"],
["A07 Authentification défaillante", "JWT signé (HS256), TTL 60 min, blacklist au logout, rate limiting", "Politique de mot de passe renforcée (complexité), vérification d'e-mail, MFA admin"],
["A09 Journalisation insuffisante", "Piste d'audit immuable (IP, ancienne/nouvelle valeur)", "Centralisation des logs + alerting (supervision)"],
],
widths=[2.1, 2.6, 1.7], size=8.6,
)
h2("5.2 Matrice de criticité des risques")
para("Chaque risque est positionné selon sa gravité et sa probabilité ; la criticité est le produit des "
"deux (échelle de 1 à 25). Cette hiérarchisation oriente la priorité des actions.")
image("matrice_risques.png", width=6.2,
caption="Matrice de criticité des risques du projet CESIZen.")
table(
["Réf.", "Risque", "Gravité", "Prob.", "Criticité", "Priorité"],
[
["R1", "Fuite de données de santé (art. 9)", "Critique (4)", "Faible (2)", "8", "Élevée"],
["R7", "Absence de chiffrement au repos", "Critique (4)", "Moyenne (3)", "12", "Élevée"],
["R2", "Injection SQL / XSS", "Grave (3)", "Moyenne (3)", "9", "Élevée"],
["R3", "Compromission d'un compte admin", "Grave (3)", "Faible (2)", "6", "Moyenne"],
["R6", "Dépendances vulnérables", "Modérée (2)", "Forte (4)", "8", "Moyenne"],
["R4", "DDoS / indisponibilité", "Modérée (2)", "Moyenne (3)", "6", "Moyenne"],
["R5", "Perte de données (sauvegarde)", "Modérée (2)", "Faible (2)", "4", "Faible"],
["R8", "Erreur humaine de déploiement", "Mineure (1)", "Faible (2)", "2", "Faible"],
],
widths=[0.5, 2.5, 1.1, 1.0, 0.9, 0.9], size=8.7,
)
h2("5.3 Actions préventives et correctives")
para("Pour chaque risque, des actions préventives (réduire la probabilité) et correctives (limiter "
"l'impact) sont définies, couvrant l'ensemble des risques critiques et majeurs identifiés.")
table(
["Réf.", "Actions préventives", "Actions correctives"],
[
["R1", "Minimisation des données, chiffrement en transit, RBAC, audit des accès, hébergement UE/HDS",
"Procédure de notification CNIL sous 72 h, isolation, révocation des accès, information des personnes"],
["R7", "Chiffrement applicatif des champs sensibles (note), chiffrement du volume PostgreSQL",
"Rotation des clés, restauration depuis sauvegarde chiffrée"],
["R2", "Validation systématique (Form Requests), ORM paramétré, échappement des sorties",
"Correctif prioritaire, revue de code, test de non-régression"],
["R3", "MFA administrateur, mots de passe robustes, rate limiting, journalisation des actions",
"Blacklist du token, réinitialisation, analyse de la piste d'audit"],
["R6", "composer/npm audit en CI, robot de mise à jour, veille CVE",
"Application du correctif, redéploiement automatisé"],
["R4", "Reverse proxy, rate limiting, health checks + redémarrage auto",
"Mise à l'échelle, activation d'une protection anti-DDoS (CDN/WAF)"],
["R5", "Sauvegardes quotidiennes automatisées et testées, réplication du volume",
"Restauration point-in-time, plan de reprise d'activité"],
],
widths=[0.5, 3.0, 3.0], size=8.4,
)
h2("5.4 Chiffrement et protection des échanges")
bullet("l'ensemble des échanges entre les clients et l'API passe par HTTPS/TLS (terminaison au niveau "
"du reverse proxy Coolify), interdisant l'interception en clair ;", bold_prefix="En transit — ")
bullet("les mots de passe sont hachés avec bcrypt (coût 12) et ne sont jamais renvoyés ni journalisés ; "
"la clé applicative (APP_KEY, AES-256) protège cookies et données chiffrées. Le chiffrement "
"applicatif des notes de santé (champ le plus sensible) est planifié via le cast chiffré de "
"Laravel ;", bold_prefix="Au repos — ")
bullet("l'authentification repose sur des jetons JWT signés (HS256), d'une durée de vie de 60 minutes, "
"invalidés à la déconnexion (liste noire).", bold_prefix="Jetons — ")
h2("5.5 Données personnelles et conformité RGPD")
para("Les données de santé mentale (émotions, intensité, notes) constituent des données sensibles "
"(art. 9 RGPD). La conformité est prise en compte par conception :")
table(
["Exigence RGPD", "Mise en œuvre dans CESIZen"],
[
["Base légale / consentement", "Consentement explicite obligatoire à l'inscription (non pré-coché), tracé en base"],
["Minimisation", "Seules les données nécessaires au service sont collectées"],
["Droit d'accès et portabilité (art. 15/20)", "Export JSON téléchargeable de toutes les données de l'utilisateur"],
["Droit à l'effacement (art. 17)", "Anonymisation (dépersonnalisation des notes de santé) et suppression logique (soft delete)"],
["Traçabilité / responsabilité", "Piste d'audit immuable (action, IP, anciennes/nouvelles valeurs, hors mot de passe)"],
["Localisation", "Hébergement dans l'Union européenne, aucun transfert hors UE ; cible HDS pour la prod"],
["Sécurité (art. 32)", "Chiffrement en transit, hachage, contrôle d'accès, journalisation"],
],
widths=[2.3, 4.1], size=9,
)
h2("5.6 Gestion de crise et escalade d'information")
para("En cas d'incident de sécurité avéré (intrusion, fuite de données, indisponibilité majeure), une "
"procédure de gestion de crise organise la réaction et la remontée d'information selon des niveaux "
"d'escalade définis.")
table(
["Niveau", "Déclencheur", "Responsable", "Actions & communication"],
[
["N1 — Détection", "Alerte supervision / signalement", "Astreinte technique", "Qualifier, ouvrir un ticket incident de sécurité, contenir"],
["N2 — Confirmation", "Incident de sécurité confirmé", "Responsable technique", "Isoler, préserver les preuves (logs), évaluer l'impact"],
["N3 — Crise", "Fuite de données personnelles avérée", "Chef de projet + DPO", "Notification CNIL sous 72 h, information des personnes concernées"],
["N4 — Direction", "Impact majeur / médiatique", "Direction + Ministère", "Communication officielle, coordination ANSSI si nécessaire"],
],
widths=[1.3, 1.9, 1.4, 1.8], size=8.6,
)
para("La communication distingue la remontée interne (technique → chef de projet → direction) et la "
"communication externe (personnes concernées, CNIL sous 72 h conformément à l'art. 33 RGPD, "
"éventuellement ANSSI). Chaque crise donne lieu à un retour d'expérience et à des actions "
"préventives pour éviter la récurrence.")
page_break()
# ══════════════════════════════════════════════════════════════════
# 6. BONNES PRATIQUES
# ══════════════════════════════════════════════════════════════════
h1("6. Bonnes pratiques de développement")
para("La maintenabilité et la sécurité de l'application reposent sur des pratiques de développement "
"structurées, appliquées tout au long du projet :")
bullet("architecture MVC côté back-end (Modèles, Contrôleurs fins, Form Requests de validation, "
"couche Service pour les rapports) ; App Router, hooks personnalisés et store côté front.",
bold_prefix="Structure du code — ")
bullet("respect du PSR-12 vérifié par Laravel Pint ; TypeScript strict et ESLint côté web et mobile ; "
"contrôles intégrés à la CI.", bold_prefix="Normes et style — ")
bullet("29 tests automatisés sur les trois couches (PHPUnit, Vitest, Jest), exécutés à chaque push, "
"empêchant toute régression de partir en production.", bold_prefix="Tests — ")
bullet("Conventional Commits, revue systématique par pull request, historique lisible.",
bold_prefix="Gestion de versions — ")
bullet("README, guide de lancement, documentation d'API commentée et guide de migration/outillage "
"(MIGRATION_GITEA.md).", bold_prefix="Documentation — ")
h1("7. Conclusion")
para("Le projet CESIZen dispose d'une chaîne de déploiement complète et outillée : trois environnements "
"clairement séparés, un versioning souverain sous Gitea, une intégration continue exécutant "
"29 tests à chaque modification et un déploiement continu automatisé vers une production réellement "
"en ligne et sécurisée par HTTPS. La maintenance est cadrée par un outil de ticketing configuré, "
"des engagements de service précis et une veille technologique méthodique. La sécurisation, enfin, "
"repose sur une analyse de risques hiérarchisée, des mesures déjà implémentées (authentification "
"JWT, RBAC, audit, RGPD) et un plan d'actions correctives et préventives, complété par une "
"procédure de gestion de crise. L'application est ainsi prête à être exploitée et à évoluer dans "
"la durée au service de la santé mentale du grand public.")
# ─── Pied de page ─────────────────────────────────────────────────
section = doc.sections[0]
footer = section.footer
fp = footer.paragraphs[0]
fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
fr = fp.add_run("CESIZen · Bloc INFCDAAL3 — Déployer et sécuriser les applications informatiques · Adam Marzuk")
fr.font.size = Pt(8)
fr.font.color.rgb = GREY
doc.save(OUT)
print("Dossier généré :", OUT)
print("Paragraphes :", len(doc.paragraphs), "· Tableaux :", len(doc.tables))
+417
View File
@@ -0,0 +1,417 @@
#!/usr/bin/env python3
"""
Génère le support de soutenance CESIZen — Activité 3 « Déployer et sécuriser »
(PowerPoint 16:9 éditable). Réutilise la charte du support du Bloc 1.
Sortie : ./CESIZen_Soutenance_Bloc3.pptx
Schémas : ./assets_bloc3/*.png
"""
import os
from PIL import Image
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE
# ─── Charte CESIZen ────────────────────────────────────────────────
GREEN = RGBColor(6, 198, 86)
GREEN_D = RGBColor(4, 150, 65)
BLUE = RGBColor(0, 70, 130)
BLUE_L = RGBColor(230, 240, 250)
YELLOW = RGBColor(252, 225, 23)
ORANGE = RGBColor(228, 128, 28)
RED = RGBColor(200, 16, 46)
GREY_D = RGBColor(50, 50, 50)
GREY = RGBColor(110, 110, 110)
WHITE = RGBColor(255, 255, 255)
INK = RGBColor(35, 35, 45)
HERE = os.path.dirname(os.path.abspath(__file__))
ASSETS = os.path.join(HERE, 'assets_bloc3')
OUT = os.path.join(HERE, 'CESIZen_Soutenance_Bloc3.pptx')
prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
SW, SH = prs.slide_width, prs.slide_height
BLANK = prs.slide_layouts[6]
def _no_line(shape):
shape.line.fill.background()
def rect(slide, x, y, w, h, color, line=None):
s = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, w, h)
s.fill.solid()
s.fill.fore_color.rgb = color
if line is None:
_no_line(s)
else:
s.line.color.rgb = line
s.line.width = Pt(1)
s.shadow.inherit = False
return s
def textbox(slide, x, y, w, h, anchor=MSO_ANCHOR.TOP):
tb = slide.shapes.add_textbox(x, y, w, h)
tf = tb.text_frame
tf.word_wrap = True
tf.vertical_anchor = anchor
tf.margin_left = Inches(0.05)
tf.margin_right = Inches(0.05)
tf.margin_top = Inches(0.02)
tf.margin_bottom = Inches(0.02)
return tb, tf
def set_par(p, text, size, color, bold=False, align=PP_ALIGN.LEFT,
font='Arial', space_after=8):
p.text = text
p.alignment = align
p.space_after = Pt(space_after)
for r in p.runs:
r.font.size = Pt(size)
r.font.color.rgb = color
r.font.bold = bold
r.font.name = font
def footer(slide):
tb, tf = textbox(slide, Inches(0.4), Inches(7.05), Inches(12.5), Inches(0.35))
set_par(tf.paragraphs[0],
"CESIZen · Bloc INFCDAAL3 — Déployer et sécuriser les applications informatiques · Adam Marzuk",
9, GREY, align=PP_ALIGN.CENTER, space_after=0)
def band_title(slide, text, num=None):
rect(slide, 0, 0, SW, Inches(1.05), BLUE)
rect(slide, 0, Inches(1.05), SW, Inches(0.06), GREEN)
label = f"{num}. {text}" if num else text
tb, tf = textbox(slide, Inches(0.5), Inches(0.12), Inches(12.3), Inches(0.85),
anchor=MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], label, 28, WHITE, bold=True, space_after=0)
def bullets(slide, items, x, y, w, h, size=18, gap=10):
tb, tf = textbox(slide, x, y, w, h)
for i, it in enumerate(items):
lvl = 0
txt = it
if isinstance(it, tuple):
txt, lvl = it
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
prefix = "• " if lvl == 0 else "– "
set_par(p, prefix + txt, size if lvl == 0 else size - 2,
INK if lvl == 0 else GREY_D, bold=False, space_after=gap)
p.level = lvl
if lvl == 1:
p.runs[0].font.color.rgb = GREY
return tb
def image_fit(slide, name, x, y, max_w, max_h, caption=None):
path = os.path.join(ASSETS, name)
iw, ih = Image.open(path).size
ar = iw / ih
box_ar = max_w / max_h
if box_ar > ar:
h = max_h
w = int(h * ar)
else:
w = max_w
h = int(w / ar)
left = x + (max_w - w) // 2
top = y + (max_h - h) // 2
slide.shapes.add_picture(path, left, top, width=w, height=h)
if caption:
tb, tf = textbox(slide, x, y + max_h + Inches(0.02), max_w, Inches(0.35))
set_par(tf.paragraphs[0], caption, 11, GREY, align=PP_ALIGN.CENTER, space_after=0)
def cards(slide, items, y, h=1.8, top=0.55):
"""items = list of (titre, description, couleur)"""
n = len(items)
total_w = 12.0
gap = 0.25
cw = (total_w - gap * (n - 1)) / n
for i, (t, d, col) in enumerate(items):
x = Inches(0.7 + i * (cw + gap))
rect(slide, x, Inches(y), Inches(cw), Inches(h), BLUE_L)
rect(slide, x, Inches(y), Inches(cw), Inches(top), col)
tb, tf = textbox(slide, x + Inches(0.1), Inches(y + 0.03), Inches(cw - 0.2),
Inches(top), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], t, 13, WHITE, bold=True, space_after=0, align=PP_ALIGN.CENTER)
tb, tf = textbox(slide, x + Inches(0.15), Inches(y + top + 0.1), Inches(cw - 0.3),
Inches(h - top - 0.15))
for j, line in enumerate(d):
p = tf.paragraphs[0] if j == 0 else tf.add_paragraph()
set_par(p, "• " + line, 11.5, INK, space_after=5)
# ════════════════════════════════════════════════════════════════════
# Slide 1 — Titre
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
rect(s, 0, 0, SW, SH, WHITE)
rect(s, 0, Inches(2.5), SW, Inches(0.08), GREEN)
tb, tf = textbox(s, Inches(1), Inches(0.9), Inches(11.3), Inches(1.5))
set_par(tf.paragraphs[0], "CESIZen", 60, GREEN, bold=True, align=PP_ALIGN.CENTER, space_after=0)
tb, tf = textbox(s, Inches(1), Inches(2.62), Inches(11.3), Inches(0.9))
set_par(tf.paragraphs[0], "L'application de votre santé mentale", 24, GREY_D,
align=PP_ALIGN.CENTER, space_after=0)
tb, tf = textbox(s, Inches(1), Inches(3.65), Inches(11.3), Inches(0.9))
set_par(tf.paragraphs[0], "Activité 3 — Déployer et sécuriser les applications informatiques",
18, BLUE, align=PP_ALIGN.CENTER, space_after=0)
tb, tf = textbox(s, Inches(1), Inches(5.4), Inches(11.3), Inches(1.4))
for i, line in enumerate([
"Adam Marzuk — CESI École d'Ingénieurs",
"Bloc INFCDAAL3 — Déployer et sécuriser les applications informatiques",
"Commanditaire : Ministère des Solidarités et de la Santé"]):
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
set_par(p, line, 15, GREY, align=PP_ALIGN.CENTER, space_after=4)
# ════════════════════════════════════════════════════════════════════
# Slide 2 — Sommaire
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Sommaire")
bullets(s, [
"Contexte et périmètre de l'Activité 3",
"Plan de déploiement : architecture et environnements",
"Versioning et intégration/livraison continues (Gitea · Coolify)",
"Plan de maintenance : ticketing, engagements de service, veille",
"Plan de sécurisation : vulnérabilités, risques, actions",
"Données personnelles, RGPD et gestion de crise",
"Démonstration des outils et conclusion",
], Inches(1.2), Inches(1.5), Inches(11), Inches(5.2), size=22, gap=13)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 3 — Contexte & périmètre
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Contexte et périmètre de l'Activité 3", 1)
bullets(s, [
"CESIZen : plateforme grand public de santé mentale (Ministère).",
"Après la conception (Act. 1) et le développement/test (Act. 2), l'Activité 3 traite du déploiement et de la sécurisation.",
"Prototype déployé : API Laravel + web Next.js + mobile Expo + PostgreSQL.",
], Inches(0.8), Inches(1.4), Inches(11.7), Inches(2.0), size=18)
tb, tf = textbox(s, Inches(0.8), Inches(3.7), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "3 livrables", 16, GREEN_D, bold=True, space_after=0)
cards(s, [
("Plan de déploiement", ["Environnements", "Versioning", "Automatisations"], GREEN),
("Plan de maintenance", ["Ticketing Gitea", "SLA & méthodologie", "Veille techno"], BLUE),
("Plan de sécurisation", ["Risques & criticité", "Actions correctives", "RGPD & crise"], GREEN_D),
], y=4.3, h=2.0)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 4 — Architecture technique
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Architecture technique de la solution", 2)
bullets(s, [
"Architecture découplée : une API REST centrale, deux clients (web + mobile).",
("Back-end : Laravel 12 (PHP 8.4) — API REST JSON, authentification JWT.", 1),
("Base de données : PostgreSQL 16 (ACID) ; Redis en développement.", 1),
("Front-end web : Next.js 16 / React 19, design system de l'État (DSFR).", 1),
("Mobile : React Native / Expo 54 (iOS + Android, Mobile First).", 1),
"Conteneurisation Docker : environnements reproductibles et isolés.",
"Une seule logique métier → non-duplication, testabilité, évolutivité.",
], Inches(0.8), Inches(1.5), Inches(11.7), Inches(5.0), size=17, gap=9)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 5 — Plan de déploiement : 3 environnements + chaîne
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Plan de déploiement — environnements et chaîne CI/CD", 2)
image_fit(s, "archi_deploiement.png", Inches(0.5), Inches(1.25), Inches(12.3), Inches(5.5),
caption="Développement (local) → Test/Intégration (Gitea Actions) → Production (Coolify, HTTPS)")
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 6 — Versioning & CI/CD
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Versioning et intégration/livraison continues", 3)
bullets(s, [
"Gitea : dépôt Git, ticketing et CI/CD réunis dans un outil souverain auto-hébergé.",
"Stratégie de branches : main (prod, protégée) · develop · feature/fix/hotfix.",
"Conventions : Conventional Commits, revue par pull request, tags de version.",
], Inches(0.8), Inches(1.4), Inches(11.7), Inches(1.9), size=17)
tb, tf = textbox(s, Inches(0.8), Inches(3.55), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "Deux automatisations complémentaires", 16, GREEN_D, bold=True, space_after=0)
cards(s, [
("Intégration continue — Gitea Actions", [
"29 tests à chaque push",
"Lint (Pint, ESLint)",
"Merge bloqué si CI rouge"], ORANGE),
("Déploiement continu — Coolify", [
"Webhook sur merge main",
"Build images + migrations",
"HTTPS, health checks, restart"], BLUE),
], y=4.15, h=2.1)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 7 — Maintenance : ticketing
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Plan de maintenance — ticketing et cycle de vie", 4)
image_fit(s, "cycle_ticket.png", Inches(0.5), Inches(1.25), Inches(12.3), Inches(5.5),
caption="Gitea Issues : gabarits incident/évolution, labels, jalons, Kanban · SLA du contrat")
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 8 — Maintenance : méthodo + veille
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Maintenance corrective, évolutive et veille", 4)
tb, tf = textbox(s, Inches(0.8), Inches(1.3), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "Méthodologie", 16, GREEN_D, bold=True, space_after=0)
bullets(s, [
"Corrective : ticket → qualification (gravité) → SLA → correction → revue (PR + CI) → clôture validée.",
"Évolutive : analyse (documentation, délai, coût) → validation client → planification par version.",
"Pilotage : tableaux de bord Gitea (Kanban, jalons, indicateurs de délais).",
], Inches(0.8), Inches(1.8), Inches(11.7), Inches(2.3), size=16, gap=9)
tb, tf = textbox(s, Inches(0.8), Inches(4.2), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "Veille technologique (pérennité)", 16, GREEN_D, bold=True, space_after=0)
bullets(s, [
"Sécurité : CVE, CERT-FR, OWASP, alertes de dépendances (composer/npm audit en CI).",
"Frameworks & dépendances : notes de version, robot de mises à jour testées par la CI.",
"Réglementation : CNIL, ANSSI (RGPD, HDS).",
], Inches(0.8), Inches(4.7), Inches(11.7), Inches(1.9), size=16, gap=9)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 9 — Sécurisation : vulnérabilités OWASP
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Plan de sécurisation — vulnérabilités (OWASP)", 5)
bullets(s, [
"Contrôle d'accès : RBAC par middleware, garde-fous admin.",
"Cryptographie : HTTPS/TLS, bcrypt (coût 12), mots de passe masqués et hors logs.",
"Injection : ORM paramétré (Eloquent) + validation par Form Requests.",
"Authentification : JWT signé, TTL 60 min, blacklist, rate limiting différencié.",
"Journalisation : piste d'audit immuable (IP, anciennes/nouvelles valeurs).",
"Configuration : APP_DEBUG=false, secrets hors dépôt, CORS en liste blanche.",
], Inches(0.8), Inches(1.5), Inches(11.7), Inches(5.0), size=17, gap=11)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 10 — Matrice de risques
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Sécurisation — matrice de criticité des risques", 5)
image_fit(s, "matrice_risques.png", Inches(2.4), Inches(1.25), Inches(8.5), Inches(5.5),
caption="Criticité = gravité × probabilité · priorisation des actions (R1, R7, R2 prioritaires)")
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 11 — Actions préventives / correctives
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Sécurisation — actions préventives et correctives", 5)
cards(s, [
("Prévention", [
"Validation & ORM (injection)",
"MFA admin, mots de passe robustes",
"audit dépendances en CI",
"Sauvegardes automatisées"], GREEN_D),
("Chiffrement", [
"HTTPS/TLS en transit",
"bcrypt + AES (APP_KEY)",
"Notes de santé chiffrées (prévu)",
"JWT signés"], BLUE),
("Correction", [
"Correctif prioritaire + test",
"Révocation / blacklist token",
"Redéploiement automatisé",
"Plan de reprise (PRA)"], ORANGE),
], y=1.6, h=4.6, top=0.6)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 12 — RGPD
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Données personnelles et RGPD", 6)
bullets(s, [
"Données de santé mentale = données sensibles (RGPD art. 9).",
"Consentement explicite obligatoire à l'inscription, tracé en base.",
"Droits implémentés : accès et portabilité (export JSON), effacement (anonymisation + soft delete).",
"Minimisation des données ; hébergement UE, aucun transfert hors UE ; cible HDS en production.",
], Inches(0.8), Inches(1.45), Inches(11.7), Inches(2.4), size=17, gap=10)
rect(s, Inches(0.8), Inches(4.25), Inches(11.7), Inches(2.3), BLUE_L)
rect(s, Inches(0.8), Inches(4.25), Inches(3.6), Inches(0.55), GREEN_D)
tb, tf = textbox(s, Inches(0.95), Inches(4.28), Inches(3.4), Inches(0.5), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], "Traçabilité (art. 32)", 15, WHITE, bold=True, space_after=0)
bullets(s, [
"Piste d'audit immuable : action, IP, anciennes/nouvelles valeurs — le mot de passe n'est jamais journalisé.",
"Chiffrement en transit, hachage bcrypt, contrôle d'accès par rôle.",
"Export et anonymisation testés automatiquement (tests RGPD).",
], Inches(1.0), Inches(4.95), Inches(11.3), Inches(1.6), size=14, gap=6)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 13 — Gestion de crise & escalade
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Gestion de crise et escalade d'information", 6)
levels = [
("N1 — Détection", "Astreinte technique", "Qualifier, contenir, ouvrir un ticket sécurité", GREEN),
("N2 — Confirmation", "Responsable technique", "Isoler, préserver les preuves, évaluer l'impact", YELLOW),
("N3 — Crise", "Chef de projet + DPO", "Notification CNIL < 72 h, information des personnes", ORANGE),
("N4 — Direction", "Direction + Ministère", "Communication officielle, coordination ANSSI", RED),
]
y = 1.55
for titre, resp, act, col in levels:
rect(s, Inches(0.8), Inches(y), Inches(11.7), Inches(1.15), BLUE_L)
rect(s, Inches(0.8), Inches(y), Inches(0.16), Inches(1.15), col)
tb, tf = textbox(s, Inches(1.1), Inches(y + 0.05), Inches(3.0), Inches(1.05), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], titre, 15, col if col != YELLOW else GREY_D, bold=True, space_after=2)
p = tf.add_paragraph(); set_par(p, resp, 12, GREY_D, space_after=0)
tb, tf = textbox(s, Inches(4.3), Inches(y + 0.05), Inches(8.0), Inches(1.05), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], act, 14, INK, space_after=0)
y += 1.32
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 14 — Démo & conclusion
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Démonstration et conclusion", 7)
tb, tf = textbox(s, Inches(0.8), Inches(1.35), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "Démonstration des outils (en direct)", 17, GREEN_D, bold=True, space_after=0)
bullets(s, [
"Versioning : arbre des commits, branches et tags sur Gitea.",
"Intégration continue : pull request → 29 tests verts (Gitea Actions).",
"Ticketing : création d'une issue (gabarit incident), labels, jalon, Kanban.",
"Déploiement : merge → Coolify → site en ligne (cesizen.cleanows.fr).",
], Inches(0.8), Inches(1.85), Inches(11.7), Inches(2.4), size=16, gap=9)
tb, tf = textbox(s, Inches(0.8), Inches(4.35), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "En synthèse", 17, GREEN_D, bold=True, space_after=0)
bullets(s, [
"Chaîne de déploiement complète, outillée et réellement en production.",
"Maintenance cadrée (ticketing, SLA, veille) et sécurité analysée par les risques + RGPD.",
], Inches(0.8), Inches(4.85), Inches(11.7), Inches(1.4), size=16, gap=8)
rect(s, 0, Inches(6.55), SW, Inches(0.95), BLUE)
tb, tf = textbox(s, Inches(0.8), Inches(6.57), Inches(11.7), Inches(0.9), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], "Merci de votre attention — Questions / Réponses", 20, WHITE,
bold=True, align=PP_ALIGN.CENTER, space_after=0)
# ─── Numéros de page (toutes les slides sauf la page de titre) ─────
for idx, slide in enumerate(prs.slides):
if idx == 0:
continue # pas de numéro sur la slide de titre
tb, tf = textbox(slide, Inches(12.4), Inches(7.05), Inches(0.7), Inches(0.35),
anchor=MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], str(idx + 1), 10, GREY, bold=True,
align=PP_ALIGN.RIGHT, space_after=0)
prs.save(OUT)
print(f"Support de soutenance généré : {OUT}")
print(f"Nombre de slides : {len(prs.slides._sldIdLst)}")