feat: add slide generation script for CESIZen project, covering deployment and security aspects
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 ? -->
|
||||||
@@ -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.
@@ -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.
|
||||||
@@ -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 |
@@ -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)
|
||||||
@@ -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))
|
||||||
@@ -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)}")
|
||||||
Reference in New Issue
Block a user