patchcenter/tools/generate_ppt.py
Admin MPCZ 677f621c81 Admin applications + correspondance cleanup + tools presentation DSI
- 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>
2026-04-13 21:11:58 +02:00

835 lines
36 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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