patchcenter/SANEF_PATCHING_PROCESS.md

19 KiB
Raw Blame History

Processus de Patching SANEF — Algorithme détaillé

Document technique extrait du code PatchCenter (avril 2026). Couvre le cycle complet : planification → exécution → validation → reporting.


1. Vue d'ensemble

PLANIFICATION → CAMPAGNE → PRÉREQUIS → CORRESPONDANCE → VALIDATION → EXÉCUTION → POST-PATCH → HISTORIQUE
     S-2           S-1        S-1           continu         J-1          J          J+1         continu

Acteurs :

  • Coordinateur : crée campagnes, assigne opérateurs, gère le planning
  • Opérateur SecOps (6) : Khalid, Mouaad, Thierno, Paul, Joel, Ayoub
  • Responsable applicatif : valide le post-patching non-prod
  • DSI : consulte dashboard et rapports

Règles métier :

  • Pas de patching vendredi (risque weekend)
  • Fenêtre : 9h21h (27 créneaux de 15 min/jour)
  • Budget : 35h/semaine pour ~20 serveurs
  • Non-prod AVANT prod (validation obligatoire)
  • Snapshot obligatoire avant patch (rollback possible)
  • Gel : S01, S30, S34, S51S53 + jours fériés
  • Trafic prod : interdit janviermars (déhivernage)

2. Planification annuelle

Table : patch_planning Source : Planning Patching 2026_ayoub.xlsxtools/import_planning_xlsx.py

2.1 Structure

Champ Description
year 2026
week_number 153
week_code S01S53
cycle 1 (janavr), 2 (avraoût), 3 (septdéc)
domain_code FK → domains.code (INFRASTRUC, trafic, PEA, FL, BI, GESTION)
env_scope prod, hprod, all, pilot, prod_pilot
status open, freeze, holiday, empty

2.2 Cycle type

Cycle 1 (Patch 1) : S02S15
    S02: Infrastructure HPROD
    S03: Trafic HPROD
    S04: Trafic PROD
    S05: Infrastructure PROD
    S06S07: vide
    S08: Péage HPROD/PROD Pilote
    S09: Péage PROD
    S10: vide
    S11: FL Test/Recette/Dev
    S12: FL Pré-Prod
    S13: BI + Gestion
    S14S15: FL Prod

Cycle 2 (Patch 2) : S16S35 (même rotation, décalé)
Cycle 3 (Patch 3) : S36S50 (même rotation, décalé)

2.3 Algorithme de validation planning

SI year < année_courante → REJET
SI year == année_courante ET week < semaine_courante → REJET
SI week == semaine_courante ET jour > mardi → REJET (trop tard)

week_start = lundi ISO de la semaine
week_end = dimanche
week_code = f"S{week:02d}"

INSERT INTO patch_planning (year, week_number, week_code, week_start, week_end,
                            cycle, domain_code, env_scope, status)

3. Création de campagne

Tables : campaigns, patch_sessions Route : POST /campaigns/create

3.1 Sélection des serveurs éligibles

-- Critères d'éligibilité (tous obligatoires)
WHERE servers.os_family = 'linux'
  AND servers.etat = 'Production'
  AND servers.patch_os_owner = 'secops'
  AND servers.licence_support IN ('active', 'els')

-- Filtre domaine/environnement selon le planning de la semaine
-- Si env_scope = 'prod'      → environment.name = 'Production'
-- Si env_scope = 'hprod'     → environment.name != 'Production'
-- Si env_scope = 'all'       → tous les environnements du domaine
-- Si env_scope = 'prod_pilot'→ Production + Pilote
-- Si domain_code = 'DMZ'     → inclut aussi zone = 'DMZ'

3.2 Partitionnement hprod / prod

hprod_servers = [s for s in eligible if s.env_name != 'Production' and s.id not in excluded]
prod_servers  = [s for s in eligible if s.env_name == 'Production' and s.id not in excluded]

# Tri par (app_group, hostname) pour grouper les applications
hprod_servers.sort(key=lambda s: (s.app_group or '', s.hostname))
prod_servers.sort(key=lambda s: (s.app_group or '', s.hostname))

3.3 Allocation des créneaux

27 créneaux/jour (15 min) :
  Matin  : 09h00, 09h15, ..., 12h00, 12h15  (13 slots)
  Après-midi : 14h00, 14h15, ..., 16h15, 16h30  (14 slots)

Jours hprod : Lundi + Mardi     → 54 créneaux max
Jours prod  : Mercredi + Jeudi  → 54 créneaux max

Pour chaque serveur :
  jour = jours[slot_index // 27]
  heure = DAILY_SLOTS[slot_index % 27]
  
  SI pref_patch_jour != 'indifferent' → forcer ce jour
  SI pref_patch_heure != 'indifferent' → forcer cette heure
  SI default_intervenant_id existe → forcer cet opérateur (forced_assignment=true)

INSERT INTO patch_sessions (campaign_id, server_id, status='pending',
                            date_prevue, heure_prevue, intervenant_id, forced_assignment)

3.4 Assignation automatique des opérateurs

Règles par priorité (table default_assignments) :
  1. Par serveur     (rule_type='server',    rule_value=hostname)
  2. Par app_type    (rule_type='app_type',  rule_value=app_type)
  3. Par app_group   (rule_type='app_group', rule_value=app_group)
  4. Par domaine     (rule_type='domain',    rule_value=domain_code)
  5. Par zone        (rule_type='zone',      rule_value=zone_name)

Pour chaque règle (ordre priorité ASC) :
  MATCH sessions non assignées → SET intervenant_id = rule.user_id

Auto-propagation app_group :
  SI opérateur assigné à serveur avec app_group X :
    → Tous les autres serveurs app_group X dans la même campagne
      reçoivent le même opérateur (propagation automatique)

3.5 Limites opérateurs

-- Table campaign_operator_limits (optionnel, par campagne)
-- Si max_servers > 0 ET count >= max_servers → refus d'assignation
SELECT COUNT(*) FROM patch_sessions
WHERE campaign_id = :cid AND intervenant_id = :uid AND status != 'excluded'

4. Vérification des prérequis

Service : prereq_service.py Route : POST /campaigns/{id}/check-prereqs

4.1 Algorithme par serveur

POUR chaque session (status='pending') :

  1. ÉLIGIBILITÉ
     SI licence_support = 'obsolete' → EXCLURE (raison: EOL)
     SI etat != 'Production'         → EXCLURE (raison: non_patchable)

  2. CONNECTIVITÉ TCP (port 22)
     Résolution DNS avec suffixes :
       "" → ".sanef.groupe" → ".sanef-rec.fr" → ".sanef.fr"
     
     POUR chaque suffixe :
       socket.connect(hostname+suffixe, port=22, timeout=5)
       SI OK → prereq_ssh = 'ok', BREAK
     
     SI aucun → prereq_ssh = 'ko' → EXCLURE (raison: creneau_inadequat)

  3. MÉTHODE ROLLBACK
     SI machine_type = 'vm'       → rollback_method = 'snapshot'
     SI machine_type = 'physical' → rollback_method = 'na'

  4. VÉRIFICATIONS SSH (si clé disponible)
     Connexion Paramiko avec clé /opt/patchcenter/keys/id_rsa_cybglobal.pem
     
     a) Espace disque :
        Commande : df -BM --output=target,avail | grep '^/ |^/var'
        Seuils   : / >= 1200 Mo, /var >= 800 Mo
        SI KO    → prereq_disk_ok = false
     
     b) Satellite Red Hat :
        Commande : subscription-manager identity
        SI "not_registered" → prereq_satellite = 'ko'
        SINON               → prereq_satellite = 'ok'

  5. RÉSULTAT
     prereq_validated = (ssh=ok ET disk_ok != false ET eligible)
     
     UPDATE patch_sessions SET
       prereq_ssh, prereq_satellite, rollback_method,
       prereq_disk_root_mb, prereq_disk_var_mb, prereq_disk_ok,
       prereq_validated, prereq_date = now()

  6. AUTO-EXCLUSION
     SI prereq_ssh='ko' OU prereq_disk_ok=false OU licence='obsolete' :
       UPDATE patch_sessions SET
         status = 'excluded',
         exclusion_reason = '...',
         excluded_by = 'system',
         excluded_at = now()

5. Correspondance prod ↔ non-prod

Table : server_correspondance Service : correspondance_service.py

5.1 Détection automatique par signature hostname

Règle de nommage SANEF :
  Position 1 : préfixe arbitraire
  Position 2 : indicateur environnement
    p, s         → Production
    r            → Recette
    t            → Test
    i, o         → Pré-production
    v            → Validation
    d            → Développement
  Position 3+  : suffixe applicatif

SIGNATURE = pos1 + "_" + pos3+
  Exemple : "vpinfadns1" → signature "v_infadns1"
            "vdinfadns1" → signature "v_infadns1" (même signature)
            "vpinfadns1" = prod, "vdinfadns1" = dev → LIEN

Exceptions (pas d'auto-détection) :
  - Hostnames commençant par "ls-" ou "sp"

POUR chaque signature :
  prods    = [serveurs avec env_char ∈ {p, s}]
  nonprods = [serveurs avec env_char ∈ {r, t, i, v, d, o}]
  
  SI 1 prod ET N nonprods :
    → Créer N liens (prod_server_id, nonprod_server_id, source='auto')
  
  SI 0 prod : orphelins (pas de lien)
  SI >1 prod : ambigu (skip)

5.2 Lien manuel

INSERT INTO server_correspondance
  (prod_server_id, nonprod_server_id, environment_code, source, created_by)
VALUES (:prod_id, :nonprod_id, :env_code, 'manual', :user_id)

6. Validation post-patching

Table : patch_validation Route : /patching/validations

6.1 Cycle de vie

                    ┌──────────────────┐
 Patching terminé → │   en_attente     │ ← notification responsable
                    └────────┬─────────┘
                             │
              ┌──────────────┼──────────────┐
              ▼              ▼              ▼
        validated_ok    validated_ko      forced
        (OK, RAS)     (problème détecté)  (forcé par admin)

6.2 Règle de blocage prod

def can_patch_prod(db, prod_server_id):
    """Le prod ne peut être patché que si TOUS ses non-prods liés sont validés."""
    
    nonprods = SELECT nonprod_server_id FROM server_correspondance
               WHERE prod_server_id = :prod_id
    
    SI aucun lien  OK (orphelin, pas de blocage)
    
    POUR chaque nonprod :
      last_status = SELECT status FROM patch_validation
                    WHERE server_id = nonprod.id
                    ORDER BY patch_date DESC LIMIT 1
      
      SI last_status NOT IN ('validated_ok', 'forced') :
         BLOQUÉ (ce non-prod n'est pas validé)
    
    RETOUR : (tous_validés, liste_bloqueurs)

7. Exécution — Mode Campagne Standard

Table : patch_sessions

7.1 Machine à états

pending
  ├─→ excluded     (auto-exclusion prérequis OU exclusion manuelle)
  ├─→ prereq_ok    (prérequis validés)
  └─→ in_progress  (opérateur démarre le patching)
       ├─→ patched  (succès)
       ├─→ failed   (échec)
       └─→ reported (validé + reporté)

excluded  → restaurable par admin
patched   → terminal (crée patch_validation)
failed    → terminal (investigation)
reported  → terminal

7.2 Ordre d'exécution

Lundi    → Hors-prod serveurs 127
Mardi    → Hors-prod serveurs 2854
           → Notification responsables applicatifs
Mercredi → Validation non-prod (responsable valide OK/KO)
           → SI tous les non-prods OK → feu vert prod
Mercredi → Prod serveurs 127
Jeudi    → Prod serveurs 2854

8. Exécution — Mode QuickWin (semi-automatique)

Tables : quickwin_runs, quickwin_entries, quickwin_logs Service : quickwin_service.py, quickwin_prereq_service.py, quickwin_snapshot_service.py

8.1 Machine à états du run

draft → prereq → snapshot → patching → result → completed
  ↑                                                  │
  └──────────────── revert to draft ─────────────────┘

8.2 Phase 1 : Création du run

# Entrée : year, week, label, server_ids
# Sortie  : run_id + quickwin_entries

reboot_pkgs = get_secret("patching_reboot_packages")
# kernel*, glibc*, systemd*, dbus*, polkit*, linux-firmware*,
# microcode_ctl*, tuned*, dracut*, grub2*, kexec-tools*,
# libselinux*, selinux-policy*, shim*, mokutil*,
# net-snmp*, NetworkManager*, network-scripts*, nss*, openssl-libs*

POUR chaque server_id :
  branch = "prod" si environment = 'Production' sinon "hprod"
  
  INSERT INTO quickwin_entries
    (run_id, server_id, branch, status='pending',
     general_excludes=reboot_pkgs, specific_excludes=server.patch_excludes)

8.3 Phase 2 : Prérequis (SSE streaming)

POUR chaque entry (branch demandé, non exclu) :

  1. RÉSOLUTION DNS
     Détection environnement par 2ème caractère hostname :
       prod/preprod : essayer sanef.groupe puis sanef-rec.fr
       recette/test : essayer sanef-rec.fr puis sanef.groupe
     
     socket.getaddrinfo(fqdn, 22) → SI résolu : OK

  2. CONNEXION SSH (chaîne de fallback)
     SI ssh_method = "ssh_psmp" :
       ESSAYER : connexion PSMP (psmp.sanef.fr, user="CYBP01336@cybsecope@{fqdn}")
       FALLBACK : connexion clé SSH directe
     SINON :
       ESSAYER : connexion clé SSH directe
       FALLBACK : connexion PSMP

  3. ESPACE DISQUE (via SSH)
     sudo df / /var --output=target,pcent
     SI usage >= 90% → disk_ok = false

  4. SATELLITE / YUM
     sudo subscription-manager status
     OU sudo yum repolist
     SI 0 repos → satellite_ok = false

  5. RÉSULTAT
     prereq_ok = dns AND ssh AND satellite AND disk
     
     UPDATE quickwin_entries SET prereq_ok, prereq_detail, prereq_date
     EMIT SSE event → affichage temps réel dans le navigateur

8.4 Phase 3 : Snapshots (VMs uniquement)

Ordre vCenter selon la branche :
  hprod : Senlis (vpgesavcs1) → Nanterre (vpmetavcs1) → DR (vpsicavcs1)
  prod  : Nanterre → Senlis → DR

POUR chaque entry (prereq_ok = true) :

  SI machine_type = 'physical' :
    snap_done = true (pas de snapshot, vérifier backup Commvault)
    CONTINUE
  
  POUR chaque vCenter dans l'ordre :
    Connexion pyVmomi → recherche VM par nom (vcenter_vm_name ou hostname)
    SI trouvé :
      snap_name = f"QW_{run_id}_{branch}_{YYYYMMDD_HHMM}"
      Créer snapshot (memory=false, quiesce=true)
      snap_done = true
      BREAK
  
  SI non trouvé sur aucun vCenter :
    snap_done = false → LOG ERREUR

  EMIT SSE event

8.5 Phase 4 : Patching (SSE streaming)

POUR chaque entry (snap_done = true, branch demandé) :

  1. CONSTRUCTION COMMANDE YUM
     excludes = parse(general_excludes + " " + specific_excludes)
     args = " ".join(f"--exclude={pkg}" for pkg in excludes)
     cmd = f"yum update -y {args}"
     
     Exemple :
       yum update -y --exclude=kernel* --exclude=glibc* --exclude=systemd*

  2. EXÉCUTION SSH
     Connexion SSH (même chaîne que prérequis)
     stdin, stdout, stderr = client.exec_command(cmd, timeout=600)
     output = stdout.read().decode('utf-8')
     exit_code = stdout.channel.recv_exit_status()

  3. ANALYSE SORTIE
     Packages comptés : lignes "Updating", "Installing", "Upgrading"
     Rien à faire : "Rien à faire" ou "Nothing to do"
     Reboot requis : "kernel" ou "reboot" dans output

  4. MISE À JOUR
     status = "patched" si exit_code == 0 sinon "failed"
     
     UPDATE quickwin_entries SET
       status, patch_output, patch_packages_count,
       patch_packages, reboot_required, patch_date

  5. CRÉATION VALIDATION
     SI status = "patched" :
       INSERT INTO patch_validation
         (server_id, campaign_id=run_id, campaign_type='quickwin',
          patch_date=now(), status='en_attente')

  EMIT SSE event {hostname, ok, packages, reboot, detail}

8.6 Phase 5 : Passage prod

AVANT de lancer le patching prod :

  can_start_prod(db, run_id) :
    SELECT COUNT(*) FROM quickwin_entries
    WHERE run_id = :rid AND branch = 'hprod'
      AND status IN ('pending', 'in_progress')
    
    SI count > 0 → BLOQUÉ (hprod pas terminé)
  
  check_prod_validations(db, run_id) :
    POUR chaque entry prod :
      Vérifier que tous les non-prods liés sont validated_ok/forced
    
    SI blockers > 0 → BLOQUÉ (validation manquante)

9. Post-patching

9.1 Enregistrement historique

-- Après chaque patch (standard ou quickwin)
INSERT INTO patch_history
  (server_id, campaign_id, intervenant_id, date_patch, status, notes, intervenant_name)
VALUES (:sid, :cid, :uid, now(), 'ok'/'ko', :notes, :interv_name)

9.2 Notifications

Déclencheurs :
  - Début patching  → notif_debut_sent = true
  - Reboot effectué → notif_reboot_sent = true
  - Fin patching    → notif_fin_sent = true
  
Canal : Teams webhook (configurable dans settings)

9.3 Rollback

SI status = 'failed' ET rollback_method = 'snapshot' :
  → Restaurer snapshot vCenter (manuel ou via quickwin_snapshot_service)
  → Marquer rollback_justif dans patch_sessions

10. Reporting

10.1 Dashboard KPIs

KPI Requête
Serveurs patchés 2026 COUNT(DISTINCT server_id) FROM patch_history WHERE year=2026
Events patching 2026 COUNT(*) FROM patch_history WHERE year=2026
Jamais patchés (prod) Serveurs Production + secops sans entrée patch_history cette année
Couverture % patchés / patchables × 100
Dernière semaine MAX(TO_CHAR(date_patch, 'IW'))

10.2 Historique (/patching/historique)

Sources unifiées :

  • patch_history (imports xlsx + campagnes standard)
  • quickwin_entries WHERE status='patched'

Filtres : année, semaine, OS, zone, domaine, intervenant, source, hostname

10.3 Intégrations

Source Usage
iTop Serveurs, contacts, applications, domaines, environnements
Qualys Assets, tags V3, agents, scans post-patch
CyberArk Accès PSMP pour SSH sur serveurs restreints
Sentinel One Agents endpoint (comparaison couverture)
AD SANEF Groupe secops (8 users), auth LDAP

11. Schéma de données

patch_planning ──→ campaigns ──→ patch_sessions ──→ patch_history
     (annuel)       (hebdo)      (1 par serveur)     (audit trail)
                                      │
                                      ▼
                                patch_validation
                                (non-prod → prod gate)
                                      ▲
                                      │
quickwin_runs ──→ quickwin_entries ────┘
   (run)          (1 par serveur)
                       │
                       ▼
                  quickwin_logs
                  (traces détaillées)

servers ──→ domain_environments ──→ domains
   │              │                 environments
   │              │
   ├──→ zones
   ├──→ server_correspondance (prod ↔ non-prod)
   ├──→ server_ips
   ├──→ server_databases
   ├──→ qualys_assets ──→ qualys_asset_tags ──→ qualys_tags
   └──→ applications (via application_id)

users ──→ contacts (via contact_id FK)
  │         │
  │         └──→ ldap_dn (source AD)
  │
  └──→ default_assignments (règles assignation)
  └──→ campaign_operator_limits

12. Exclusions packages (yum --exclude)

12.1 Packages reboot (globaux)

kernel*, glibc*, systemd*, dbus*, polkit*, linux-firmware*,
microcode_ctl*, tuned*, dracut*, grub2*, kexec-tools*,
libselinux*, selinux-policy*, shim*, mokutil*,
net-snmp*, NetworkManager*, network-scripts*, nss*, openssl-libs*

Stockés dans app_secrets['patching_reboot_packages']. Appliqués à quickwin_entries.general_excludes à la création du run.

12.2 Exclusions par serveur

Champ servers.patch_excludes (texte libre, séparé par espaces). Gestion via /patching/config-exclusions (UI + bulk). Synchronisé avec iTop (best-effort).

12.3 Commande générée

yum update -y --exclude=kernel* --exclude=glibc* --exclude=systemd* \
              --exclude=<specific1> --exclude=<specific2>