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

This commit is contained in:
Azmog
2026-07-09 16:41:44 +02:00
parent 37a1079201
commit 867d744242
14 changed files with 1648 additions and 0 deletions
+417
View File
@@ -0,0 +1,417 @@
#!/usr/bin/env python3
"""
Génère le support de soutenance CESIZen — Activité 3 « Déployer et sécuriser »
(PowerPoint 16:9 éditable). Réutilise la charte du support du Bloc 1.
Sortie : ./CESIZen_Soutenance_Bloc3.pptx
Schémas : ./assets_bloc3/*.png
"""
import os
from PIL import Image
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE
# ─── Charte CESIZen ────────────────────────────────────────────────
GREEN = RGBColor(6, 198, 86)
GREEN_D = RGBColor(4, 150, 65)
BLUE = RGBColor(0, 70, 130)
BLUE_L = RGBColor(230, 240, 250)
YELLOW = RGBColor(252, 225, 23)
ORANGE = RGBColor(228, 128, 28)
RED = RGBColor(200, 16, 46)
GREY_D = RGBColor(50, 50, 50)
GREY = RGBColor(110, 110, 110)
WHITE = RGBColor(255, 255, 255)
INK = RGBColor(35, 35, 45)
HERE = os.path.dirname(os.path.abspath(__file__))
ASSETS = os.path.join(HERE, 'assets_bloc3')
OUT = os.path.join(HERE, 'CESIZen_Soutenance_Bloc3.pptx')
prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
SW, SH = prs.slide_width, prs.slide_height
BLANK = prs.slide_layouts[6]
def _no_line(shape):
shape.line.fill.background()
def rect(slide, x, y, w, h, color, line=None):
s = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, w, h)
s.fill.solid()
s.fill.fore_color.rgb = color
if line is None:
_no_line(s)
else:
s.line.color.rgb = line
s.line.width = Pt(1)
s.shadow.inherit = False
return s
def textbox(slide, x, y, w, h, anchor=MSO_ANCHOR.TOP):
tb = slide.shapes.add_textbox(x, y, w, h)
tf = tb.text_frame
tf.word_wrap = True
tf.vertical_anchor = anchor
tf.margin_left = Inches(0.05)
tf.margin_right = Inches(0.05)
tf.margin_top = Inches(0.02)
tf.margin_bottom = Inches(0.02)
return tb, tf
def set_par(p, text, size, color, bold=False, align=PP_ALIGN.LEFT,
font='Arial', space_after=8):
p.text = text
p.alignment = align
p.space_after = Pt(space_after)
for r in p.runs:
r.font.size = Pt(size)
r.font.color.rgb = color
r.font.bold = bold
r.font.name = font
def footer(slide):
tb, tf = textbox(slide, Inches(0.4), Inches(7.05), Inches(12.5), Inches(0.35))
set_par(tf.paragraphs[0],
"CESIZen · Bloc INFCDAAL3 — Déployer et sécuriser les applications informatiques · Adam Marzuk",
9, GREY, align=PP_ALIGN.CENTER, space_after=0)
def band_title(slide, text, num=None):
rect(slide, 0, 0, SW, Inches(1.05), BLUE)
rect(slide, 0, Inches(1.05), SW, Inches(0.06), GREEN)
label = f"{num}. {text}" if num else text
tb, tf = textbox(slide, Inches(0.5), Inches(0.12), Inches(12.3), Inches(0.85),
anchor=MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], label, 28, WHITE, bold=True, space_after=0)
def bullets(slide, items, x, y, w, h, size=18, gap=10):
tb, tf = textbox(slide, x, y, w, h)
for i, it in enumerate(items):
lvl = 0
txt = it
if isinstance(it, tuple):
txt, lvl = it
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
prefix = "• " if lvl == 0 else "– "
set_par(p, prefix + txt, size if lvl == 0 else size - 2,
INK if lvl == 0 else GREY_D, bold=False, space_after=gap)
p.level = lvl
if lvl == 1:
p.runs[0].font.color.rgb = GREY
return tb
def image_fit(slide, name, x, y, max_w, max_h, caption=None):
path = os.path.join(ASSETS, name)
iw, ih = Image.open(path).size
ar = iw / ih
box_ar = max_w / max_h
if box_ar > ar:
h = max_h
w = int(h * ar)
else:
w = max_w
h = int(w / ar)
left = x + (max_w - w) // 2
top = y + (max_h - h) // 2
slide.shapes.add_picture(path, left, top, width=w, height=h)
if caption:
tb, tf = textbox(slide, x, y + max_h + Inches(0.02), max_w, Inches(0.35))
set_par(tf.paragraphs[0], caption, 11, GREY, align=PP_ALIGN.CENTER, space_after=0)
def cards(slide, items, y, h=1.8, top=0.55):
"""items = list of (titre, description, couleur)"""
n = len(items)
total_w = 12.0
gap = 0.25
cw = (total_w - gap * (n - 1)) / n
for i, (t, d, col) in enumerate(items):
x = Inches(0.7 + i * (cw + gap))
rect(slide, x, Inches(y), Inches(cw), Inches(h), BLUE_L)
rect(slide, x, Inches(y), Inches(cw), Inches(top), col)
tb, tf = textbox(slide, x + Inches(0.1), Inches(y + 0.03), Inches(cw - 0.2),
Inches(top), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], t, 13, WHITE, bold=True, space_after=0, align=PP_ALIGN.CENTER)
tb, tf = textbox(slide, x + Inches(0.15), Inches(y + top + 0.1), Inches(cw - 0.3),
Inches(h - top - 0.15))
for j, line in enumerate(d):
p = tf.paragraphs[0] if j == 0 else tf.add_paragraph()
set_par(p, "• " + line, 11.5, INK, space_after=5)
# ════════════════════════════════════════════════════════════════════
# Slide 1 — Titre
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
rect(s, 0, 0, SW, SH, WHITE)
rect(s, 0, Inches(2.5), SW, Inches(0.08), GREEN)
tb, tf = textbox(s, Inches(1), Inches(0.9), Inches(11.3), Inches(1.5))
set_par(tf.paragraphs[0], "CESIZen", 60, GREEN, bold=True, align=PP_ALIGN.CENTER, space_after=0)
tb, tf = textbox(s, Inches(1), Inches(2.62), Inches(11.3), Inches(0.9))
set_par(tf.paragraphs[0], "L'application de votre santé mentale", 24, GREY_D,
align=PP_ALIGN.CENTER, space_after=0)
tb, tf = textbox(s, Inches(1), Inches(3.65), Inches(11.3), Inches(0.9))
set_par(tf.paragraphs[0], "Activité 3 — Déployer et sécuriser les applications informatiques",
18, BLUE, align=PP_ALIGN.CENTER, space_after=0)
tb, tf = textbox(s, Inches(1), Inches(5.4), Inches(11.3), Inches(1.4))
for i, line in enumerate([
"Adam Marzuk — CESI École d'Ingénieurs",
"Bloc INFCDAAL3 — Déployer et sécuriser les applications informatiques",
"Commanditaire : Ministère des Solidarités et de la Santé"]):
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
set_par(p, line, 15, GREY, align=PP_ALIGN.CENTER, space_after=4)
# ════════════════════════════════════════════════════════════════════
# Slide 2 — Sommaire
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Sommaire")
bullets(s, [
"Contexte et périmètre de l'Activité 3",
"Plan de déploiement : architecture et environnements",
"Versioning et intégration/livraison continues (Gitea · Coolify)",
"Plan de maintenance : ticketing, engagements de service, veille",
"Plan de sécurisation : vulnérabilités, risques, actions",
"Données personnelles, RGPD et gestion de crise",
"Démonstration des outils et conclusion",
], Inches(1.2), Inches(1.5), Inches(11), Inches(5.2), size=22, gap=13)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 3 — Contexte & périmètre
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Contexte et périmètre de l'Activité 3", 1)
bullets(s, [
"CESIZen : plateforme grand public de santé mentale (Ministère).",
"Après la conception (Act. 1) et le développement/test (Act. 2), l'Activité 3 traite du déploiement et de la sécurisation.",
"Prototype déployé : API Laravel + web Next.js + mobile Expo + PostgreSQL.",
], Inches(0.8), Inches(1.4), Inches(11.7), Inches(2.0), size=18)
tb, tf = textbox(s, Inches(0.8), Inches(3.7), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "3 livrables", 16, GREEN_D, bold=True, space_after=0)
cards(s, [
("Plan de déploiement", ["Environnements", "Versioning", "Automatisations"], GREEN),
("Plan de maintenance", ["Ticketing Gitea", "SLA & méthodologie", "Veille techno"], BLUE),
("Plan de sécurisation", ["Risques & criticité", "Actions correctives", "RGPD & crise"], GREEN_D),
], y=4.3, h=2.0)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 4 — Architecture technique
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Architecture technique de la solution", 2)
bullets(s, [
"Architecture découplée : une API REST centrale, deux clients (web + mobile).",
("Back-end : Laravel 12 (PHP 8.4) — API REST JSON, authentification JWT.", 1),
("Base de données : PostgreSQL 16 (ACID) ; Redis en développement.", 1),
("Front-end web : Next.js 16 / React 19, design system de l'État (DSFR).", 1),
("Mobile : React Native / Expo 54 (iOS + Android, Mobile First).", 1),
"Conteneurisation Docker : environnements reproductibles et isolés.",
"Une seule logique métier → non-duplication, testabilité, évolutivité.",
], Inches(0.8), Inches(1.5), Inches(11.7), Inches(5.0), size=17, gap=9)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 5 — Plan de déploiement : 3 environnements + chaîne
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Plan de déploiement — environnements et chaîne CI/CD", 2)
image_fit(s, "archi_deploiement.png", Inches(0.5), Inches(1.25), Inches(12.3), Inches(5.5),
caption="Développement (local) → Test/Intégration (Gitea Actions) → Production (Coolify, HTTPS)")
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 6 — Versioning & CI/CD
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Versioning et intégration/livraison continues", 3)
bullets(s, [
"Gitea : dépôt Git, ticketing et CI/CD réunis dans un outil souverain auto-hébergé.",
"Stratégie de branches : main (prod, protégée) · develop · feature/fix/hotfix.",
"Conventions : Conventional Commits, revue par pull request, tags de version.",
], Inches(0.8), Inches(1.4), Inches(11.7), Inches(1.9), size=17)
tb, tf = textbox(s, Inches(0.8), Inches(3.55), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "Deux automatisations complémentaires", 16, GREEN_D, bold=True, space_after=0)
cards(s, [
("Intégration continue — Gitea Actions", [
"29 tests à chaque push",
"Lint (Pint, ESLint)",
"Merge bloqué si CI rouge"], ORANGE),
("Déploiement continu — Coolify", [
"Webhook sur merge main",
"Build images + migrations",
"HTTPS, health checks, restart"], BLUE),
], y=4.15, h=2.1)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 7 — Maintenance : ticketing
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Plan de maintenance — ticketing et cycle de vie", 4)
image_fit(s, "cycle_ticket.png", Inches(0.5), Inches(1.25), Inches(12.3), Inches(5.5),
caption="Gitea Issues : gabarits incident/évolution, labels, jalons, Kanban · SLA du contrat")
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 8 — Maintenance : méthodo + veille
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Maintenance corrective, évolutive et veille", 4)
tb, tf = textbox(s, Inches(0.8), Inches(1.3), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "Méthodologie", 16, GREEN_D, bold=True, space_after=0)
bullets(s, [
"Corrective : ticket → qualification (gravité) → SLA → correction → revue (PR + CI) → clôture validée.",
"Évolutive : analyse (documentation, délai, coût) → validation client → planification par version.",
"Pilotage : tableaux de bord Gitea (Kanban, jalons, indicateurs de délais).",
], Inches(0.8), Inches(1.8), Inches(11.7), Inches(2.3), size=16, gap=9)
tb, tf = textbox(s, Inches(0.8), Inches(4.2), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "Veille technologique (pérennité)", 16, GREEN_D, bold=True, space_after=0)
bullets(s, [
"Sécurité : CVE, CERT-FR, OWASP, alertes de dépendances (composer/npm audit en CI).",
"Frameworks & dépendances : notes de version, robot de mises à jour testées par la CI.",
"Réglementation : CNIL, ANSSI (RGPD, HDS).",
], Inches(0.8), Inches(4.7), Inches(11.7), Inches(1.9), size=16, gap=9)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 9 — Sécurisation : vulnérabilités OWASP
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Plan de sécurisation — vulnérabilités (OWASP)", 5)
bullets(s, [
"Contrôle d'accès : RBAC par middleware, garde-fous admin.",
"Cryptographie : HTTPS/TLS, bcrypt (coût 12), mots de passe masqués et hors logs.",
"Injection : ORM paramétré (Eloquent) + validation par Form Requests.",
"Authentification : JWT signé, TTL 60 min, blacklist, rate limiting différencié.",
"Journalisation : piste d'audit immuable (IP, anciennes/nouvelles valeurs).",
"Configuration : APP_DEBUG=false, secrets hors dépôt, CORS en liste blanche.",
], Inches(0.8), Inches(1.5), Inches(11.7), Inches(5.0), size=17, gap=11)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 10 — Matrice de risques
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Sécurisation — matrice de criticité des risques", 5)
image_fit(s, "matrice_risques.png", Inches(2.4), Inches(1.25), Inches(8.5), Inches(5.5),
caption="Criticité = gravité × probabilité · priorisation des actions (R1, R7, R2 prioritaires)")
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 11 — Actions préventives / correctives
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Sécurisation — actions préventives et correctives", 5)
cards(s, [
("Prévention", [
"Validation & ORM (injection)",
"MFA admin, mots de passe robustes",
"audit dépendances en CI",
"Sauvegardes automatisées"], GREEN_D),
("Chiffrement", [
"HTTPS/TLS en transit",
"bcrypt + AES (APP_KEY)",
"Notes de santé chiffrées (prévu)",
"JWT signés"], BLUE),
("Correction", [
"Correctif prioritaire + test",
"Révocation / blacklist token",
"Redéploiement automatisé",
"Plan de reprise (PRA)"], ORANGE),
], y=1.6, h=4.6, top=0.6)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 12 — RGPD
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Données personnelles et RGPD", 6)
bullets(s, [
"Données de santé mentale = données sensibles (RGPD art. 9).",
"Consentement explicite obligatoire à l'inscription, tracé en base.",
"Droits implémentés : accès et portabilité (export JSON), effacement (anonymisation + soft delete).",
"Minimisation des données ; hébergement UE, aucun transfert hors UE ; cible HDS en production.",
], Inches(0.8), Inches(1.45), Inches(11.7), Inches(2.4), size=17, gap=10)
rect(s, Inches(0.8), Inches(4.25), Inches(11.7), Inches(2.3), BLUE_L)
rect(s, Inches(0.8), Inches(4.25), Inches(3.6), Inches(0.55), GREEN_D)
tb, tf = textbox(s, Inches(0.95), Inches(4.28), Inches(3.4), Inches(0.5), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], "Traçabilité (art. 32)", 15, WHITE, bold=True, space_after=0)
bullets(s, [
"Piste d'audit immuable : action, IP, anciennes/nouvelles valeurs — le mot de passe n'est jamais journalisé.",
"Chiffrement en transit, hachage bcrypt, contrôle d'accès par rôle.",
"Export et anonymisation testés automatiquement (tests RGPD).",
], Inches(1.0), Inches(4.95), Inches(11.3), Inches(1.6), size=14, gap=6)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 13 — Gestion de crise & escalade
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Gestion de crise et escalade d'information", 6)
levels = [
("N1 — Détection", "Astreinte technique", "Qualifier, contenir, ouvrir un ticket sécurité", GREEN),
("N2 — Confirmation", "Responsable technique", "Isoler, préserver les preuves, évaluer l'impact", YELLOW),
("N3 — Crise", "Chef de projet + DPO", "Notification CNIL < 72 h, information des personnes", ORANGE),
("N4 — Direction", "Direction + Ministère", "Communication officielle, coordination ANSSI", RED),
]
y = 1.55
for titre, resp, act, col in levels:
rect(s, Inches(0.8), Inches(y), Inches(11.7), Inches(1.15), BLUE_L)
rect(s, Inches(0.8), Inches(y), Inches(0.16), Inches(1.15), col)
tb, tf = textbox(s, Inches(1.1), Inches(y + 0.05), Inches(3.0), Inches(1.05), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], titre, 15, col if col != YELLOW else GREY_D, bold=True, space_after=2)
p = tf.add_paragraph(); set_par(p, resp, 12, GREY_D, space_after=0)
tb, tf = textbox(s, Inches(4.3), Inches(y + 0.05), Inches(8.0), Inches(1.05), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], act, 14, INK, space_after=0)
y += 1.32
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 14 — Démo & conclusion
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Démonstration et conclusion", 7)
tb, tf = textbox(s, Inches(0.8), Inches(1.35), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "Démonstration des outils (en direct)", 17, GREEN_D, bold=True, space_after=0)
bullets(s, [
"Versioning : arbre des commits, branches et tags sur Gitea.",
"Intégration continue : pull request → 29 tests verts (Gitea Actions).",
"Ticketing : création d'une issue (gabarit incident), labels, jalon, Kanban.",
"Déploiement : merge → Coolify → site en ligne (cesizen.cleanows.fr).",
], Inches(0.8), Inches(1.85), Inches(11.7), Inches(2.4), size=16, gap=9)
tb, tf = textbox(s, Inches(0.8), Inches(4.35), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "En synthèse", 17, GREEN_D, bold=True, space_after=0)
bullets(s, [
"Chaîne de déploiement complète, outillée et réellement en production.",
"Maintenance cadrée (ticketing, SLA, veille) et sécurité analysée par les risques + RGPD.",
], Inches(0.8), Inches(4.85), Inches(11.7), Inches(1.4), size=16, gap=8)
rect(s, 0, Inches(6.55), SW, Inches(0.95), BLUE)
tb, tf = textbox(s, Inches(0.8), Inches(6.57), Inches(11.7), Inches(0.9), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], "Merci de votre attention — Questions / Réponses", 20, WHITE,
bold=True, align=PP_ALIGN.CENTER, space_after=0)
# ─── Numéros de page (toutes les slides sauf la page de titre) ─────
for idx, slide in enumerate(prs.slides):
if idx == 0:
continue # pas de numéro sur la slide de titre
tb, tf = textbox(slide, Inches(12.4), Inches(7.05), Inches(0.7), Inches(0.35),
anchor=MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], str(idx + 1), 10, GREY, bold=True,
align=PP_ALIGN.RIGHT, space_after=0)
prs.save(OUT)
print(f"Support de soutenance généré : {OUT}")
print(f"Nombre de slides : {len(prs.slides._sldIdLst)}")