- Admin applications: CRUD module (list/add/edit/delete/assign/multi-app) avec push iTop bidirectionnel (applications.py + 3 templates) - Correspondance prod<->hors-prod: migration vers server_correspondance globale, suppression ancien code quickwin, ajout filtre environnement et solution applicative, colonne environnement dans builder - Servers page: colonne application_name + equivalent(s) via get_links_bulk, filtre application_id, push iTop sur changement application - Patching: bulk_update_application, bulk_update_excludes, validations - Fix paramiko sftp.put (remote_path -> positional arg) - Tools: wiki_to_pdf.py (DokuWiki -> PDF) + generate_ppt.py (PPTX 19 slides DSI patching) + contenu source (processus_patching.txt, script_presentation.txt) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
835 lines
36 KiB
Python
835 lines
36 KiB
Python
"""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}")
|