"""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}")