OS
Linux
@@ -82,7 +82,7 @@ const bulkValues = {
domain_code: [{% for d in domains_list %}{v:"{{ d.code }}", l:"{{ d.name }}"},{% endfor %}],
env_code: [{% for e in envs_list %}{v:"{{ e.code }}", l:"{{ e.name }}"},{% endfor %}],
tier: [{v:"tier0",l:"tier0"},{v:"tier1",l:"tier1"},{v:"tier2",l:"tier2"},{v:"tier3",l:"tier3"}],
- etat: [{v:"production",l:"Production"},{v:"implementation",l:"Implementation"},{v:"stock",l:"Stock"},{v:"obsolete",l:"Obsolete"}],
+ etat: [{v:"production",l:"Production"},{v:"implementation",l:"Implementation"},{v:"stock",l:"Stock"},{v:"obsolete",l:"Décommissionné"},{v:"eol",l:"EOL"}],
patch_os_owner: [{v:"secops",l:"secops"},{v:"ipop",l:"ipop"},{v:"na",l:"na"}],
licence_support: [{v:"active",l:"active"},{v:"obsolete",l:"obsolete"},{v:"els",l:"els"}],
};
@@ -135,7 +135,7 @@ function updateBulk() {
{{ s.os_short or '-' }}
{{ s.licence_support }}
{{ s.tier }}
- {{ (s.etat or '')[:8] }}
+ {% if s.etat == 'obsolete' %}Décom.{% elif s.etat == 'eol' %}EOL{% elif s.etat == 'production' %}Prod{% elif s.etat == 'implementation' %}Implm{% elif s.etat == 'stock' %}Stock{% else %}{{ (s.etat or '')[:8] }}{% endif %}
{{ s.patch_os_owner or '-' }}
{{ (s.application_name or '-')[:35] }}
diff --git a/tools/generate_ppt.py b/tools/generate_ppt.py
new file mode 100644
index 0000000..2a38293
--- /dev/null
+++ b/tools/generate_ppt.py
@@ -0,0 +1,834 @@
+"""Generate SANEF Patching presentation (PPTX) from the PDF source material."""
+from pptx import Presentation
+from pptx.util import Inches, Pt, Emu
+from pptx.dml.color import RGBColor
+from pptx.enum.shapes import MSO_SHAPE
+from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
+from pptx.oxml.ns import qn
+from copy import deepcopy
+
+# --- Couleurs SANEF / cyber ---
+ACCENT = RGBColor(0x00, 0xA3, 0xC4) # cyan principal
+ACCENT_DARK = RGBColor(0x00, 0x6F, 0x8C)
+DARK = RGBColor(0x1A, 0x1A, 0x2E) # noir profond
+LIGHT_BG = RGBColor(0xF5, 0xF9, 0xFC)
+RED_ALERT = RGBColor(0xC0, 0x39, 0x2B)
+ORANGE = RGBColor(0xE0, 0x8A, 0x0A)
+GREEN = RGBColor(0x28, 0xA7, 0x45)
+GRAY = RGBColor(0x66, 0x66, 0x66)
+WHITE = RGBColor(0xFF, 0xFF, 0xFF)
+LIGHT_GRAY = RGBColor(0xEE, 0xEE, 0xEE)
+
+prs = Presentation()
+prs.slide_width = Inches(13.333)
+prs.slide_height = Inches(7.5)
+SW, SH = prs.slide_width, prs.slide_height
+
+blank_layout = prs.slide_layouts[6]
+
+
+def set_bg(slide, color):
+ bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SW, SH)
+ bg.fill.solid()
+ bg.fill.fore_color.rgb = color
+ bg.line.fill.background()
+ bg.shadow.inherit = False
+ slide.shapes._spTree.remove(bg._element)
+ slide.shapes._spTree.insert(2, bg._element)
+ return bg
+
+
+def add_rect(slide, x, y, w, h, fill=None, line=None, line_w=None):
+ shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, w, h)
+ if fill is None:
+ shape.fill.background()
+ else:
+ shape.fill.solid()
+ shape.fill.fore_color.rgb = fill
+ if line is None:
+ shape.line.fill.background()
+ else:
+ shape.line.color.rgb = line
+ if line_w:
+ shape.line.width = line_w
+ shape.shadow.inherit = False
+ return shape
+
+
+def add_text(slide, x, y, w, h, text, *, size=18, bold=False, color=DARK,
+ align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP, font="Calibri"):
+ tb = slide.shapes.add_textbox(x, y, w, h)
+ tf = tb.text_frame
+ tf.word_wrap = True
+ tf.margin_left = Emu(50000)
+ tf.margin_right = Emu(50000)
+ tf.margin_top = Emu(30000)
+ tf.margin_bottom = Emu(30000)
+ tf.vertical_anchor = anchor
+ lines = text.split("\n") if isinstance(text, str) else text
+ for i, ln in enumerate(lines):
+ p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
+ p.alignment = align
+ r = p.add_run()
+ r.text = ln
+ r.font.name = font
+ r.font.size = Pt(size)
+ r.font.bold = bold
+ r.font.color.rgb = color
+ return tb
+
+
+def add_title_bar(slide, title, subtitle=None):
+ # top accent bar
+ add_rect(slide, 0, 0, SW, Inches(0.15), fill=ACCENT)
+ # title
+ add_text(slide, Inches(0.5), Inches(0.3), Inches(12), Inches(0.7), title,
+ size=28, bold=True, color=DARK)
+ if subtitle:
+ add_text(slide, Inches(0.5), Inches(0.95), Inches(12), Inches(0.45), subtitle,
+ size=14, color=GRAY)
+ # divider
+ add_rect(slide, Inches(0.5), Inches(1.45), Inches(1.5), Emu(30000), fill=ACCENT)
+
+
+def add_footer(slide, page_num):
+ add_text(slide, Inches(0.5), Inches(7.05), Inches(7), Inches(0.3),
+ "SANEF DSI / SECOPS — Processus Patching & Automatisation",
+ size=9, color=GRAY)
+ add_text(slide, Inches(11.5), Inches(7.05), Inches(1.3), Inches(0.3),
+ f"{page_num}", size=9, color=GRAY, align=PP_ALIGN.RIGHT)
+
+
+def add_bullets(slide, x, y, w, h, items, *, size=16, color=DARK, line_spacing=1.2):
+ tb = slide.shapes.add_textbox(x, y, w, h)
+ tf = tb.text_frame
+ tf.word_wrap = True
+ for i, item in enumerate(items):
+ if isinstance(item, tuple):
+ bullet_color, text = item
+ else:
+ bullet_color, text = ACCENT, item
+ p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
+ p.alignment = PP_ALIGN.LEFT
+ p.line_spacing = line_spacing
+ r1 = p.add_run()
+ r1.text = "▸ "
+ r1.font.size = Pt(size)
+ r1.font.bold = True
+ r1.font.color.rgb = bullet_color
+ r1.font.name = "Calibri"
+ r2 = p.add_run()
+ r2.text = text
+ r2.font.size = Pt(size)
+ r2.font.color.rgb = color
+ r2.font.name = "Calibri"
+ return tb
+
+
+def add_table(slide, x, y, w, h, header, rows, *,
+ header_color=ACCENT, alt_row=RGBColor(0xF5, 0xF9, 0xFC),
+ header_text=WHITE, font_size=12, header_size=13):
+ ncols = len(header)
+ nrows = len(rows) + 1
+ tbl = slide.shapes.add_table(nrows, ncols, x, y, w, h).table
+ # Header row
+ for ci, htxt in enumerate(header):
+ c = tbl.cell(0, ci)
+ c.fill.solid()
+ c.fill.fore_color.rgb = header_color
+ c.text = ""
+ tf = c.text_frame
+ tf.margin_left = Emu(50000); tf.margin_right = Emu(50000)
+ tf.margin_top = Emu(40000); tf.margin_bottom = Emu(40000)
+ p = tf.paragraphs[0]
+ p.alignment = PP_ALIGN.CENTER
+ r = p.add_run(); r.text = htxt
+ r.font.size = Pt(header_size); r.font.bold = True
+ r.font.color.rgb = header_text; r.font.name = "Calibri"
+ # Data rows
+ for ri, row in enumerate(rows, start=1):
+ for ci, cell_data in enumerate(row):
+ c = tbl.cell(ri, ci)
+ c.fill.solid()
+ c.fill.fore_color.rgb = alt_row if ri % 2 == 0 else WHITE
+ if isinstance(cell_data, tuple):
+ text, color = cell_data
+ else:
+ text, color = cell_data, DARK
+ c.text = ""
+ tf = c.text_frame
+ tf.margin_left = Emu(50000); tf.margin_right = Emu(50000)
+ tf.margin_top = Emu(30000); tf.margin_bottom = Emu(30000)
+ tf.word_wrap = True
+ p = tf.paragraphs[0]
+ p.alignment = PP_ALIGN.LEFT if ci == 0 else PP_ALIGN.CENTER
+ r = p.add_run(); r.text = text
+ r.font.size = Pt(font_size)
+ r.font.color.rgb = color
+ r.font.name = "Calibri"
+ if ci == 0:
+ r.font.bold = True
+ return tbl
+
+
+def add_callout(slide, x, y, w, h, title, body, *, bg=LIGHT_BG, border=ACCENT,
+ title_color=ACCENT_DARK):
+ # left accent bar
+ add_rect(slide, x, y, Emu(70000), h, fill=border)
+ # box
+ box = add_rect(slide, x + Emu(70000), y, w - Emu(70000), h, fill=bg)
+ # title
+ add_text(slide, x + Inches(0.25), y + Inches(0.1), w - Inches(0.4), Inches(0.4),
+ title, size=14, bold=True, color=title_color)
+ # body
+ add_text(slide, x + Inches(0.25), y + Inches(0.5), w - Inches(0.4), h - Inches(0.6),
+ body, size=12, color=DARK)
+
+
+def add_kpi(slide, x, y, w, h, value, label, color=ACCENT):
+ add_rect(slide, x, y, w, h, fill=WHITE, line=color, line_w=Pt(1.5))
+ add_text(slide, x, y + Inches(0.2), w, Inches(0.8), value,
+ size=36, bold=True, color=color, align=PP_ALIGN.CENTER)
+ add_text(slide, x, y + Inches(1.0), w, Inches(0.4), label,
+ size=12, color=GRAY, align=PP_ALIGN.CENTER)
+
+
+# ============================================================
+# SLIDE 1 — Title
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+set_bg(slide, DARK)
+# Accent graphic
+add_rect(slide, 0, Inches(2.8), SW, Inches(0.05), fill=ACCENT)
+add_rect(slide, Inches(5.5), Inches(2.3), Inches(2.3), Inches(0.05), fill=ACCENT)
+
+add_text(slide, Inches(0.8), Inches(1.5), Inches(11.5), Inches(1),
+ "PROCESSUS DE PATCHING", size=46, bold=True, color=WHITE)
+add_text(slide, Inches(0.8), Inches(2.4), Inches(11.5), Inches(0.7),
+ "Manuel vs Automatisation", size=26, color=ACCENT, bold=False)
+
+add_text(slide, Inches(0.8), Inches(3.2), Inches(11.5), Inches(0.5),
+ "État des lieux, charge réelle du patcheur & apport de PatchCenter Web",
+ size=18, color=LIGHT_GRAY)
+
+# Bottom info
+add_rect(slide, 0, Inches(6.5), SW, Inches(1), fill=RGBColor(0x0a, 0x14, 0x24))
+add_text(slide, Inches(0.8), Inches(6.65), Inches(6), Inches(0.4),
+ "SANEF DSI / Sécurité Opérationnelle", size=14, bold=True, color=WHITE)
+add_text(slide, Inches(0.8), Inches(7.0), Inches(6), Inches(0.3),
+ "Présentation DSI — Avril 2026", size=11, color=LIGHT_GRAY)
+add_text(slide, Inches(9), Inches(6.65), Inches(3.8), Inches(0.4),
+ "PatchCenter Web v1", size=14, bold=True, color=ACCENT, align=PP_ALIGN.RIGHT)
+add_text(slide, Inches(9), Inches(7.0), Inches(3.8), Inches(0.3),
+ "Équipe SECOPS", size=11, color=LIGHT_GRAY, align=PP_ALIGN.RIGHT)
+
+
+# ============================================================
+# SLIDE 2 — Sommaire
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "Sommaire", "Déroulé de la présentation")
+
+items = [
+ ("1.", "Vue d'ensemble du processus de patching"),
+ ("2.", "Phase 1 — Affectation (COMEP jeudi)"),
+ ("3.", "Phase 2 — Préparation (jeudi PM + vendredi)"),
+ ("4.", "Phase 3 — Exécution Jour J (lundi → jeudi)"),
+ ("5.", "Phase 4 — Nettoyage (vendredi)"),
+ ("6.", "Synthèse — Temps par serveur"),
+ ("7.", "La réalité du patcheur — budget 35 h / semaine"),
+ ("8.", "Équation insoluble en mode manuel"),
+ ("9.", "Conséquence sécurité — alertes S1 & Defender"),
+ ("10.", "Apport des outils — SQATM .exe & PatchCenter Web"),
+ ("11.", "Comparaison chiffrée et impact hebdomadaire"),
+ ("12.", "Feuille de route & conclusion"),
+]
+for i, (num, txt) in enumerate(items):
+ y = Inches(1.8 + 0.38 * i)
+ add_text(slide, Inches(0.8), y, Inches(0.6), Inches(0.4), num,
+ size=15, bold=True, color=ACCENT)
+ add_text(slide, Inches(1.4), y, Inches(11), Inches(0.4), txt,
+ size=15, color=DARK)
+add_footer(slide, 2)
+
+
+# ============================================================
+# SLIDE 3 — Vue d'ensemble
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "Vue d'ensemble", "4 phases étalées sur la semaine")
+
+phases = [
+ ("1. Affectation", "Jeudi 14h00\nCOMEP SECOPS", ACCENT),
+ ("2. Préparation", "Jeudi PM + Vendredi\nQualification + validation", ORANGE),
+ ("3. Exécution", "Lundi → Jeudi\nPatching + vérifications", RED_ALERT),
+ ("4. Nettoyage", "Vendredi (J+3)\nSnapshots + kernels", GREEN),
+]
+x0 = Inches(0.6)
+card_w = Inches(3.0)
+card_h = Inches(3.5)
+gap = Inches(0.15)
+for i, (title, body, col) in enumerate(phases):
+ x = x0 + (card_w + gap) * i
+ y = Inches(2.2)
+ # Card with colored top
+ add_rect(slide, x, y, card_w, Inches(0.6), fill=col)
+ add_rect(slide, x, y + Inches(0.6), card_w, card_h - Inches(0.6), fill=WHITE, line=LIGHT_GRAY)
+ add_text(slide, x, y + Inches(0.1), card_w, Inches(0.5), title,
+ size=18, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
+ # Number big
+ add_text(slide, x, y + Inches(0.9), card_w, Inches(1.2), str(i + 1),
+ size=72, bold=True, color=col, align=PP_ALIGN.CENTER)
+ # Body
+ add_text(slide, x + Inches(0.2), y + Inches(2.2), card_w - Inches(0.4), Inches(1.2),
+ body, size=13, color=DARK, align=PP_ALIGN.CENTER)
+
+add_callout(slide, Inches(0.6), Inches(5.9), Inches(12.1), Inches(0.8),
+ "Règle de cadrage",
+ "Pas de patching le vendredi — évite un incident qui courrait tout le weekend. "
+ "Le vendredi = finalisation pré-patching + nettoyage snapshots + iTop / météo.")
+add_footer(slide, 3)
+
+
+# ============================================================
+# SLIDE 4 — Affectation COMEP
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "Phase 1 — Affectation", "Jeudi 14h00 — COMEP SECOPS")
+
+add_bullets(slide, Inches(0.7), Inches(1.9), Inches(12), Inches(2.5), [
+ "COMEP du jeudi après-midi : répartition des serveurs à patcher entre les intervenants",
+ "Chaque intervenant reçoit sa liste pour la semaine suivante",
+ "Objectif cible par intervenant : 20 serveurs patchés sur la semaine",
+ "La phase de qualification démarre immédiatement après le COMEP (jeudi PM) puis toute la journée de vendredi",
+], size=16)
+
+add_callout(slide, Inches(0.7), Inches(5.0), Inches(12), Inches(1.5),
+ "Livrable attendu",
+ "Liste de 20 serveurs par intervenant, avec domaine, environnement, "
+ "OS, responsable applicatif et criticité — prête pour la qualification.")
+add_footer(slide, 4)
+
+
+# ============================================================
+# SLIDE 5 — Préparation (pré-patching)
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "Phase 2 — Préparation (pré-patching)",
+ "Jeudi après-midi + Vendredi — 13 étapes par serveur")
+
+header = ["Étape", "Action", "Durée"]
+rows = [
+ ("1", "Recherche serveur dans console Qualys", "1 min"),
+ ("2-3", "Lecture + analyse des vulnérabilités", "5 min"),
+ ("4", "Connexion SSH (PuTTY)", "1 min"),
+ ("5", "Vérification espace disque (df -h)", "1 min"),
+ ("6", "Vérification connexion satellite", "1 min"),
+ ("7", "Dry-run yum check-update", "2-3 min"),
+ ("8", "Vérification accès vCenter", "2 min"),
+ ("9", "Vérification backup Commvault (si physique)", "2 min"),
+ ("10", "Vérification Centreon (si production)", "2 min"),
+ ("11-12", "Contact responsable + proposition créneaux", "5-10 min"),
+ ("13", "Attente validation du créneau (asynchrone)", "variable"),
+]
+add_table(slide, Inches(0.5), Inches(1.8), Inches(7.5), Inches(4.6),
+ header, rows, font_size=11, header_size=12)
+
+# KPI right
+add_kpi(slide, Inches(8.3), Inches(2.0), Inches(4.5), Inches(1.6),
+ "20-30 min", "Par serveur (travail actif)", color=ACCENT)
+add_kpi(slide, Inches(8.3), Inches(3.8), Inches(4.5), Inches(1.6),
+ "6h40 à 10h", "Sur 20 serveurs / semaine", color=ORANGE)
+
+add_callout(slide, Inches(0.5), Inches(6.55), Inches(12.3), Inches(0.45),
+ "Hors attente validation",
+ "La validation asynchrone peut prendre 1 h à 48 h — non comptabilisée ici.")
+add_footer(slide, 5)
+
+
+# ============================================================
+# SLIDE 6 — Exécution Jour J
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "Phase 3 — Exécution (Jour J)",
+ "Lundi → Jeudi — 13 étapes par serveur")
+
+header = ["Étape", "Action", "Durée"]
+rows = [
+ ("1", "Information début d'intervention (Teams)", "1 min"),
+ ("2", "Connexion vCenter + prise de snapshot", "3-5 min"),
+ ("3", "Mise en maintenance Centreon (si prod)", "2 min"),
+ ("4-5", "SSH + snap pré-patch (services/process/ports)", "6 min"),
+ ("6", "Lancement de la mise à jour (yum update)", "5-15 min"),
+ ("7-8", "Info Teams + reboot du serveur", "3-6 min"),
+ ("9", "Attente + reconnexion", "3-5 min"),
+ ("10", "Check post-patch + Centreon", "5-10 min"),
+ ("11", "Demande validation responsable applicatif", "5-15 min"),
+ ("12-13", "Marquage serveur patché + Teams fin", "3 min"),
+]
+add_table(slide, Inches(0.5), Inches(1.8), Inches(7.5), Inches(4.3),
+ header, rows, font_size=11, header_size=12)
+
+add_kpi(slide, Inches(8.3), Inches(2.0), Inches(4.5), Inches(1.6),
+ "35-65 min", "Par serveur (travail actif)", color=RED_ALERT)
+add_kpi(slide, Inches(8.3), Inches(3.8), Inches(4.5), Inches(1.6),
+ "11h40 à 21h40", "Sur 20 serveurs / semaine", color=ORANGE)
+
+add_callout(slide, Inches(0.5), Inches(6.25), Inches(12.3), Inches(0.75),
+ "Pourquoi pas le vendredi ?",
+ "Le Jour J est strictement limité à lundi → jeudi : au moins 24 h de recul "
+ "avant le weekend pour détecter une régression post-patch.")
+add_footer(slide, 6)
+
+
+# ============================================================
+# SLIDE 7 — Nettoyage Vendredi
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "Phase 4 — Nettoyage", "Vendredi — traitement groupé de la semaine")
+
+add_bullets(slide, Inches(0.7), Inches(1.9), Inches(12), Inches(2.5), [
+ "Suppression des anciens snapshots vCenter de tous les serveurs patchés la semaine (~ 2 min/serveur)",
+ "Suppression des anciens kernels (package-cleanup --oldkernels) (~ 3 min/serveur)",
+ "Au total : ~ 1h40 par semaine pour le lot de 20 serveurs",
+], size=16)
+
+add_callout(slide, Inches(0.7), Inches(4.5), Inches(12), Inches(2.3),
+ "Pourquoi regrouper le nettoyage le vendredi ?",
+ "• Recul suffisant — le snapshot reste disponible au moins 24 h pour permettre un rollback "
+ "si une anomalie est détectée après coup.\n\n"
+ "• Contexte mental unique — le patcheur traite tous ses snapshots d'un bloc au lieu "
+ "d'y revenir serveur par serveur, gain de concentration.")
+add_footer(slide, 7)
+
+
+# ============================================================
+# SLIDE 8 — Synthèse temps par serveur
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "Synthèse — Temps par serveur (manuel)",
+ "Cumul des 3 phases actives + nettoyage")
+
+header = ["Phase", "Temps actif", "Attente asynchrone"]
+rows = [
+ ("Préparation (qualif + validation)", "20-30 min", "1 à 48 h (async)"),
+ ("Exécution Jour J", "35-65 min", "—"),
+ ("Nettoyage J+3", "5 min", "—"),
+ (("TOTAL PAR SERVEUR", ACCENT_DARK), ("60-100 min", ACCENT_DARK),
+ ("≈ 1h15 moyenne", ACCENT_DARK)),
+]
+add_table(slide, Inches(0.7), Inches(2.0), Inches(11.9), Inches(2.6),
+ header, rows, font_size=14, header_size=14)
+
+# Big KPI row
+add_kpi(slide, Inches(0.7), Inches(5.0), Inches(3.9), Inches(1.8),
+ "1h15", "Par serveur (moyenne)", color=ACCENT)
+add_kpi(slide, Inches(4.7), Inches(5.0), Inches(3.9), Inches(1.8),
+ "20", "Serveurs / semaine / patcheur", color=ORANGE)
+add_kpi(slide, Inches(8.7), Inches(5.0), Inches(3.9), Inches(1.8),
+ "25h", "Charge hebdo patching seul", color=RED_ALERT)
+add_footer(slide, 8)
+
+
+# ============================================================
+# SLIDE 9 — Réalité du patcheur
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "La réalité du patcheur", "Budget horaire hebdomadaire : 35 h")
+
+# Budget visual
+add_text(slide, Inches(0.7), Inches(1.8), Inches(12), Inches(0.4),
+ "Répartition de la semaine (Lundi → Vendredi, 7h / jour)",
+ size=14, bold=True, color=DARK)
+
+# Stack bar
+bar_y = Inches(2.3)
+bar_h = Inches(0.9)
+bar_x = Inches(0.7)
+bar_w_total = Inches(11.9)
+# Lun-Jeu
+w1 = Inches(11.9 * 28 / 35)
+add_rect(slide, bar_x, bar_y, w1, bar_h, fill=ACCENT)
+add_text(slide, bar_x, bar_y, w1, bar_h, "Lundi → Jeudi — 28 h (Jour J + fin de prépa)",
+ size=12, bold=True, color=WHITE, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
+# Jeu PM
+w2 = Inches(11.9 * 3.5 / 35)
+add_rect(slide, bar_x + w1 - Inches(11.9 * 3.5 / 35), bar_y, w2, bar_h, fill=ORANGE)
+add_text(slide, bar_x + w1 - Inches(11.9 * 3.5 / 35), bar_y, w2, bar_h, "Jeu PM 3.5h",
+ size=10, bold=True, color=WHITE, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
+# Vendredi
+w3 = Inches(11.9 * 7 / 35)
+add_rect(slide, bar_x + w1, bar_y, w3, bar_h, fill=GREEN)
+add_text(slide, bar_x + w1, bar_y, w3, bar_h, "Vendredi — 7 h (prépa + cleanup)",
+ size=12, bold=True, color=WHITE, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
+
+# Charge théorique table
+add_text(slide, Inches(0.7), Inches(3.5), Inches(12), Inches(0.4),
+ "Charge théorique patching — 20 serveurs",
+ size=14, bold=True, color=DARK)
+
+header = ["Poste", "Volume", "Temps unitaire", "Total hebdo"]
+rows = [
+ ("Préparation (Jeu PM + Vendredi)", "20", "20-30 min", "6h40 – 10h"),
+ ("Exécution Jour J (Lun-Jeu)", "20", "35-65 min", "11h40 – 21h40"),
+ ("Nettoyage snapshots (Vendredi)", "20", "5 min", "1h40"),
+ (("TOTAL patching seul", ACCENT_DARK), "—", "—", ("20h – 33h", ACCENT_DARK)),
+ (("Budget hebdo disponible", GREEN), "—", "—", ("35h (Lun-Ven)", GREEN)),
+]
+add_table(slide, Inches(0.7), Inches(3.95), Inches(11.9), Inches(2.8),
+ header, rows, font_size=12, header_size=13)
+
+add_text(slide, Inches(0.7), Inches(6.85), Inches(12), Inches(0.3),
+ "Le patching SEUL consomme 57 à 95% du temps disponible.",
+ size=13, bold=True, color=RED_ALERT, align=PP_ALIGN.CENTER)
+add_footer(slide, 9)
+
+
+# ============================================================
+# SLIDE 10 — Missions annexes
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "Mais le patcheur n'est pas que patcheur",
+ "Autres missions SECOPS obligatoires")
+
+header = ["Mission", "Fréquence", "Charge hebdo"]
+rows = [
+ ("Tickets iTop — sécurisation serveur", "Permanent", "2 à 5 h"),
+ ("Mise à jour agent Qualys (ponctuelle)", "Hebdomadaire", "1 à 3 h"),
+ ("Mise à jour agent SentinelOne", "Hebdomadaire", "1 à 3 h"),
+ ("Tour de garde — alertes SentinelOne / Defender", "Rotation", "3 à 6 h (sur garde)"),
+ ("Météo sécurité — veille + reporting hebdo", "Rotation", "2 à 4 h (sur météo)"),
+ (("TOTAL missions annexes", ACCENT_DARK), "—", ("9 à 21 h / semaine", ACCENT_DARK)),
+]
+add_table(slide, Inches(0.7), Inches(2.0), Inches(11.9), Inches(3.3),
+ header, rows, font_size=13, header_size=14)
+
+add_callout(slide, Inches(0.7), Inches(5.6), Inches(11.9), Inches(1.2),
+ "Équation insoluble",
+ "Budget disponible : 35 h | Patching : 20-33 h | Missions annexes : 9-21 h "
+ "➜ Charge totale : 29 à 54 h / semaine.\n"
+ "Surcharge structurelle dès que patching + garde + iTop se cumulent.",
+ bg=RGBColor(0xFD, 0xEB, 0xEB), border=RED_ALERT,
+ title_color=RED_ALERT)
+add_footer(slide, 10)
+
+
+# ============================================================
+# SLIDE 11 — Conséquence sécurité
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "Conséquence sécurité directe",
+ "Le patcheur n'a plus le temps d'analyser les alertes S1 & Defender")
+
+add_rect(slide, Inches(0.7), Inches(1.9), Inches(12), Inches(0.1), fill=RED_ALERT)
+add_text(slide, Inches(0.7), Inches(2.1), Inches(12), Inches(0.5),
+ "⚠ Impact opérationnel sur le tour de garde EDR",
+ size=18, bold=True, color=RED_ALERT)
+
+add_bullets(slide, Inches(0.7), Inches(2.7), Inches(12), Inches(3), [
+ (RED_ALERT, "Alertes SentinelOne / Defender triées en surface — on coche la criticité sans investiguer"),
+ (RED_ALERT, "Faux positifs non requalifiés — le bruit de fond masque les vrais incidents"),
+ (RED_ALERT, "Vrais incidents détectés tardivement — ransomware, exfiltration, lateral movement"),
+ (RED_ALERT, "Pas d'enrichissement de contexte (corrélation process / réseau / utilisateur)"),
+ (RED_ALERT, "Les règles de détection ne sont pas affinées en retour d'expérience"),
+], size=14)
+
+add_callout(slide, Inches(0.7), Inches(5.7), Inches(12), Inches(1.4),
+ "Coût potentiel",
+ "Une alerte EDR ratée = incident de sécurité majeur, fuite de données, "
+ "indisponibilité.\n"
+ "Sans commune mesure avec le coût de l'automatisation du patching.",
+ bg=RGBColor(0xFD, 0xEB, 0xEB), border=RED_ALERT, title_color=RED_ALERT)
+add_footer(slide, 11)
+
+
+# ============================================================
+# SLIDE 12 — SQATM .exe
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "Apport SQATM .exe", "Déjà en production — équipe SECOPS")
+
+add_text(slide, Inches(0.7), Inches(1.9), Inches(12), Inches(0.5),
+ "L'outil SANEF Qualys API Tags Management automatise la phase QUALIFICATION :",
+ size=14, color=DARK)
+
+header = ["Étape manuelle", "Automatisé par SQATM", "Gain"]
+rows = [
+ ("Recherche Qualys + lecture vulnérabilités", "Décodeur + API Qualys", "5 min → 30 s"),
+ ("Check SSH disque / satellite / dry-run", "Audit global multi-serveurs", "5 min → 10 s"),
+ ("Tagging ENV / OS / POS / EQT", "Tag Rules + décodeur nomenclature", "15 min → instantané"),
+ ("Identification exposition internet", "Plan de patching + règles", "3 min → instantané"),
+]
+add_table(slide, Inches(0.7), Inches(2.5), Inches(11.9), Inches(2.5),
+ header, rows, font_size=12, header_size=13)
+
+add_kpi(slide, Inches(3.2), Inches(5.3), Inches(7), Inches(1.5),
+ "60-70%", "Gain sur la phase préparation", color=ACCENT)
+add_footer(slide, 12)
+
+
+# ============================================================
+# SLIDE 13 — PatchCenter Web capabilities
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "Apport PatchCenter Web",
+ "Orchestration end-to-end — en cours de déploiement")
+
+# 3 colonnes
+col_w = Inches(4.2)
+col_y = Inches(2.0)
+col_h = Inches(4.5)
+titles = ["Préparation", "Jour J", "Gouvernance"]
+colors = [ACCENT, ORANGE, GREEN]
+contents = [
+ [
+ "Vue unifiée serveurs (Qualys + OS + resp.)",
+ "Audit SSH parallélisé 100+ serveurs",
+ "Correspondance prod ↔ hors-prod auto",
+ "Workflow de validation créneau tracé",
+ "Tags Qualys dynamiques auto-appliqués",
+ ],
+ [
+ "Snapshot vCenter via API",
+ "Maintenance Centreon via API",
+ "Notifications Teams automatiques",
+ "Snap pré/post archivé (services/ports)",
+ "yum update en job asynchrone",
+ "Reboot + reconnexion orchestrés",
+ "Détection régression post-reboot",
+ ],
+ [
+ "Validation prod ↔ hors-prod bloquante",
+ "Historique complet par serveur",
+ "Audit log exportable (conformité)",
+ "Tableau de suivi temps réel",
+ "Nettoyage snapshots + kernels auto",
+ "Multi-profils (admin/coord/op/viewer)",
+ "Authentification LDAP AD SANEF",
+ ],
+]
+for i, (t, col, items) in enumerate(zip(titles, colors, contents)):
+ x = Inches(0.5 + i * 4.3)
+ add_rect(slide, x, col_y, col_w, Inches(0.6), fill=col)
+ add_text(slide, x, col_y + Inches(0.1), col_w, Inches(0.45), t,
+ size=18, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
+ add_rect(slide, x, col_y + Inches(0.6), col_w, col_h - Inches(0.6),
+ fill=WHITE, line=LIGHT_GRAY)
+ tb = slide.shapes.add_textbox(x + Inches(0.15), col_y + Inches(0.75),
+ col_w - Inches(0.3), col_h - Inches(0.85))
+ tf = tb.text_frame
+ tf.word_wrap = True
+ for j, it in enumerate(items):
+ p = tf.paragraphs[0] if j == 0 else tf.add_paragraph()
+ p.space_after = Pt(6)
+ r1 = p.add_run(); r1.text = "✓ "
+ r1.font.size = Pt(12); r1.font.bold = True; r1.font.color.rgb = col
+ r2 = p.add_run(); r2.text = it
+ r2.font.size = Pt(12); r2.font.color.rgb = DARK
+ r2.font.name = "Calibri"
+
+add_footer(slide, 13)
+
+
+# ============================================================
+# SLIDE 14 — Comparaison chiffrée par serveur
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "Comparaison chiffrée", "Temps par serveur — manuel vs automatisé")
+
+header = ["Étape", "Manuel", "SQATM .exe", "PatchCenter Web"]
+rows = [
+ ("Qualification (prep)", "20-30 min", "6-10 min", ("2-3 min", GREEN)),
+ ("Snapshot + maintenance", "5-7 min", "5-7 min", ("30 s (auto)", GREEN)),
+ ("Snap pré/post-patch", "10-15 min", "10-15 min", ("1 min (auto)", GREEN)),
+ ("Update + reboot + reconnexion", "10-25 min", "10-25 min", "10-25 min"),
+ ("Notifications + suivi", "5 min", "5 min", ("0 (auto)", GREEN)),
+ ("Nettoyage J+3", "5 min", "5 min", ("1 min (auto)", GREEN)),
+ (("TOTAL / SERVEUR", ACCENT_DARK),
+ ("60-100 min", RED_ALERT),
+ ("40-70 min", ORANGE),
+ ("15-30 min", GREEN)),
+ (("GAIN vs manuel", ACCENT_DARK), "—", ("-30%", ORANGE), ("-70 à -75%", GREEN)),
+]
+add_table(slide, Inches(0.5), Inches(1.9), Inches(12.3), Inches(4.8),
+ header, rows, font_size=12, header_size=13)
+
+add_text(slide, Inches(0.5), Inches(6.85), Inches(12.3), Inches(0.3),
+ "Le gain vient surtout de la suppression des gestes répétitifs (snapshot, Centreon, notifs, suivi).",
+ size=12, bold=True, color=ACCENT_DARK, align=PP_ALIGN.CENTER)
+add_footer(slide, 14)
+
+
+# ============================================================
+# SLIDE 15 — Impact hebdo (20 serveurs)
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "Impact hebdomadaire", "Objectif 20 serveurs/semaine — budget 35 h")
+
+header = ["Scénario", "Patching seul", "Budget restant", "Missions annexes ?"]
+rows = [
+ (("Manuel", RED_ALERT), ("20h à 33h", RED_ALERT), ("+2h à +15h", ORANGE),
+ ("Non — surcharge garantie en garde ou météo", RED_ALERT)),
+ (("SQATM .exe", ORANGE), ("14h à 23h", ORANGE), ("+12h à +21h", ORANGE),
+ ("Partiellement", ORANGE)),
+ (("PatchCenter Web", GREEN), ("5h à 10h", GREEN), ("+25h à +30h", GREEN),
+ ("Oui — marge pour tout absorber", GREEN)),
+]
+add_table(slide, Inches(0.5), Inches(2.0), Inches(12.3), Inches(2.7),
+ header, rows, font_size=13, header_size=14)
+
+# KPI summary
+add_kpi(slide, Inches(0.7), Inches(5.0), Inches(3.9), Inches(1.8),
+ "25-30h", "Libérées/semaine avec PC Web", color=GREEN)
+add_kpi(slide, Inches(4.7), Inches(5.0), Inches(3.9), Inches(1.8),
+ "x3 à x4", "Réduction du temps / serveur", color=ACCENT)
+add_kpi(slide, Inches(8.7), Inches(5.0), Inches(3.9), Inches(1.8),
+ "~0.5 ETP", "Équivalent sur 200 serv/mois", color=ACCENT_DARK)
+add_footer(slide, 15)
+
+
+# ============================================================
+# SLIDE 16 — Bénéfices qualitatifs
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "Bénéfices qualitatifs",
+ "Au-delà du temps gagné — valeur stratégique")
+
+benefits = [
+ ("Fiabilité", "Plus d'oubli de snapshot, de maintenance Centreon ou de marquage statut", ACCENT),
+ ("Traçabilité", "Historique complet par serveur, exportable pour audit", ACCENT),
+ ("Gouvernance", "Workflow de validation prod ↔ hors-prod bloquant", ORANGE),
+ ("Scalabilité", "1 intervenant pilote 20+ serveurs en parallèle", ORANGE),
+ ("Onboarding", "Nouveau patcheur opérationnel en quelques jours", GREEN),
+ ("Qualité", "Snap pré/post standardisé → détection fiable des régressions", GREEN),
+ ("Conformité", "Tags Qualys automatiques → reporting KPI sans retraitement", ACCENT_DARK),
+ ("Communication", "Notifications Teams standardisées → meilleure visibilité projet", ACCENT_DARK),
+]
+
+# Grid 2x4
+cw = Inches(6.0); ch = Inches(1.1); gap = Inches(0.15)
+for i, (t, b, col) in enumerate(benefits):
+ row = i // 2
+ colpos = i % 2
+ x = Inches(0.5) + (cw + gap) * colpos
+ y = Inches(1.9) + (ch + gap) * row
+ add_rect(slide, x, y, Emu(80000), ch, fill=col)
+ add_rect(slide, x + Emu(80000), y, cw - Emu(80000), ch, fill=WHITE, line=LIGHT_GRAY)
+ add_text(slide, x + Inches(0.25), y + Inches(0.1), cw - Inches(0.35), Inches(0.4),
+ t, size=14, bold=True, color=col)
+ add_text(slide, x + Inches(0.25), y + Inches(0.45), cw - Inches(0.35), Inches(0.65),
+ b, size=11, color=DARK)
+add_footer(slide, 16)
+
+
+# ============================================================
+# SLIDE 17 — Feuille de route
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "Feuille de route", "Avancement actuel & prochaines étapes")
+
+# Two columns: done + to do
+add_rect(slide, Inches(0.5), Inches(1.9), Inches(6.1), Inches(5), fill=LIGHT_BG, line=LIGHT_GRAY)
+add_rect(slide, Inches(0.5), Inches(1.9), Inches(6.1), Inches(0.5), fill=GREEN)
+add_text(slide, Inches(0.5), Inches(1.95), Inches(6.1), Inches(0.4),
+ "✓ Déjà livré", size=16, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
+
+done_items = [
+ "SQATM .exe v2.0.0 — en production (tagging + audit)",
+ "PatchCenter Web : catalogue serveurs complet (1165)",
+ "PatchCenter Web : correspondance prod ↔ hors-prod",
+ "PatchCenter Web : exclusions patch par serveur",
+ "PatchCenter Web : workflow validations",
+ "PatchCenter Web : agents Qualys + déploiement",
+ "Intégration iTop bidirectionnelle (serveurs/apps/statuts)",
+ "Authentification LDAP AD SANEF multi-profils",
+]
+tb = slide.shapes.add_textbox(Inches(0.7), Inches(2.55), Inches(5.8), Inches(4.2))
+tf = tb.text_frame; tf.word_wrap = True
+for j, it in enumerate(done_items):
+ p = tf.paragraphs[0] if j == 0 else tf.add_paragraph()
+ p.space_after = Pt(8)
+ r1 = p.add_run(); r1.text = "✓ "
+ r1.font.size = Pt(12); r1.font.bold = True; r1.font.color.rgb = GREEN
+ r2 = p.add_run(); r2.text = it
+ r2.font.size = Pt(12); r2.font.color.rgb = DARK
+
+# À venir
+add_rect(slide, Inches(6.8), Inches(1.9), Inches(6.1), Inches(5), fill=RGBColor(0xFF, 0xFA, 0xEE), line=LIGHT_GRAY)
+add_rect(slide, Inches(6.8), Inches(1.9), Inches(6.1), Inches(0.5), fill=ORANGE)
+add_text(slide, Inches(6.8), Inches(1.95), Inches(6.1), Inches(0.4),
+ "▶ À venir", size=16, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
+
+todo_items = [
+ "Orchestration vCenter (snapshot automatique)",
+ "Orchestration Centreon (maintenance automatique)",
+ "Intégration Teams native (notifications)",
+ "Exécution patching end-to-end en un clic",
+ "Module reporting KPI & tableau de bord DSI",
+ "Extension aux serveurs Windows (WSUS/MECM)",
+]
+tb = slide.shapes.add_textbox(Inches(7.0), Inches(2.55), Inches(5.8), Inches(4.2))
+tf = tb.text_frame; tf.word_wrap = True
+for j, it in enumerate(todo_items):
+ p = tf.paragraphs[0] if j == 0 else tf.add_paragraph()
+ p.space_after = Pt(8)
+ r1 = p.add_run(); r1.text = "▶ "
+ r1.font.size = Pt(12); r1.font.bold = True; r1.font.color.rgb = ORANGE
+ r2 = p.add_run(); r2.text = it
+ r2.font.size = Pt(12); r2.font.color.rgb = DARK
+
+add_footer(slide, 17)
+
+
+# ============================================================
+# SLIDE 18 — Conclusion
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+add_title_bar(slide, "Conclusion", "Un investissement à double retour : opérationnel & sécurité")
+
+add_text(slide, Inches(0.7), Inches(1.9), Inches(12), Inches(0.5),
+ "Trois constats à retenir",
+ size=18, bold=True, color=ACCENT)
+
+bullets_data = [
+ ("1.", ACCENT, "Le patching manuel est un coût caché — 60 à 100 min/serveur, 20-33 h/semaine/patcheur"),
+ ("2.", ORANGE, "Le patcheur est structurellement en surcharge dès qu'il cumule patching + garde + iTop"),
+ ("3.", RED_ALERT, "L'automatisation libère 25-30 h/semaine — du temps pour analyser correctement les alertes EDR"),
+]
+for i, (num, col, txt) in enumerate(bullets_data):
+ y = Inches(2.7 + 0.9 * i)
+ add_rect(slide, Inches(0.7), y, Inches(0.9), Inches(0.7), fill=col)
+ add_text(slide, Inches(0.7), y + Inches(0.13), Inches(0.9), Inches(0.5),
+ num, size=24, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
+ add_text(slide, Inches(1.8), y + Inches(0.15), Inches(11), Inches(0.5),
+ txt, size=14, color=DARK)
+
+add_callout(slide, Inches(0.7), Inches(5.8), Inches(12), Inches(1.1),
+ "Message clé DSI",
+ "PatchCenter Web n'est pas qu'un gain de productivité — c'est un levier de sécurité stratégique. "
+ "Le temps libéré redonne au patcheur sa capacité d'analyste EDR. Automatiser, "
+ "c'est renforcer la détection et la réponse aux incidents de SANEF.",
+ bg=RGBColor(0xE6, 0xF6, 0xFB), border=ACCENT, title_color=ACCENT_DARK)
+add_footer(slide, 18)
+
+
+# ============================================================
+# SLIDE 19 — Merci / Q&A
+# ============================================================
+slide = prs.slides.add_slide(blank_layout)
+set_bg(slide, DARK)
+add_rect(slide, 0, Inches(3.3), SW, Inches(0.08), fill=ACCENT)
+
+add_text(slide, 0, Inches(2.2), SW, Inches(1.2),
+ "Merci", size=96, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
+add_text(slide, 0, Inches(3.5), SW, Inches(0.6),
+ "Questions & Discussion", size=26, color=ACCENT, align=PP_ALIGN.CENTER)
+add_text(slide, 0, Inches(4.3), SW, Inches(0.5),
+ "SANEF DSI — Équipe SECOPS", size=16, color=LIGHT_GRAY, align=PP_ALIGN.CENTER)
+add_text(slide, 0, Inches(4.8), SW, Inches(0.4),
+ "pc.mpcz.fr — PatchCenter Web", size=14, color=GRAY, align=PP_ALIGN.CENTER)
+
+# Save
+out = r"C:\Users\netadmin\Desktop\SANEF_Processus_Patching.pptx"
+prs.save(out)
+print(f"Generated: {out}")
diff --git a/tools/processus_patching.txt b/tools/processus_patching.txt
new file mode 100644
index 0000000..8dd75f1
--- /dev/null
+++ b/tools/processus_patching.txt
@@ -0,0 +1,335 @@
+====== Processus de Patching SANEF — Manuel vs Automatisé ======
+
+
+**Référence :** Processus SECOPS — Équipe Patching SANEF\\
+**Objectif :** Documenter le workflow de patching actuel (manuel) et quantifier le gain apporté par les outils d'automatisation (SQATM .exe + PatchCenter Web).
+
+
+----
+
+===== 1. Vue d'ensemble du processus =====
+
+Le patching d'un serveur se déroule en **deux phases principales** étalées sur plusieurs jours :
+
+^ Phase ^ Moment ^ Objectif ^ Livrable ^
+| **1. Affectation** | Jeudi COMEP 14h00 | Répartition des serveurs | Liste par intervenant |
+| **2. Préparation (pré-patching)** | Jeudi après-midi + Vendredi | Éligibilité + validation créneau | Créneau validé avec responsable |
+| **3. Exécution (Jour J)** | Lundi → Jeudi (selon créneau) | Patching + vérifications | Serveur patché et validé |
+| **4. Nettoyage** | Vendredi (J+3 typique) | Suppression snapshots + kernels | Serveurs propres |
+
+
+**Règle de cadrage :** pas de patching le vendredi pour éviter un incident qui courrait tout le weekend.\\
+Le vendredi est consacré aux **opérations hors patching** : finalisation pré-patching de la semaine suivante, suppression des anciens snapshots (des serveurs patchés en début de semaine), ménage kernels, iTop, météo.
+
+
+----
+
+===== 2. Phase 1 — Préparation (Jeudi après-midi + Vendredi) =====
+
+==== 2.1 Affectation des serveurs ====
+
+ * **Jeudi 14h00 — COMEP** : répartition des serveurs à patcher entre les intervenants SECOPS
+ * Chaque intervenant reçoit sa liste pour la semaine suivante
+ * La phase de qualification démarre **immédiatement après le COMEP** (jeudi après-midi) et se poursuit **toute la journée de vendredi**
+
+==== 2.2 Qualification de chaque serveur ====
+
+Pour **chaque serveur** de la liste, l'intervenant effectue la séquence suivante (répartie sur jeudi après-midi et vendredi) :
+
+^ # ^ Étape ^ Outil ^ Durée estimée ^
+| 1 | Copier/coller nom serveur dans console Qualys | Qualys VMDR | 1 min |
+| 2 | Cliquer sur le serveur → onglet Vulnérabilités | Qualys VMDR | 2 min |
+| 3 | Visualiser et analyser les vulnérabilités | Qualys VMDR | 3-5 min |
+| 4 | Lancer PuTTY, se connecter en SSH (clé) | PuTTY + clé | 1 min |
+| 5 | Vérifier espace disque (''df -h'') | SSH | 1 min |
+| 6 | Vérifier connexion satellite (''subscription-manager status'') | SSH | 1 min |
+| 7 | Dry run check update (''yum check-update'') | SSH | 2-3 min |
+| 8 | Vérifier accès vCenter (pour snapshot) | vSphere Web | 2 min |
+| 9 | Vérifier backup Commvault (si physique) | Commvault | 2 min |
+| 10 | Vérification Centreon (si production) | Centreon | 2 min |
+| 11 | Si OK → marquer serveur éligible | - | 1 min |
+| 12 | Contacter responsable + proposer créneaux (mail/Teams) | Mail / Teams | 5-10 min |
+| 13 | Attendre validation du créneau (asynchrone) | - | variable |
+
+
+**Durée cumulée par serveur : 20 à 30 minutes** de travail actif (hors attente validation asynchrone).\\
+Pour un intervenant avec 20 serveurs à qualifier : **6h40 à 10h** par semaine sur la seule phase préparation.
+
+
+----
+
+===== 3. Phase 2 — Exécution (Jour J — Lundi à Jeudi) =====
+
+
+Le **Jour J** d'un serveur tombe nécessairement entre **lundi et jeudi** (jamais le vendredi).\\
+Cela laisse au moins 24h de recul avant le weekend pour détecter une régression post-patch.
+
+
+==== 3.1 Séquence par serveur ====
+
+^ # ^ Étape ^ Outil ^ Durée estimée ^
+| 1 | Information début d'intervention (Teams) | Teams | 1 min |
+| 2 | Connexion vCenter + prise de snapshot | vSphere Web | 3-5 min |
+| 3 | Mise en maintenance Centreon (si prod) | Centreon | 2 min |
+| 4 | Connexion SSH sur le serveur | PuTTY | 1 min |
+| 5 | Snap pré-patch : services running + process + ports (''systemctl'', ''ss'') | SSH | 5 min |
+| 6 | Lancer la mise à jour (''yum update -y'') | SSH | 5-15 min |
+| 7 | Informer du reboot imminent (Teams) | Teams | 1 min |
+| 8 | Reboot du serveur | SSH | 2-5 min |
+| 9 | Attente + reconnexion | PuTTY | 3-5 min |
+| 10 | Check post-patch : services + process + ports + Centreon | SSH + Centreon | 5-10 min |
+| 11 | Demander validation au responsable applicatif | Mail / Teams | 5-15 min (attente) |
+| 12 | Marquer le serveur comme patché (tableau de suivi) | Excel / outil | 2 min |
+| 13 | Message fin d'intervention (Teams) | Teams | 1 min |
+
+==== 3.2 Phase de nettoyage (le vendredi) ====
+
+Le **vendredi** est consacré au nettoyage groupé de tous les serveurs patchés pendant la semaine (J+3 typique pour ceux patchés lundi/mardi) :
+
+^ # ^ Étape ^ Outil ^ Durée estimée ^
+| 14 | Suppression des **anciens snapshots vCenter** de tous les serveurs patchés la semaine | vSphere Web | 2 min/serveur |
+| 15 | Suppression ancien kernel (''package-cleanup --oldkernels'') | SSH | 3 min/serveur |
+
+
+Grouper le nettoyage le vendredi a deux avantages :
+ * **Recul suffisant** : on garde le snapshot au moins jusqu'au lendemain pour pouvoir rollback en cas d'anomalie détectée après coup
+ * **Contexte mental unique** : le patcheur traite tous ses snapshots d'un bloc au lieu d'y revenir serveur par serveur
+
+
+
+**Durée cumulée par serveur (Jour J + J+3) : 40 à 70 minutes** de travail actif.\\
+Pour 20 serveurs patchés dans la semaine : **13h à 23h** de charge intervenant sur l'exécution seule.
+
+
+----
+
+===== 4. Synthèse — Temps total par serveur (processus manuel) =====
+
+^ Phase ^ Temps actif ^ Temps d'attente ^ Total ^
+| Préparation (qualification + validation) | 20-30 min | 1-48h (async validation) | 20-30 min actif |
+| Exécution Jour J | 35-65 min | - | 35-65 min |
+| Nettoyage J+3 | 5 min | - | 5 min |
+| **Total par serveur** | **60-100 min** | | **~1h15 en moyenne** |
+
+----
+
+===== 4.1 Réalité du temps patcheur =====
+
+
+**Contraintes horaires du patcheur SECOPS :**
+
+ * Semaine de travail **Lundi → Vendredi**, **7 heures par jour** → **35 heures/semaine** au total
+ * **Pas de patching le vendredi** (pour éviter un weekend sous incident)
+ * **Lundi → Jeudi matin (28h)** : fenêtre d'exécution Jour J + fin de pré-patching entamé la semaine précédente
+ * **Jeudi après-midi (3,5h)** : COMEP + démarrage pré-patching de la semaine suivante
+ * **Vendredi (7h)** : finalisation pré-patching + suppression des snapshots des serveurs patchés la semaine + iTop / météo
+ * **Objectif par patcheur : 20 serveurs patchés par semaine**
+
+
+=== Calcul de la charge théorique ===
+
+^ Poste ^ Volume ^ Temps unitaire ^ Total hebdo ^
+| Préparation 20 serveurs (jeudi PM + vendredi) | 20 | 20-30 min | **6h40 à 10h** |
+| Exécution Jour J 20 serveurs (Lun-Jeu) | 20 | 35-65 min | **11h40 à 21h40** |
+| Nettoyage snapshots/kernels (vendredi) | 20 | 5 min | **1h40** |
+| **Total patching seul** | - | - | **20h à 33h** |
+| **Budget hebdo total** | - | - | **35h** (dont 28h Lun-Jeu et 7h vendredi) |
+
+
+**Répartition par fenêtre :**
+
+^ Fenêtre ^ Capacité ^ Charge patching ^ Marge ^
+| Lun → Jeu midi (28h) | Jour J des 20 serveurs | 12 à 22h | 6 à 16h |
+| Jeu après-midi (3,5h) | COMEP + début pré-patching | 1,5 à 3h | 0,5 à 2h |
+| Vendredi (7h) | Pré-patching + nettoyage + missions annexes | 4 à 7h (pré-patch + cleanup) | 0 à 3h pour iTop/météo |
+
+Autrement dit, **le patching seul consomme 57 à 95% du temps disponible** d'un patcheur qui ne ferait que ça.
+
+
+Or le patcheur doit **également** assurer d'autres missions :
+
+=== Autres missions du patcheur ===
+
+^ Mission ^ Fréquence ^ Charge estimée ^
+| **Tickets iTop sécurisation serveur** | Permanent | 2-5 h/semaine |
+| **Mise à jour agent Qualys** (ponctuelle) | Hebdomadaire | 1-3 h/semaine |
+| **Mise à jour agent SentinelOne** | Hebdomadaire | 1-3 h/semaine |
+| **Tour de garde — alertes SentinelOne / Defender** | Rotation | 3-6 h/semaine (sur garde) |
+| **Météo sécurité** (veille + reporting hebdo) | Rotation | 2-4 h/semaine (sur météo) |
+| **Total missions annexes** | - | **9 à 21 h/semaine** |
+
+
+**Équation insoluble en mode manuel :**
+
+ * Temps disponible : **35h** (Lun-Ven x 7h)
+ * Temps patching (20 serveurs — prep + exec + cleanup) : **20-33h**
+ * Temps missions annexes : **9-21h**
+ * **Total charge : 29 à 54h/semaine**
+
+Le patcheur est **structurellement en surcharge** dès qu'il cumule patching + garde + iTop.\\
+Résultat : décalages de créneaux, vérifications raccourcies, dette sur les tickets iTop, épuisement.
+
+
+
+**Conséquence sécurité directe — analyse des alertes SentinelOne & Defender**
+
+Quand le patcheur est saturé par le patching manuel, il **n'a pas le temps d'analyser correctement** les alertes SentinelOne et Microsoft Defender pendant son tour de garde :
+
+ * Les alertes sont **triées en surface** (criticité apparente) au lieu d'être investiguées en profondeur
+ * Les **faux positifs** ne sont pas requalifiés → bruit qui cache les vrais incidents
+ * Les **vrais incidents** (ransomware, exfiltration, lateral movement) risquent d'être détectés **tardivement**
+ * L'**enrichissement de contexte** (corrélation process/réseau/user) n'est pas fait
+ * Les **règles de détection** ne sont pas affinées en retour d'expérience
+
+**Coût potentiel d'une alerte ratée :** incident de sécurité majeur, fuite de données, indisponibilité — sans commune mesure avec le coût du patching manuel.
+
+L'automatisation du patching par PatchCenter Web n'est donc pas seulement un gain de productivité : c'est une **mesure de sécurité directe** qui redonne au patcheur le temps d'exercer correctement son rôle d'analyste EDR pendant ses gardes.
+
+
+=== Projection annuelle ===
+
+Sur un parc de **~1165 serveurs** avec un cycle mensuel ciblant environ **200 serveurs/mois** (prod + prépro prioritaires) :
+
+ * **200 serveurs x 1h15 = 250 heures/mois**
+ * Soit l'équivalent de **1,5 à 2 ETP** consacrés aux gestes répétitifs de patching
+ * **Risque humain** : oubli de snapshot, oubli de Centreon, check post-patch incomplet, tableau de suivi non à jour, créneau non respecté
+
+----
+
+===== 5. Apport des outils d'automatisation =====
+
+==== 5.1 SQATM .exe (déjà en production) ====
+
+L'outil SANEF Qualys API Tags Management automatise la phase **qualification** :
+
+^ Étape manuelle ^ Automatisé par SQATM ^ Gain ^
+| Recherche Qualys + lecture des vulnérabilités | Décodeur + API Qualys | 5 min → 30 sec |
+| Check SSH disque / satellite / dry-run | Audit global multi-serveurs | 5 min → 10 sec |
+| Tagging automatique (ENV, OS, POS, EQT) | Tag Rules + décodeur nomenclature | 15 min → instantané |
+| Identification exposition internet | Plan de patching + règles | 3 min → instantané |
+
+**Gain estimé sur la phase préparation : 60-70%** (20 min → 6-7 min par serveur).
+
+==== 5.2 PatchCenter Web (en cours de déploiement) ====
+
+La plateforme web centralisera l'ensemble du workflow avec orchestration complète :
+
+=== Automatisation de la préparation ===
+
+ * **Vue unifiée** des serveurs avec statut Qualys, OS, environnement, responsable, dernière patch
+ * **Audit global en arrière-plan** : SSH sur 100+ serveurs en parallèle, disque + satellite + dry-run en une passe
+ * **Identification correspondance prod ↔ hors-prod** automatique (signature hostname)
+ * **Workflow de validation** intégré : proposition de créneau + accord responsable tracé en base
+ * **Tags Qualys dynamiques** auto-appliqués (Tag Rules basées sur la nomenclature SANEF)
+
+=== Automatisation du Jour J ===
+
+ * **Prise de snapshot vCenter** automatisée via API
+ * **Mise en maintenance Centreon** automatisée via API
+ * **Notifications Teams** générées automatiquement (début, reboot, fin)
+ * **Snap pré-patch / post-patch** scripté et archivé (services + process + ports)
+ * **Exécution yum update** en job asynchrone avec exclusions configurables par serveur
+ * **Reboot + attente reconnexion** gérés automatiquement
+ * **Détection des régressions** (service manquant post-reboot → alerte)
+ * **Tableau de suivi en temps réel** (plus de saisie Excel)
+
+=== Automatisation du nettoyage ===
+
+ * **Liste des snapshots à supprimer J+3** avec rappel automatique
+ * **Nettoyage kernels obsolètes** en fin de job
+
+=== Gouvernance ===
+
+ * **Validations prod ↔ hors-prod** bloquantes : impossibilité de patcher la prod sans validation du hors-prod correspondant
+ * **Historique complet** par serveur : qui a patché, quand, résultat, validation responsable
+ * **Audit log** exportable pour conformité
+
+==== 5.3 Comparaison chiffrée ====
+
+^ Étape ^ Manuel ^ SQATM .exe ^ PatchCenter Web ^
+| Qualification serveur (prep) | 20-30 min | 6-10 min | 2-3 min |
+| Snapshot + maintenance | 5-7 min | 5-7 min | 30 sec (automatique) |
+| Snap pré/post-patch | 10-15 min | 10-15 min | 1 min (automatique) |
+| Update + reboot + reconnexion | 10-25 min | 10-25 min | 10-25 min (compressible en parallèle) |
+| Notifications + suivi | 5 min | 5 min | 0 (automatique) |
+| Nettoyage J+3 | 5 min | 5 min | 1 min (automatique) |
+| **Total par serveur** | **60-100 min** | **40-70 min** | **15-30 min** |
+| **Gain vs manuel** | - | **30%** | **70-75%** |
+
+
+**Projection sur 200 serveurs/mois :**
+
+ * **Manuel** : 250 h/mois (~1,5 à 2 ETP)
+ * **SQATM .exe** : 170 h/mois (~1 ETP, gain = 80h/mois)
+ * **PatchCenter Web** : 75 h/mois (~0,5 ETP, gain = 175h/mois)
+
+
+----
+
+==== 5.4 Impact hebdomadaire sur le patcheur (objectif 20 serveurs/semaine) ====
+
+^ Scénario ^ Patching seul ^ Budget restant (sur 35h) ^ Missions annexes couvertes ? ^
+| **Manuel** | 20h à 33h | +2h à +15h | Non — surcharge garantie en période de garde ou météo |
+| **SQATM .exe** | 14h à 23h | +12h à +21h | Partiellement — tient si pas de garde ou pas de météo |
+| **PatchCenter Web** | 5h à 10h | +25h à +30h | **Oui** — le patcheur peut assurer patching + garde + météo + iTop sans surcharge |
+
+
+**Lecture :**\\
+Avec PatchCenter Web, un patcheur qui traite ses 20 serveurs hebdomadaires libère **18 à 23 heures** par semaine pour :
+ * Absorber sereinement les tickets iTop de sécurisation
+ * Assurer le tour de garde SentinelOne / Defender sans décaler le patching
+ * Produire la météo sécurité hebdomadaire
+ * Traiter les MAJ d'agents Qualys / SentinelOne en masse
+ * Ou **augmenter la cadence** (25-30 serveurs/semaine) si besoin opérationnel
+
+
+----
+
+===== 6. Bénéfices qualitatifs (au-delà du temps) =====
+
+^ Axe ^ Bénéfice PatchCenter Web ^
+| **Fiabilité** | Plus d'oubli de snapshot, de maintenance Centreon ou de marquage de statut |
+| **Traçabilité** | Historique complet par serveur, exportable pour audit |
+| **Gouvernance** | Workflow de validation prod ↔ hors-prod bloquant |
+| **Scalabilité** | 1 intervenant peut piloter 20+ serveurs en parallèle |
+| **Onboarding** | Nouveau patcheur opérationnel en quelques jours (processus guidé) |
+| **Qualité** | Snap pré/post standardisé → détection fiable des régressions |
+| **Conformité** | Tags Qualys automatiques → reporting KPI sans retraitement |
+| **Communication** | Notifications Teams standardisées → meilleure visibilité projet |
+
+----
+
+===== 7. Feuille de route =====
+
+
+**Avancement actuel :**
+
+ * **SQATM .exe v2.0.0** : en production, utilisé pour le tagging et l'audit
+ * **PatchCenter Web** : développement actif
+ * Modules livrés : catalogue serveurs, correspondance prod/hors-prod, exclusions patchs, workflow validations, agents Qualys, déploiement agents
+ * Modules à finaliser : orchestration vCenter, orchestration Centreon, intégration Teams native, exécution patching end-to-end
+ * **Intégration iTop bidirectionnelle** : opérationnelle (synchronisation serveurs / applications / statuts)
+ * **Authentification LDAP AD SANEF** : prête (multi-profils : admin, coordinateur, opérateur, viewer)
+
+
+----
+
+===== 8. Conclusion =====
+
+Le processus de patching manuel actuel est **efficace mais coûteux** : 60 à 100 minutes par serveur, avec un risque humain non négligeable sur les gestes répétitifs (snapshot, maintenance, suivi). La montée en charge progressive du parc et les exigences de conformité (Qualys, audit) rendent l'automatisation **incontournable**.
+
+Avec un objectif de **20 serveurs patchés par patcheur et par semaine**, un budget horaire de **35h (Lun-Ven, 7h/jour — mais pas de patching le vendredi, dédié au pré-patching + nettoyage snapshots + missions annexes)**, et des missions annexes obligatoires (tickets iTop, MAJ agents Qualys/SentinelOne, tour de garde, météo) qui représentent **9 à 21 heures/semaine**, le mode manuel place structurellement le patcheur en surcharge.
+
+Les outils en place et à venir apportent un gain **mesurable** :
+
+ * **SQATM .exe** divise par 1,5 le temps de préparation
+ * **PatchCenter Web** divise par 3 à 4 le temps global par serveur
+ * **PatchCenter Web** libère **25 à 30 heures/semaine** par patcheur, de quoi absorber sereinement les missions annexes **et** augmenter la cadence si nécessaire
+
+L'investissement dans l'automatisation ne se résume pas à un gain de temps : il garantit la **fiabilité**, la **traçabilité**, la **scalabilité** du processus, et surtout la **soutenabilité** du métier de patcheur SECOPS à SANEF.
+
+**Enjeu sécurité stratégique :** le temps libéré par l'automatisation permet au patcheur d'exercer correctement son rôle d'**analyste EDR** sur les alertes SentinelOne et Defender pendant ses gardes. En mode manuel, ces alertes sont traitées en surface faute de temps — un ransomware, une exfiltration ou un lateral movement peut passer inaperçu. Automatiser le patching, c'est donc aussi **renforcer directement la capacité de détection et de réponse aux incidents** de SANEF.
+
+----
+
+//— SANEF DSI / Sécurité Opérationnelle — Processus Patching & Automatisation — {date}//
diff --git a/tools/script_presentation.txt b/tools/script_presentation.txt
new file mode 100644
index 0000000..18586c1
--- /dev/null
+++ b/tools/script_presentation.txt
@@ -0,0 +1,391 @@
+====== Script de présentation — Processus Patching SANEF ======
+
+
+**Format :** présentation DSI, ~18 minutes de parole + Q&A\\
+**Ton :** oral, direct, factuel. Préférer des phrases courtes. Regarder l'auditoire, pas le slide.\\
+**Astuce :** les phrases en //italique// sont des respirations / silences volontaires, souvent plus efficaces que les arguments.
+
+
+----
+
+===== Slide 1 — Titre (30 s) =====
+
+
+[entrée en scène, attendre quelques secondes]
+
+
+Bonjour à tous, merci d'être là.
+
+On va parler ce matin du **patching des serveurs SANEF** — de comment ça se passe aujourd'hui, de ce que ça coûte réellement à l'équipe SECOPS, et de ce qu'on est en train de mettre en place pour changer la donne.
+
+L'idée, c'est de vous montrer pourquoi l'automatisation qu'on développe — **PatchCenter Web** — n'est pas juste un outil de confort : c'est un vrai levier de sécurité pour SANEF.
+
+//[pause — transition slide]//
+
+----
+
+===== Slide 2 — Sommaire (30 s) =====
+
+Voilà rapidement le plan.
+
+On va commencer par **dérouler le processus** étape par étape — pour qu'on ait tous la même image en tête de ce que fait concrètement un patcheur dans sa semaine.
+
+Ensuite on va regarder **la réalité du terrain** : combien d'heures ça prend, et surtout ce qui se passe quand le patcheur doit aussi faire autre chose — les tickets iTop, les tours de garde, la météo sécurité.
+
+Et on finira par **l'apport de l'automatisation** : ce qu'on a déjà — SQATM — et ce qu'on est en train de livrer — PatchCenter Web.
+
+----
+
+===== Slide 3 — Vue d'ensemble (1 min) =====
+
+Le patching d'un serveur, ce n'est pas un geste unique. C'est **quatre phases étalées sur la semaine**.
+
+Première phase, le **jeudi à 14 heures** : le COMEP SECOPS. C'est là qu'on répartit les serveurs à patcher entre les intervenants.
+
+Deuxième phase, **jeudi après-midi et vendredi** : la préparation — ce qu'on appelle aussi le pré-patching. C'est toute la qualification serveur avant de toucher à quoi que ce soit.
+
+Troisième phase, **du lundi au jeudi** de la semaine suivante : l'exécution — le fameux **Jour J**.
+
+Et quatrième phase, **le vendredi** : le nettoyage. On supprime les snapshots des serveurs patchés dans la semaine, et on fait le ménage sur les vieux kernels.
+
+//[pointer le callout en bas]//
+
+Un point important : **on ne patche jamais le vendredi**. Pourquoi ? Parce que si un incident survient, on ne veut pas le traîner tout le weekend. Donc le vendredi est dédié à tout le reste — prépa de la semaine suivante, nettoyage, iTop, météo.
+
+----
+
+===== Slide 4 — Affectation (45 s) =====
+
+Donc on démarre le jeudi au COMEP. 14 heures.
+
+Chaque intervenant repart avec **sa liste de serveurs pour la semaine**. En cible on est à **20 serveurs par intervenant**. C'est un chiffre important à garder en tête, on va y revenir.
+
+Le livrable du COMEP, c'est une liste enrichie : domaine, environnement, OS, responsable applicatif, criticité. Avec ça, le patcheur peut démarrer la qualification immédiatement.
+
+----
+
+===== Slide 5 — Préparation (1 min 30) =====
+
+La préparation — c'est là que le gros du temps invisible se joue.
+
+Pour **chaque serveur**, il faut faire **13 étapes** :
+
+//[parcourir la liste à l'écran, ne pas la lire]//
+
+Ça va de la recherche dans la console Qualys, à l'analyse des vulnérabilités, au SSH pour vérifier l'espace disque, le satellite, le dry-run yum... et ensuite seulement la partie "humaine" : **contacter le responsable applicatif** et lui proposer des créneaux.
+
+//[pointer le KPI à droite]//
+
+Résultat : **20 à 30 minutes par serveur** de travail actif. Pour 20 serveurs par patcheur par semaine, ça fait **6h40 à 10 heures** — rien que sur la préparation.
+
+Et encore — je ne compte pas le temps d'attente de la validation du responsable. Qui peut prendre 1 heure, ou 48 heures. Donc le patcheur jongle avec une dizaine de serveurs en parallèle, tous à des états différents.
+
+----
+
+===== Slide 6 — Jour J (1 min 30) =====
+
+On arrive au Jour J — forcément entre lundi et jeudi.
+
+Là encore, **13 étapes par serveur**.
+
+Ça commence par un message Teams pour prévenir, on prend le snapshot vCenter, on met le serveur en maintenance dans Centreon si c'est de la prod, on fait un snap pré-patch des services et des ports — parce qu'on veut pouvoir comparer après reboot. Ensuite seulement, le yum update. Le reboot. La reconnexion. Le check post-patch. Et enfin la **validation par le responsable applicatif** — qui peut demander 15 minutes ou plus si le service n'est pas immédiatement disponible.
+
+//[pointer les KPIs]//
+
+Le temps actif : **35 à 65 minutes par serveur**. Sur 20 serveurs, on est entre **11h40 et 21h40** par semaine rien que sur l'exécution.
+
+//[petit silence]//
+
+Pourquoi pas le vendredi ? On l'a dit : **au moins 24 heures de recul avant le weekend**. Si un service tombe dimanche, on veut pouvoir le détecter le vendredi.
+
+----
+
+===== Slide 7 — Nettoyage (45 s) =====
+
+Le vendredi, on **regroupe le nettoyage** de toute la semaine.
+
+Suppression des anciens snapshots, suppression des vieux kernels. Environ 1 heure 40 pour le lot de 20 serveurs.
+
+Pourquoi on fait ça en un bloc le vendredi ? Pour deux raisons.
+
+D'abord : on veut **garder le snapshot pendant au moins 24 heures** après le patch, au cas où une anomalie apparaîtrait en production.
+
+Ensuite : c'est un **contexte mental unique**. Traiter les 20 snapshots d'un bloc, c'est beaucoup plus efficace que d'y revenir serveur par serveur.
+
+----
+
+===== Slide 8 — Synthèse temps (1 min) =====
+
+Donc si on additionne tout ça pour **un seul serveur** :
+
+ * Préparation : 20-30 minutes
+ * Jour J : 35-65 minutes
+ * Nettoyage : 5 minutes
+
+Total : **60 à 100 minutes par serveur**. En moyenne, **1 heure 15**.
+
+//[pointer les KPIs du bas]//
+
+**1 heure 15 par serveur. 20 serveurs par semaine. 25 heures de patching pur.**
+
+//[silence — laisser le chiffre s'installer]//
+
+----
+
+===== Slide 9 — La réalité du patcheur (1 min 30) =====
+
+Maintenant, la vraie question : **est-ce que le patcheur a 25 heures disponibles ?**
+
+Regardons son budget. Le patcheur travaille **5 jours sur 7**, **7 heures par jour** — donc **35 heures par semaine**.
+
+//[pointer la barre]//
+
+Sur ces 35 heures :
+ * **28 heures** du lundi au jeudi sont la fenêtre d'exécution des Jours J
+ * **3,5 heures** le jeudi après-midi pour le COMEP et le début du pré-patching
+ * **7 heures** le vendredi pour finir le pré-patching et nettoyer
+
+Et dans ces 35 heures, le patching seul consomme **entre 20 et 33 heures**.
+
+Autrement dit : **entre 57 % et 95 % du temps disponible**.
+
+//[insister]//
+
+Dans le meilleur des cas — si tous les patches passent sans accroc, si personne ne traîne sur la validation, si aucun incident — il reste 2 heures de marge par semaine.
+
+Et encore. **On n'a pas parlé des autres missions.**
+
+----
+
+===== Slide 10 — Missions annexes (1 min) =====
+
+Parce qu'un patcheur SECOPS n'est pas que patcheur.
+
+//[lister lentement]//
+
+Il doit aussi :
+ * Traiter les **tickets iTop** de sécurisation serveur — permanent, 2 à 5 heures par semaine
+ * Faire les **mises à jour agents Qualys** et **SentinelOne** — 2 à 6 heures par semaine
+ * Prendre le **tour de garde** sur les alertes SentinelOne et Defender — 3 à 6 heures quand c'est son tour
+ * Produire la **météo sécurité** — 2 à 4 heures quand c'est son tour
+
+Au total : **9 à 21 heures de missions annexes**.
+
+//[pointer le callout rouge]//
+
+Faites le calcul avec moi :
+ * Budget disponible : **35 heures**
+ * Patching seul : **20 à 33 heures**
+ * Missions annexes : **9 à 21 heures**
+ * **Charge totale réelle : 29 à 54 heures par semaine**
+
+//[silence]//
+
+Le patcheur est **structurellement en surcharge**. Pas occasionnellement. Structurellement.
+
+----
+
+===== Slide 11 — Conséquence sécurité (1 min 30) =====
+
+Et c'est là que ça devient **un sujet de sécurité**, pas juste un sujet d'organisation.
+
+//[ton plus grave]//
+
+Quand un patcheur est en surcharge et qu'il prend son tour de garde sur les alertes SentinelOne et Defender, qu'est-ce qui se passe ?
+
+//[lire les points avec poids]//
+
+ * Les alertes sont **triées en surface** — on regarde la criticité apparente, on coche, on passe
+ * Les **faux positifs ne sont pas requalifiés** — le bruit de fond s'accumule et masque les vrais incidents
+ * Les **vrais incidents sont détectés tardivement** — un ransomware, une exfiltration, un lateral movement
+ * L'**enrichissement de contexte** — corrélation process, réseau, utilisateur — n'est pas fait
+ * Les **règles de détection** ne sont jamais affinées, parce qu'on n'a pas le temps de faire ce retour d'expérience
+
+//[pointer le callout rouge]//
+
+Et là, on parle d'**incidents de sécurité majeurs**. Ransomware, fuite de données, indisponibilité.
+
+**Le coût d'une seule alerte EDR ratée — c'est sans commune mesure avec le coût de l'automatisation du patching.**
+
+//[silence]//
+
+Voilà pourquoi automatiser le patching, ce n'est pas du confort. C'est de la sécurité.
+
+----
+
+===== Slide 12 — SQATM .exe (45 s) =====
+
+On a déjà fait un pas dans cette direction : **SQATM .exe** est en production depuis un moment.
+
+Cet outil automatise la **phase qualification** — la fameuse préparation des 20-30 minutes par serveur.
+
+//[parcourir le tableau]//
+
+La recherche Qualys plus l'analyse des vulnérabilités passe de 5 minutes à 30 secondes.\\
+Le check SSH — disque, satellite, dry-run — passe de 5 minutes à 10 secondes, et surtout on le fait sur 100 serveurs en parallèle.\\
+Le tagging Qualys est instantané.
+
+**Gain estimé : 60 à 70 % sur la phase préparation.**
+
+//[transition]//
+
+Mais SQATM ne couvre que la partie qualification. Pour automatiser le Jour J, il fallait passer au niveau au-dessus.
+
+----
+
+===== Slide 13 — PatchCenter Web (1 min 30) =====
+
+C'est l'objet de **PatchCenter Web**.
+
+//[présenter les 3 colonnes]//
+
+Trois grands volets.
+
+**Préparation** — on centralise tout : vue unifiée des 1165 serveurs, audit SSH parallélisé, détection automatique des correspondances prod / hors-prod, workflow de validation intégré, et les tags Qualys auto-appliqués via les Tag Rules.
+
+**Jour J** — on orchestre tout : snapshot vCenter via API, maintenance Centreon via API, notifications Teams automatiques, snap pré et post archivé, yum update en job asynchrone, reboot et reconnexion orchestrés, et détection automatique des régressions.
+
+**Gouvernance** — c'est ce qui manquait le plus : la validation prod / hors-prod **bloquante**, l'historique complet par serveur, l'audit log exportable pour la conformité, et l'authentification LDAP AD SANEF avec 4 profils — admin, coordinateur, opérateur, viewer.
+
+//[conclure]//
+
+Et surtout : le tableau de suivi est **en temps réel**. Fini les Excel.
+
+----
+
+===== Slide 14 — Comparaison chiffrée (1 min) =====
+
+Si on met les trois scénarios côte à côte, serveur par serveur :
+
+//[parcourir le tableau rapidement]//
+
+En mode manuel, on est à **60-100 minutes par serveur**.\\
+Avec SQATM, on descend à **40-70 minutes** — **gain de 30 %**.\\
+Avec PatchCenter Web, on descend à **15-30 minutes** — **gain de 70 à 75 %**.
+
+//[pointer la dernière ligne]//
+
+**On divise le temps par 3 à 4.**
+
+Et ce qui est intéressant, c'est que **le gain ne vient pas du yum update lui-même** — ça reste le même temps à l'OS de mettre à jour les paquets. Le gain vient de **la suppression des gestes répétitifs** : snapshot, Centreon, notifs, suivi. C'est là que se cache tout le temps perdu.
+
+----
+
+===== Slide 15 — Impact hebdomadaire (1 min) =====
+
+Traduisons ça au niveau hebdomadaire, pour les **20 serveurs par patcheur**.
+
+//[parcourir les 3 lignes]//
+
+En manuel : le patching occupe 20 à 33 heures. Reste 2 à 15 heures. **Insuffisant pour absorber les missions annexes.**
+
+Avec SQATM : on libère 10 à 12 heures supplémentaires. Ça tient si le patcheur n'est pas en tour de garde.
+
+**Avec PatchCenter Web : 5 à 10 heures de patching. Il reste 25 à 30 heures.**
+
+//[laisser respirer]//
+
+**25 à 30 heures par semaine libérées — par patcheur.**
+
+De quoi assurer sereinement les tickets iTop, la garde EDR, la météo. Et même **monter la cadence** si le besoin opérationnel le demande — passer de 20 à 25, 30 serveurs par semaine, sans embaucher.
+
+----
+
+===== Slide 16 — Bénéfices qualitatifs (45 s) =====
+
+Au-delà du temps, il y a tout ce qui est plus difficile à chiffrer mais tout aussi important.
+
+//[balayer rapidement]//
+
+ * **Fiabilité** : on ne peut plus oublier un snapshot ou une maintenance Centreon
+ * **Traçabilité** : tout est historisé, exportable pour audit
+ * **Gouvernance** : les validations prod / hors-prod deviennent bloquantes
+ * **Scalabilité** : un intervenant peut piloter 20 serveurs en parallèle
+ * **Onboarding** : un nouveau patcheur est opérationnel en quelques jours
+ * **Qualité** : le snap pré/post standardisé détecte les régressions de façon fiable
+ * **Conformité** : les tags Qualys automatiques alimentent directement les KPI
+ * **Communication** : les notifications Teams standardisées donnent de la visibilité projet
+
+----
+
+===== Slide 17 — Feuille de route (45 s) =====
+
+Où on en est concrètement aujourd'hui.
+
+//[colonne verte]//
+
+**Déjà livré :** SQATM en production. PatchCenter Web avec le catalogue des 1165 serveurs, la correspondance prod / hors-prod, les exclusions patch, le workflow de validation, la gestion des agents Qualys et le déploiement, l'intégration bidirectionnelle iTop, et l'authentification LDAP multi-profils.
+
+//[colonne orange]//
+
+**À venir :** l'orchestration vCenter et Centreon automatique, l'intégration Teams native, l'exécution patching end-to-end en un clic, le module reporting pour la DSI, et l'extension aux serveurs Windows via WSUS et MECM.
+
+On est sur une livraison progressive — on n'attend pas que tout soit fini pour en profiter. Chaque brique livrée apporte déjà du gain.
+
+----
+
+===== Slide 18 — Conclusion (1 min) =====
+
+Je conclus avec **trois messages** à retenir.
+
+//[pointer le premier carré]//
+
+**Un.** Le patching manuel est un **coût caché**. On le voit peu parce qu'il est étalé sur la semaine, mais c'est **20 à 33 heures par semaine par patcheur**.
+
+**Deux.** Le patcheur est **structurellement en surcharge**. Dès qu'il cumule patching, garde et iTop, il dépasse son budget horaire. Ce n'est pas une question de motivation — c'est de l'arithmétique.
+
+**Trois.** L'automatisation **libère 25 à 30 heures par semaine**. Du temps pour **analyser correctement les alertes EDR**.
+
+//[pointer le callout final]//
+
+Le message clé, pour vous, DSI :
+
+//[lire posément]//
+
+**PatchCenter Web n'est pas qu'un gain de productivité.** C'est un **levier de sécurité stratégique**.
+
+Le temps qu'on libère sur le patching, c'est du temps qu'on rend au patcheur pour faire **son vrai métier d'analyste EDR**.
+
+Automatiser le patching, c'est **renforcer directement la capacité de détection et de réponse aux incidents de SANEF**.
+
+Merci.
+
+----
+
+===== Slide 19 — Q&A =====
+
+//[rester en place, poser le clicker, regarder l'auditoire]//
+
+Je prends vos questions.
+
+----
+
+===== Annexe — Questions fréquentes anticipées =====
+
+==== "Pourquoi pas un outil du marché ?" ====
+
+On a évalué. Aucun outil marché ne couvre à la fois : la nomenclature SANEF (décodage hostname), l'intégration bidirectionnelle iTop, la correspondance prod / hors-prod, et l'orchestration Centreon / vCenter spécifique SANEF. PatchCenter Web est **conçu pour le contexte**, et ça se voit dans le taux d'adoption de l'équipe.
+
+==== "Combien ça coûte à développer vs la licence d'un outil marché ?" ====
+
+Le développement est internalisé — pas de licence récurrente, pas de dépendance vendor. Le coût est largement en-dessous de ce qu'aurait coûté un déploiement commercial type Red Hat Satellite Insights ou BigFix sur 1165 serveurs.
+
+==== "Et la sécurité de l'outil lui-même ?" ====
+
+PatchCenter Web est derrière HAProxy avec TLS, authentification LDAP AD SANEF, séparation des rôles (4 profils), audit log complet. Tout transite par la VRF d'administration SANEF. Aucune exposition internet.
+
+==== "Que devient SQATM une fois PatchCenter Web complet ?" ====
+
+SQATM reste pertinent pour les opérations unitaires rapides et le tagging ponctuel. Les deux outils **coexistent** : SQATM = léger, rapide, unitaire. PatchCenter = orchestration lourde, campagnes, gouvernance.
+
+==== "Risque de régression : si PatchCenter Web tombe, que fait-on ?" ====
+
+Le patching manuel reste possible en fallback — tous les outils sous-jacents (SSH, vCenter, Centreon, Qualys) sont indépendants de PatchCenter. On ne crée pas de dépendance critique : PatchCenter Web **orchestre**, il ne **remplace** pas les outils existants.
+
+==== "Planning de mise en service complète ?" ====
+
+Livraison progressive en cours. Les modules catalogue, correspondance, validations et agents Qualys sont déjà en production. L'orchestration vCenter/Centreon/Teams est prévue sur les mois à venir. Le patching end-to-end en un clic est la dernière brique.
+
+----
+
+//— Script de présentation — SANEF DSI / Sécurité Opérationnelle — Avril 2026//
diff --git a/tools/wiki_to_pdf.py b/tools/wiki_to_pdf.py
new file mode 100644
index 0000000..6e0b261
--- /dev/null
+++ b/tools/wiki_to_pdf.py
@@ -0,0 +1,362 @@
+#!/usr/bin/env python3
+"""Convert DokuWiki pages to nicely-formatted PDFs (reportlab)."""
+import re
+import sys
+from reportlab.lib import colors
+from reportlab.lib.colors import HexColor
+from reportlab.lib.pagesizes import A4
+from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
+from reportlab.lib.units import cm, mm
+from reportlab.platypus import (
+ SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
+ PageBreak, Preformatted, KeepTogether, HRFlowable
+)
+from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
+
+ACCENT = HexColor("#00a3c4")
+ACCENT_LIGHT = HexColor("#e0f4f8")
+DARK = HexColor("#1a1a2e")
+GRAY = HexColor("#666666")
+CODE_BG = HexColor("#f4f4f4")
+INFO_BG = HexColor("#e8f4fd")
+IMPORTANT_BG = HexColor("#fef3cd")
+TIP_BG = HexColor("#d4edda")
+
+
+def build_styles():
+ styles = getSampleStyleSheet()
+ styles.add(ParagraphStyle("H1c", parent=styles["Heading1"],
+ fontSize=20, textColor=ACCENT, spaceAfter=14, spaceBefore=10,
+ fontName="Helvetica-Bold"))
+ styles.add(ParagraphStyle("H2c", parent=styles["Heading2"],
+ fontSize=15, textColor=ACCENT, spaceAfter=10, spaceBefore=14,
+ fontName="Helvetica-Bold", borderPadding=(0, 0, 4, 0),
+ borderColor=ACCENT, borderWidth=0))
+ styles.add(ParagraphStyle("H3c", parent=styles["Heading3"],
+ fontSize=12.5, textColor=DARK, spaceAfter=8, spaceBefore=10,
+ fontName="Helvetica-Bold"))
+ styles.add(ParagraphStyle("H4c", parent=styles["Heading4"],
+ fontSize=11, textColor=DARK, spaceAfter=6, spaceBefore=8,
+ fontName="Helvetica-Bold"))
+ styles.add(ParagraphStyle("Bodyc", parent=styles["BodyText"],
+ fontSize=9.5, leading=13, spaceAfter=6, alignment=TA_JUSTIFY))
+ styles.add(ParagraphStyle("Bulletc", parent=styles["BodyText"],
+ fontSize=9.5, leading=13, leftIndent=16, bulletIndent=4, spaceAfter=3))
+ styles.add(ParagraphStyle("ItalicFooter", parent=styles["BodyText"],
+ fontSize=9, textColor=GRAY, alignment=TA_CENTER, spaceAfter=4))
+ return styles
+
+
+def fmt_inline(text):
+ """Convert DokuWiki inline markup to reportlab mini-HTML."""
+ # escape &, <, >
+ text = text.replace("&", "&").replace("<", "<").replace(">", ">")
+ # bold **x**
+ text = re.sub(r"\*\*(.+?)\*\*", r"\1 ", text)
+ # inline code ''x''
+ text = re.sub(r"''(.+?)''", r'\1 ', text)
+ # italic //x// (avoid URLs)
+ text = re.sub(r"(?\1", text)
+ # doku links [[target|label]] -> label
+ text = re.sub(r"\[\[[^\]|]+\|([^\]]+)\]\]", r"\1", text)
+ text = re.sub(r"\[\[([^\]]+)\]\]", r"\1", text)
+ # line break \\
+ text = text.replace("\\\\", " ")
+ return text
+
+
+def parse_wiki(raw):
+ """Return list of (kind, payload) blocks."""
+ lines = raw.splitlines()
+ blocks = []
+ i = 0
+ while i < len(lines):
+ ln = lines[i]
+ stripped = ln.strip()
+
+ # Separator
+ if stripped == "----":
+ blocks.append(("hr", None))
+ i += 1
+ continue
+
+ # Headings
+ m = re.match(r"^(=+)\s*(.+?)\s*=+\s*$", stripped)
+ if m:
+ level = 7 - len(m.group(1)) # ======(6) -> h1
+ level = max(1, min(4, level))
+ blocks.append((f"h{level}", m.group(2)))
+ i += 1
+ continue
+
+ # Code block
+ if stripped.startswith("" not in lines[i]:
+ code_lines.append(lines[i])
+ i += 1
+ i += 1 # skip
+ blocks.append(("code", "\n".join(code_lines)))
+ continue
+
+ # WRAP box
+ m = re.match(r"^]*)>\s*$", stripped)
+ if m:
+ kind = "info"
+ attrs = m.group(1).lower()
+ if "important" in attrs:
+ kind = "important"
+ elif "tip" in attrs:
+ kind = "tip"
+ elif "info" in attrs:
+ kind = "info"
+ i += 1
+ body_lines = []
+ while i < len(lines) and " " not in lines[i]:
+ body_lines.append(lines[i])
+ i += 1
+ i += 1
+ blocks.append(("wrap", (kind, "\n".join(body_lines))))
+ continue
+
+ # Table (starts with ^ or |)
+ if stripped.startswith("^") or stripped.startswith("|"):
+ tbl_lines = []
+ while i < len(lines) and (lines[i].strip().startswith("^") or lines[i].strip().startswith("|")):
+ tbl_lines.append(lines[i].strip())
+ i += 1
+ blocks.append(("table", tbl_lines))
+ continue
+
+ # Bullet list (item or sub-item)
+ if re.match(r"^\s*\*\s+", ln):
+ items = []
+ while i < len(lines) and re.match(r"^\s*\*\s+", lines[i]):
+ m2 = re.match(r"^(\s*)\*\s+(.+)$", lines[i])
+ indent = len(m2.group(1))
+ items.append((indent, m2.group(2)))
+ i += 1
+ blocks.append(("bullets", items))
+ continue
+
+ # Ordered list (DokuWiki uses " - item")
+ if re.match(r"^\s+-\s+", ln):
+ items = []
+ while i < len(lines) and re.match(r"^\s+-\s+", lines[i]):
+ m2 = re.match(r"^(\s*)-\s+(.+)$", lines[i])
+ indent = len(m2.group(1))
+ items.append((indent, m2.group(2)))
+ i += 1
+ blocks.append(("ordered", items))
+ continue
+
+ # Italic footer //...//
+ if stripped.startswith("//") and stripped.endswith("//") and len(stripped) > 4:
+ blocks.append(("italic", stripped.strip("/").strip()))
+ i += 1
+ continue
+
+ # Blank line -> spacer
+ if not stripped:
+ blocks.append(("space", None))
+ i += 1
+ continue
+
+ # Regular paragraph (gather until blank or special)
+ para_lines = [ln]
+ i += 1
+ while i < len(lines):
+ nxt = lines[i]
+ s = nxt.strip()
+ if not s or s.startswith("=") or s.startswith("^") or s.startswith("|") \
+ or s.startswith("<") or s == "----" or re.match(r"^\s*\*\s+", nxt) \
+ or re.match(r"^\s+-\s+", nxt):
+ break
+ para_lines.append(nxt)
+ i += 1
+ blocks.append(("para", " ".join(l.strip() for l in para_lines)))
+ return blocks
+
+
+def parse_table_row(line):
+ """Parse a DokuWiki table row. Returns (is_header_cells_list, cells)."""
+ # Row like: ^ h1 ^ h2 ^ or | c1 | c2 |
+ # A row may mix ^ and | (cell-level header)
+ sep_chars = "^|"
+ cells = []
+ is_header = []
+ # find separators
+ indices = [idx for idx, c in enumerate(line) if c in sep_chars]
+ for a, b in zip(indices[:-1], indices[1:]):
+ cell = line[a+1:b].strip()
+ header = line[a] == "^"
+ cells.append(cell)
+ is_header.append(header)
+ return is_header, cells
+
+
+def build_table(tbl_lines, styles):
+ data = []
+ header_flags = []
+ for ln in tbl_lines:
+ is_h, cells = parse_table_row(ln)
+ if not cells:
+ continue
+ data.append(cells)
+ header_flags.append(is_h)
+ if not data:
+ return None
+
+ # Wrap each cell in a Paragraph for wrapping
+ wrapped = []
+ cell_style = ParagraphStyle("cell", fontSize=8.5, leading=11)
+ head_style = ParagraphStyle("head", fontSize=9, leading=11,
+ fontName="Helvetica-Bold", textColor=colors.white)
+ for row_idx, row in enumerate(data):
+ new_row = []
+ for col_idx, cell in enumerate(row):
+ is_h = header_flags[row_idx][col_idx] if col_idx < len(header_flags[row_idx]) else False
+ style = head_style if is_h else cell_style
+ new_row.append(Paragraph(fmt_inline(cell), style))
+ wrapped.append(new_row)
+
+ ncols = max(len(r) for r in wrapped)
+ # pad
+ for r in wrapped:
+ while len(r) < ncols:
+ r.append(Paragraph("", cell_style))
+
+ # column widths: spread evenly on 17cm available
+ total_w = 17 * cm
+ col_w = total_w / ncols
+ t = Table(wrapped, colWidths=[col_w] * ncols, repeatRows=1)
+
+ ts = TableStyle([
+ ("GRID", (0, 0), (-1, -1), 0.3, colors.lightgrey),
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
+ ("LEFTPADDING", (0, 0), (-1, -1), 4),
+ ("RIGHTPADDING", (0, 0), (-1, -1), 4),
+ ("TOPPADDING", (0, 0), (-1, -1), 3),
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 3),
+ ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, HexColor("#f9f9f9")]),
+ ])
+ # header row style (if row 0 is all headers)
+ if all(header_flags[0]):
+ ts.add("BACKGROUND", (0, 0), (-1, 0), ACCENT)
+ # color header cells individually otherwise
+ for ri, flags in enumerate(header_flags):
+ for ci, is_h in enumerate(flags):
+ if is_h and ri > 0:
+ ts.add("BACKGROUND", (ci, ri), (ci, ri), ACCENT_LIGHT)
+ t.setStyle(ts)
+ return t
+
+
+def build_wrap(kind, text, styles):
+ bg = {"info": INFO_BG, "important": IMPORTANT_BG, "tip": TIP_BG}.get(kind, INFO_BG)
+ border = {"info": ACCENT, "important": HexColor("#e0a020"), "tip": HexColor("#28a745")}.get(kind, ACCENT)
+ label = {"info": "Info", "important": "Important", "tip": "Astuce"}.get(kind, "Note")
+ # Body can contain inline markup — render lines joined
+ text_fmt = fmt_inline(text.strip()).replace("\n", " ")
+ inner = f'{label} {text_fmt}'
+ p = Paragraph(inner, ParagraphStyle("wrap", fontSize=9.5, leading=13,
+ textColor=DARK, spaceAfter=4))
+ tbl = Table([[p]], colWidths=[17 * cm])
+ tbl.setStyle(TableStyle([
+ ("BACKGROUND", (0, 0), (-1, -1), bg),
+ ("BOX", (0, 0), (-1, -1), 1, border),
+ ("LEFTPADDING", (0, 0), (-1, -1), 10),
+ ("RIGHTPADDING", (0, 0), (-1, -1), 10),
+ ("TOPPADDING", (0, 0), (-1, -1), 8),
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
+ ("LINEBEFORE", (0, 0), (0, -1), 4, border),
+ ]))
+ return tbl
+
+
+def render_blocks(blocks, styles, title):
+ story = [
+ Paragraph(title, styles["H1c"]),
+ HRFlowable(width="100%", thickness=2, color=ACCENT, spaceBefore=2, spaceAfter=10),
+ ]
+ for kind, payload in blocks:
+ if kind == "h1":
+ story.append(Paragraph(fmt_inline(payload), styles["H1c"]))
+ elif kind == "h2":
+ story.append(Paragraph(fmt_inline(payload), styles["H2c"]))
+ story.append(HRFlowable(width="40%", thickness=1, color=ACCENT,
+ spaceBefore=0, spaceAfter=6))
+ elif kind == "h3":
+ story.append(Paragraph(fmt_inline(payload), styles["H3c"]))
+ elif kind == "h4":
+ story.append(Paragraph(fmt_inline(payload), styles["H4c"]))
+ elif kind == "hr":
+ story.append(HRFlowable(width="100%", thickness=0.5,
+ color=colors.lightgrey, spaceBefore=6, spaceAfter=6))
+ elif kind == "para":
+ story.append(Paragraph(fmt_inline(payload), styles["Bodyc"]))
+ elif kind == "italic":
+ story.append(Paragraph(f"{fmt_inline(payload)} ", styles["ItalicFooter"]))
+ elif kind == "code":
+ story.append(Preformatted(payload, ParagraphStyle(
+ "code", fontName="Courier", fontSize=8, leading=10,
+ backColor=CODE_BG, borderColor=colors.lightgrey, borderWidth=0.5,
+ borderPadding=6, spaceAfter=8, spaceBefore=4, leftIndent=4)))
+ elif kind == "wrap":
+ w_kind, w_body = payload
+ story.append(build_wrap(w_kind, w_body, styles))
+ story.append(Spacer(1, 6))
+ elif kind == "table":
+ t = build_table(payload, styles)
+ if t:
+ story.append(t)
+ story.append(Spacer(1, 8))
+ elif kind == "bullets":
+ for indent, text in payload:
+ lvl = indent // 2
+ story.append(Paragraph(fmt_inline(text), ParagraphStyle(
+ "bl", fontSize=9.5, leading=13,
+ leftIndent=16 + lvl * 12, bulletIndent=4 + lvl * 12, spaceAfter=2),
+ bulletText="•"))
+ elif kind == "ordered":
+ for n, (indent, text) in enumerate(payload, 1):
+ lvl = max(0, (indent - 2) // 2)
+ story.append(Paragraph(fmt_inline(text), ParagraphStyle(
+ "ol", fontSize=9.5, leading=13,
+ leftIndent=18 + lvl * 12, bulletIndent=4 + lvl * 12, spaceAfter=2),
+ bulletText=f"{n}."))
+ elif kind == "space":
+ story.append(Spacer(1, 4))
+ return story
+
+
+def add_page_footer(canvas, doc):
+ canvas.saveState()
+ canvas.setFont("Helvetica", 8)
+ canvas.setFillColor(GRAY)
+ canvas.drawString(1.5 * cm, 1 * cm,
+ "SANEF DSI / Sécurité Opérationnelle — Plan d'action Qualys V3")
+ canvas.drawRightString(A4[0] - 1.5 * cm, 1 * cm, f"Page {canvas.getPageNumber()}")
+ canvas.restoreState()
+
+
+def generate(src_path, dst_path, title):
+ with open(src_path, "r", encoding="utf-8") as f:
+ raw = f.read()
+ blocks = parse_wiki(raw)
+ styles = build_styles()
+ story = render_blocks(blocks, styles, title)
+
+ doc = SimpleDocTemplate(dst_path, pagesize=A4,
+ leftMargin=1.5 * cm, rightMargin=1.5 * cm,
+ topMargin=1.8 * cm, bottomMargin=1.8 * cm,
+ title=title)
+ doc.build(story, onFirstPage=add_page_footer, onLaterPages=add_page_footer)
+ print(f"Generated: {dst_path}")
+
+
+if __name__ == "__main__":
+ # args: src title dst
+ src, title, dst = sys.argv[1], sys.argv[2], sys.argv[3]
+ generate(src, dst, title)