Expert

Automatisation Réseau et SOC

API REST avec requests, pilotage CLI avec Netmiko, parsing de logs et IOC, mini-projets complets

Avertissement lab : les scripts de ce niveau envoient des commandes à des équipements et scannent des ports. Testez-les uniquement sur votre lab (GNS3, EVE-NG, machines virtuelles) et jamais sur des systèmes sans autorisation écrite.
[09]Module 9

Requêtes HTTP et API REST avec requests

Dialoguer avec des API : GET, POST, authentification, codes de statut, pagination, exploitation du JSON.

HTTPrequestsJSONAPI réseau
9.1 HTTP et API REST : les fondamentaux

Une API REST expose des ressources via des URL, manipulées avec les verbes HTTP : GET (lire), POST (créer), PUT/PATCH (modifier), DELETE (supprimer). Le serveur répond avec un code de statut : 2xx succès, 4xx erreur client, 5xx erreur serveur :

# Codes a connaitre par coeur
# 200 OK | 201 Created | 204 No Content
# 400 Bad Request | 401 Unauthorized | 403 Forbidden | 404 Not Found
# 429 Too Many Requests | 500 Internal Server Error
Pourquoi c'est important : Les inventaires NetBox, les contrôleurs SDN et les SIEM modernes ne se pilotent plus en CLI copier-coller mais en API REST. Comprendre verbes, codes et pagination, c'est parler leur langue avant même d'écrire une ligne de requests.
L'analogie qui aide : L'API, c'est un restaurant : l'URL est la carte, les verbes sont les actions (GET = lire le menu, POST = commander, DELETE = annuler), et le code de statut est l'addition qui dit si tout s'est bien passé (200) ou si ta carte est refusée (401).

Les API réseau (NetBox pour l'inventaire, contrôleurs SDN, plateformes SOC) renvoient du JSON et exigent un token d'authentification passé dans l'en-tête Authorization. Les volumes importants se parcourent par pagination (paramètres limit / offset ou page suivante).

Démonstration pas à pas : Déroulé type :
1. GET /devices avec en-tête Authorization. État : demande envoyée.
2. Serveur répond 200 + JSON {« results »: [...]}. État : succès, on parse.
3. S'il manque des pages : on relance avec ?limit=50&offset=50. État : inventaire complet.
Codes à lire d'un coup d'œil : 200 OK, 201 créé, 401 non authentifié, 404 introuvable, 500 faute serveur.
Pièges classiques : Confondre 401 (qui es-tu ? montre ton token) et 403 (je sais qui tu es, mais interdit). Oublier la pagination : tu ne vois que les 50 premiers objets et tu crois l'inventaire complet. Mettre le token en dur dans le code au lieu d'une variable d'environnement. Lancer des requêtes sans timeout.
À vous de jouer : Quel code attend-on après un POST de création réussi ? Et que signifie un 401 ? Vérifie avec la leçon.
Voir la réponse

Réponse : 201 Created pour une création réussie, 401 Unauthorized = token manquant ou invalide, il faut vérifier ses identifiants.
Explication : 200 = lecture OK, 201 = création OK, 401 = authentification à refaire.

Exercice 9.1

À faire : listez de mémoire les 4 verbes CRUD et leur correspondance, puis les codes 200, 401, 404 et 500. Vérifiez avec la fiche mémo.
9.2 GET avec requests : lire une API

La bibliothèque requests (pip install requests) simplifie le HTTP. requests.get() envoie la requête, .status_code donne le code, .json() convertit la réponse JSON en dictionnaire :

import requests

url = "https://api.example.com/devices"
headers = {"Authorization": "Token MON_TOKEN_SECRET"}
try:
    r = requests.get(url, headers=headers, timeout=10)
    r.raise_for_status()          # leve une erreur si code 4xx/5xx
    for eq in r.json()["results"]:
        print(eq["hostname"], "-", eq["ip"])
except requests.RequestException as e:
    print(f"Erreur API : {e}")
Pourquoi c'est important : Lire l'inventaire NetBox ou la liste des alertes du SIEM à la main, c'est recopier des tableaux sans fin. Un GET avec token et timeout fait la même lecture en une seconde, avec gestion d'erreur propre.
L'analogie qui aide : C'est appeler le standard avec ton badge (le token dans l'en-tête) : tu demandes la liste (get()), le standard vérifie ton badge (raise_for_status()), puis te lit les fiches (.json()["results"]).

Imposez toujours un timeout (sinon le script peut attendre indéfiniment) et appelez raise_for_status() pour transformer les erreurs HTTP en exceptions traitables. Les paramètres d'URL passent par params={"limit": 50}.

Démonstration pas à pas : Trace nominale : requests.get(url, headers=..., timeout=10) → réponse 200.
r.raise_for_status() → rien levé, on continue.
r.json()["results"] → ex. [{« hostname »: « R1 », « ip »: « 10.0.0.1 »}, {« hostname »: « SW1 », ...}].
Boucle : affiche « R1 - 10.0.0.1 » puis « SW1 - ... ».
Cas d'échec : 404 ou timeout → requests.RequestException → bloc except → « Erreur API : ... ».
Pièges classiques : Omettre timeout : le script peut attendre indéfiniment un serveur mort. Oublier raise_for_status() : tu parses une page d'erreur 404 comme du JSON et ça explose plus loin. Appeler .json() sur une réponse non-JSON. Coller le token en dur dans le script versionné.
À vous de jouer : Que doit retourner lister_equipements(url, token) en cas d'erreur réseau, et pourquoi ?
Voir la réponse

Réponse : une liste vide [].
Explication : l'appelant peut toujours boucler dessus sans tester None, et le except requests.RequestException avec timeout garantit qu'on ne plante pas.

Exercice 9.2

À faire : écrivez lister_equipements(url, token) qui retourne la liste des hostnames lus dans la clé « results », ou une liste vide en cas d'erreur (try/except + timeout).
9.3 POST : créer des ressources et gérer les erreurs

requests.post() envoie des données avec le paramètre json= (sérialisation automatique + bon Content-Type). On vérifie le code 201 et on exploite la réponse pour récupérer l'objet créé :

import requests

payload = {"hostname": "SW3", "ip": "10.0.0.5", "role": "access"}
r = requests.post("https://api.example.com/devices",
                  json=payload, headers={"Authorization": "Token X"},
                  timeout=10)
if r.status_code == 201:
    print("Cree :", r.json()["hostname"])
elif r.status_code == 401:
    print("Token invalide : verifiez vos identifiants")
else:
    print(f"Echec ({r.status_code}) : {r.text}")
Pourquoi c'est important : Déclarer un équipement dans NetBox ou pousser une alerte vers le SIEM, c'est une création : sans POST maîtrisé, tu restes coincé en lecture seule et quelqu'un d'autre fait la saisie à la main.
L'analogie qui aide : Le POST, c'est déposer un formulaire au guichet : tu glisses ton dossier (json=payload), le guichet tamponne 201 (accepté, créé) ou te le rend avec le motif du refus (401, 400).

Ne stockez jamais un token en dur dans le code : lisez-le depuis une variable d'environnement (os.environ["API_TOKEN"]) ou un fichier de configuration exclu du versionnement.

Démonstration pas à pas : Trace : payload = {« hostname »: « SW3 », « ip »: « 10.0.0.5 », « role »: « access »}.
requests.post(..., json=payload, timeout=10)json= sérialise et pose le bon Content-Type.
Réponse 201 → r.json()["hostname"] = « SW3 » → affiche « Cree : SW3 ».
Réponse 401 → « Token invalide ». Autre → « Echec (code) : texte ».
Pièges classiques : Utiliser data= au lieu de json= : le serveur reçoit du formulaire au lieu du JSON et refuse. Tester == 200 après un POST : une création réussie renvoie 201. Laisser le token en dur dans le code. Ne gérer aucun timeout ni aucun code autre que le succès.
À vous de jouer : Que doit retourner creer_alerte(url, token, ip, niveau) si le serveur répond 201 ? Et s'il répond 401 ?
Voir la réponse

Réponse : True si 201, False sinon (donc False pour 401).
Explication : l'appelant teste le booléen pour journaliser « alerte créée » ou « échec d'envoi », sans parser lui-même les codes.

Exercice 9.3

À faire : écrivez creer_alerte(url, token, ip, niveau) qui POSTE le dictionnaire correspondant et retourne True si le code est 201, False sinon.
[10]Module 10

Netmiko et automatisation CLI réseau

Piloter des équipements en SSH : connexion, envoi de show, déploiement de configuration, sauvegardes.

SSHNetmikoshowSauvegarde
10.1 Première connexion SSH avec Netmiko

Netmiko (pip install netmiko) pilote les CLI réseau en SSH. On décrit l'équipement par un dictionnaire (type, hôte, identifiants) et on utilise ConnectHandler en gestionnaire de contexte pour fermer proprement la session :

from netmiko import ConnectHandler

r1 = {
    "device_type": "cisco_ios",
    "host": "10.0.0.1",
    "username": "admin",
    "password": "mot_de_passe_lab",
}
with ConnectHandler(**r1) as ssh:
    sortie = ssh.send_command("show ip interface brief")
    print(sortie)
Pourquoi c'est important : Collecter un show ip interface brief sur 50 équipements à la main prend une matinée et trois fautes de frappe. Netmiko fait la connexion SSH, envoie la commande et rend la sortie, équipement par équipement, sans erreur.
L'analogie qui aide : C'est une télécommande universelle : le device_type choisit l'adaptateur (cisco_ios, arista_eos...), le dictionnaire donne l'adresse et les piles (identifiants), ConnectHandler appuie sur les boutons puis éteint proprement.

Types courants : cisco_ios, cisco_nxos, arista_eos, juniper_junos, linux. Stockez les identifiants hors du code (variables d'environnement ou coffre) et testez d'abord en lecture seule (show) avant toute écriture.

Démonstration pas à pas : Trace : dict r1 = type cisco_ios, host 10.0.0.1, identifiants lab.
ConnectHandler(**r1) → session SSH ouverte (le ** déballe le dict en arguments).
ssh.send_command("show ip interface brief") → sortie texte de l'équipement.
Sortie du with → session fermée automatiquement, même en cas d'erreur.
Pièges classiques : Mauvais device_type : les prompts ne sont pas reconnus et la session échoue. Identifiants en dur dans le script versionné. Session ouverte sans with et jamais fermée : le pool SSH de l'équipement sature. Commencer par de l'écriture avant d'avoir validé la lecture seule.
À vous de jouer : Que retourne collecter(hote, commandes) et à quoi ressemble son contenu pour ["show version"] ?
Voir la réponse

Réponse : un dict {commande: sortie}, ex. {« show version »: « Cisco IOS Software, ... »}.
Explication : la clé est la commande envoyée, la valeur le texte brut renvoyé par l'équipement via send_command().

Exercice 10.1

À faire : écrivez collecter(hote, commandes) qui se connecte, exécute chaque commande show de la liste et retourne un dict {commande: sortie}.
10.2 Déployer une configuration sur un parc

send_config_set() envoie une liste de commandes en mode configuration. En bouclant sur un inventaire CSV, on déploie la même politique (VLAN, banner, NTP) sur tout le parc en quelques secondes :

import csv
from netmiko import ConnectHandler

config_vlan = ["vlan 30", "name INVITES", "exit",
               "interface range Fa0/5-8",
               "switchport mode access",
               "switchport access vlan 30"]

with open("parc.csv", encoding="utf-8", newline="") as f:
    for eq in csv.DictReader(f):
        dev = {"device_type": "cisco_ios", "host": eq["ip"],
               "username": "admin", "password": "mot_de_passe_lab"}
        with ConnectHandler(**dev) as ssh:
            print(ssh.send_config_set(config_vlan))
            ssh.save_config()   # = write memory
Pourquoi c'est important : Créer un VLAN ou poser un banner sur 30 switchs un par un, c'est une journée d'erreurs. La boucle inventaire CSV + send_config_set() déploie la même politique partout en quelques secondes, avec journalisation.
L'analogie qui aide : C'est le publipostage : tu écris la lettre une fois (la liste config_vlan), le script l'imprime et la poste à chaque adresse de l'annuaire (chaque ligne du CSV).

Bonnes pratiques : déployez d'abord sur un équipement pilote, journalisez chaque résultat avec logging, et prévoyez un plan de retour arrière (sauvegarde préalable, leçon 10.3).

Démonstration pas à pas : Trace avec 2 lignes CSV :
Itération 1 : eq = R1/10.0.0.1 → connexion → send_config_set(config_vlan) → 6 commandes appliquées → save_config() → affiche le retour.
Itération 2 : idem pour SW1/10.0.0.2.
État final : les deux équipements ont le VLAN 30 et les ports Fa0/5-8 en access, avec sauvegarde persistante.
Pièges classiques : Déployer sur tout le parc sans tester sur un pilote d'abord. Aucune sauvegarde préalable (leçon 10.3). Pas de try/except par hôte : le premier équipement injoignable arrête toute la boucle. Oublier save_config() : la config disparaît au reboot.
À vous de jouer : Quelle méthode envoie une liste de commandes en mode configuration, et que faut-il appeler ensuite pour persister ?
Voir la réponse

Réponse : ssh.send_config_set(config_vlan) puis ssh.save_config() (équivalent de write memory).
Explication : la première applique en running-config, la seconde sauvegarde pour survivre au redémarrage.

Exercice 10.2

À faire : adaptez le script pour déployer le banner « Acces reserve - Lab » (banner motd #...#) sur les équipements du CSV, avec try/except autour de chaque connexion.
10.3 Sauvegardes automatiques et parsing de show

Avant tout changement, sauvegardez la running-config avec un nom horodaté. Ensuite, parsez les sorties show avec des regex pour en extraire des données structurées (interfaces down, VLAN, routes) :

import re
from datetime import datetime
from netmiko import ConnectHandler

with ConnectHandler(**r1) as ssh:
    conf = ssh.send_command("show running-config")
    nom = f"sauvegardes/R1_{datetime.now():%Y%m%d_%H%M}.cfg"
    with open(nom, "w", encoding="utf-8") as f:
        f.write(conf)

    brief = ssh.send_command("show ip interface brief")
    down = re.findall(r"^(\S+)\s+\S+\s+\S+\s+\S+\s+down",
                      brief, re.MULTILINE)
    print("Interfaces down :", down)
Pourquoi c'est important : Aucun changement sans filet : une sauvegarde horodatée avant chaque déploiement permet le retour arrière, et parser les show détecte les interfaces down avant que les utilisateurs appellent.
L'analogie qui aide : C'est la photocopie avant travaux (la sauvegarde) puis le surlignage des pannes au marqueur (la regex qui extrait les lignes « down »).

Planifiez ce script (tâche cron ou planificateur Windows) pour une sauvegarde quotidienne, et versionnez les sauvegardes avec Git pour tracer chaque changement de configuration.

Démonstration pas à pas : Trace : send_command("show running-config") → texte conf.
nom = "sauvegardes/R1_20260909_1430.cfg" via datetime.now() → écriture du fichier.
brief = sortie du show → re.findall(r"^(\S+)\s+\S+\s+\S+\s+\S+\s+down", brief, re.MULTILINE) → ex. [« Gi0/2 »] : chaque ligne dont le statut est down est capturée par le premier groupe.
Pièges classiques : Le dossier sauvegardes/ n'existe pas → FileNotFoundError à l'écriture : crée-le avant. Regex de show fragile : une version d'OS différente change les colonnes. Tâche planifiée avec chemin relatif : elle s'exécute ailleurs et ne trouve rien, utilise des chemins absolus. Accents sans encoding="utf-8".
À vous de jouer : Avec un show ip interface brief contenant deux lignes au statut down, que contient la liste down ?
Voir la réponse

Réponse : les deux noms d'interfaces, ex. ['Gi0/1', 'Gi0/2'].
Explication : findall() avec re.MULTILINE capture le premier groupe de chaque ligne qui se termine par down.

Exercice 10.3

À faire : écrivez une regex qui extrait les couples (interface, IP) des lignes « GigabitEthernet0/0 ... 10.0.0.1 » d'un « show ip interface brief » simulé.
[11]Module 11

Automatisation SOC : logs et IOC

Du log brut au rapport d'incident : parser, compter, croiser avec des IOC et générer des alertes.

ParsingCounterIOCAlertes
11.1 Parser des logs de sécurité

Les logs (pare-feu, proxy, IDS) sont des lignes semi-structurées. La recette SOC : lire ligne par ligne (jamais tout en mémoire pour les gros fichiers), extraire les champs par regex, filtrer les événements intéressants :

import re

motif = re.compile(r"SRC=(?P<src>\d+\.\d+\.\d+\.\d+).*DPT=(?P<port>\d+)")
evenements = []
with open("firewall.log", encoding="utf-8") as f:
    for ligne in f:
        if "DROP" in ligne:
            m = motif.search(ligne)
            if m:
                evenements.append(m.groupdict())
print(f"{len(evenements)} paquets bloques analyses")
Pourquoi c'est important : Un pare-feu crache des millions de lignes par jour. Les lire une par une à l'œil est impossible ; les parser ligne par ligne avec un pré-filtre et une regex compilée extrait exactement les DROP exploitables.
L'analogie qui aide : C'est un tapis roulant avec trieur : le test if "DROP" in ligne jette d'abord les cartons « ALLOW », puis la regex ouvre les cartons restants et en sort les étiquettes src et port.

m.groupdict() retourne directement un dictionnaire des groupes nommés — parfait pour alimenter un CSV ou une API. Filtrez tôt (ici « DROP ») pour ne parser que l'utile : c'est 10 fois plus rapide sur des millions de lignes.

Démonstration pas à pas : Trace sur « ... DROP ... SRC=203.0.113.7 ... DPT=443 » :
1. "DROP" in ligne → True, on garde.
2. motif.search(ligne) → match, groupes src = 203.0.113.7, port = 443.
3. m.groupdict() → {« src »: « 203.0.113.7 », « port »: « 443 »} ajouté à evenements.
Une ligne ALLOW est jetée à l'étape 1 sans regex : 10 fois plus rapide sur gros volume.
Pièges classiques : Charger tout le fichier avec .read() : 2 Go de logs saturent la RAM, itère ligne par ligne. Parser chaque ligne sans pré-filtre : la regex sur des millions de lignes ALLOW est lente. Groupe nommé mal fermé (?P<src>...) → erreur de syntaxe regex. Oublier l'encodage à l'ouverture.
À vous de jouer : Avec 5 lignes simulées dont 2 DROP valides, combien d'éléments contient evenements à la fin ?
Voir la réponse

Réponse : 2.
Explication : seules les lignes contenant DROP passent le filtre, et chacune ajoute un dict {src, port} via groupdict().

Exercice 11.1

À faire : parsez 5 lignes simulées « ALLOW/DROP SRC=... DPT=... » et construisez la liste des dictionnaires {src, port} pour les DROP uniquement.
11.2 Compter, détecter et croiser des IOC

collections.Counter compte les occurrences en une ligne : top talkers, ports les plus attaqués, codes HTTP anormaux. On croise ensuite avec une liste d'IOC (indicateurs de compromission : IP, domaines, hash malveillants) via des sets :

from collections import Counter

sources = [e["src"] for e in evenements]
top = Counter(sources).most_common(5)
print("Top sources bloquees :", top)

ioc = {"5.6.7.8", "9.9.9.9"}   # issus du flux de threat intel
detectees = set(sources) & ioc
for ip in detectees:
    print(f"CRITIQUE : IOC connu detecte : {ip}")
Pourquoi c'est important : Extraire les événements ne suffit pas : il faut classer (top 5 des sources), décider (seuil anti-bruit) et confronter au renseignement (croisement IOC). C'est ce trio qui transforme du bruit en détection.
L'analogie qui aide : C'est un vote suivi d'un contrôle des suspects : Counter dépouille les bulletins (les src) et donne le top 5, puis le set compare la liste des votants à la liste des suspects (les IOC) par intersection.

Ajoutez un seuil (ex. plus de 10 tentatives) pour distinguer le bruit de fond des vraies attaques, et journalisez chaque détection avec horodatage pour la traçabilité de l'incident.

Démonstration pas à pas : Trace : sources = [« 1.1.1.1 », « 2.2.2.2 », « 1.1.1.1 », ...].
Counter(sources) → {« 1.1.1.1 »: 3, ...}.
.most_common(5) → [(« 1.1.1.1 », 3), ...].
set(sources) & {"5.6.7.8", "9.9.9.9"} → ex. {« 5.6.7.8 »} si présente → affiche « CRITIQUE : IOC connu detecte : 5.6.7.8 ».
Pièges classiques : Compter sans seuil : chaque scan de fond devient une alerte et noie l'équipe. Passer des dicts (non hashables) à Counter ou au set au lieu des chaînes src. Oublier que le set perd l'ordre : trie pour le rapport. most_common() sans argument sur un million de clés affiche tout.
À vous de jouer : Prédis la sortie, puis vérifie :
from collections import Counter
c = Counter(["1.1.1.1", "2.2.2.2", "1.1.1.1", "3.3.3.3", "1.1.1.1"])
print(c.most_common(1))
print("1.1.1.1" in {"1.1.1.1"})
Voir la réponse

Sortie :
[('1.1.1.1', 3)]
True

Explication : « 1.1.1.1 » apparaît 3 fois, c'est le top 1, et elle figure bien dans l'ensemble d'IOC.

Exercice 11.2

À faire : avec Counter, trouvez l'IP la plus fréquente de ["1.1.1.1", "2.2.2.2", "1.1.1.1", "3.3.3.3", "1.1.1.1"] et vérifiez si elle figure dans ioc = {"1.1.1.1"}.
11.3 Générer un rapport SOC

Un rapport SOC horodaté, chiffré et actionnable : période couverte, volumes, top 5, IOC détectés, recommandations. On le génère en texte et en CSV pour l'outil de ticketing :

import csv
from datetime import datetime

def generer_rapport(evenements, detectees, seuil=10):
    lignes = [f"Rapport SOC - {datetime.now():%Y-%m-%d %H:%M}",
              f"Evenements analyses : {len(evenements)}",
              f"IOC detectes : {len(detectees)}"]
    for ip in sorted(detectees):
        lignes.append(f"  - {ip} : blocage immediat recommande")
    texte = "\n".join(lignes)
    with open("rapport_soc.txt", "w", encoding="utf-8") as f:
        f.write(texte)
    return texte
Pourquoi c'est important : Une détection sans rapport horodaté, chiffré et actionnable ne sert à rien : le manager ne peut ni décider ni prouver. Générer le texte et le CSV automatiquement, c'est livrer un dossier complet en une commande.
L'analogie qui aide : C'est le compte-rendu d'intervention du pompier : date et heure, ampleur du sinistre (volumes), points chauds (top), suspects identifiés (IOC) et conduite à tenir (blocage recommandé).

Automatisez l'envoi (e-mail, webhook, API du SIEM) et archivez chaque rapport : un SOC doit pouvoir prouver ce qui a été détecté, quand, et quelle action a été recommandée.

Démonstration pas à pas : Trace avec evenements de 100 éléments et detectees = {« 5.6.7.8 »} :
lignes = [« Rapport SOC - 2026-09-09 14:30 », « Evenements analyses : 100 », « IOC detectes : 1 », « - 5.6.7.8 : blocage immediat recommande »].
"\n".join(lignes) → texte final → écrit dans rapport_soc.txt → renvoyé aussi à l'appelant.
Pièges classiques : Rapport sans horodatage : impossible de prouver quand la détection a eu lieu. Chemin relatif qui écrit le rapport ailleurs qu'attendu. CSV ouvert sans newline="" : lignes vides sous Windows. Rapport non archivé : aucune traçabilité en cas d'audit.
À vous de jouer : Prédis la sortie, puis vérifie :
def resume(evenements):
srcs = {e["src"] for e in evenements}
return f"{len(evenements)} evenements, {len(srcs)} sources uniques"
print(resume([{"src": "a", "port": "80"}, {"src": "b", "port": "22"}]))
Voir la réponse

Sortie : 2 evenements, 2 sources uniques.
Explication : 2 événements et un set de 2 sources distinctes (« a », « b »).

Exercice 11.3

À faire : écrivez resume(evenements) qui retourne une chaîne « X événements, Y sources uniques » à partir d'une liste de dicts {src, port}.
[12]Module 12

Mini-projets : scanner et rapports

Deux projets complets qui combinent tout le parcours : scanner de ports et générateur de rapports.

socketProjetQualité
12.1 Mini-projet 1 : scanner de ports

Le module socket ouvre des connexions TCP avec un timeout court : si la connexion réussit, le port est ouvert. On boucle sur une liste de ports courants et on structure le résultat :

import socket

def scanner(hote, ports, timeout=1.0):
    """Retourne le dict {port: True/False} des ports ouverts."""
    resultats = {}
    for p in ports:
        s = socket.socket()
        s.settimeout(timeout)
        try:
            s.connect((hote, p))
            resultats[p] = True
        except OSError:
            resultats[p] = False
        finally:
            s.close()
    return resultats

print(scanner("127.0.0.1", [22, 80, 443]))
Pourquoi c'est important : Avant un audit, vérifier quels ports sont réellement exposés sur tes propres machines évite les mauvaises surprises. socket avec timeout court donne un verdict ouvert/fermé fiable, à condition de ne scanner que ton lab.
L'analogie qui aide : C'est toquer à chaque porte de l'immeuble : si on ouvre vite (connect() réussit), la porte est ouverte ; si personne ne répond (exception OSError), elle est fermée. Le timeout, c'est le temps max avant de partir.

Limites à connaître : scannez uniquement vos machines de lab, utilisez des timeouts courts, parallélisez avec concurrent.futures pour les gros périmètres, et journalisez chaque résultat.

Démonstration pas à pas : Trace de scanner("127.0.0.1", [22, 80, 443]) :
p = 22 → connect(("127.0.0.1", 22)) réussit → resultats[22] = True → close().
p = 80 → OSError (refusé) → False → close() dans finally.
p = 443 → idem. État final : {22: True, 80: False, 443: False}.
Pièges classiques : Scanner une machine sans autorisation écrite est illégal, même avec un petit script : lab uniquement. Oublier le timeout : chaque port fermé bloque longtemps. Oublier s.close() (ou le finally) : les descripteurs saturent. Scanner séquentiellement un /24 entier : trop lent, parallélise avec concurrent.futures.
À vous de jouer : Avec {22: True, 80: False, 443: True}, qu'affiche afficher() si elle liste les ports ouverts ?
Voir la réponse

Réponse : les ports 22 et 443, ex. Ouverts : [22, 443].
Explication : elle filtre le dict sur les valeurs True : [p for p, ouvert in resultats.items() if ouvert].

Exercice 12.1

À faire : ajoutez à scanner() un paramètre ports_bien_connus = [21, 22, 23, 25, 53, 80, 443, 3389] par défaut et une fonction afficher(resultats) qui liste les ports ouverts.
12.2 Mini-projet 2 : pipeline de rapport complet

Le projet final combine les modules 5, 8 et 11 : lire un CSV d'événements, filtrer par regex, compter avec Counter, croiser des IOC, écrire un rapport texte + CSV. Chaque étape est une fonction testable :

import csv
from collections import Counter

def charger(csv_path):
    with open(csv_path, encoding="utf-8", newline="") as f:
        return list(csv.DictReader(f))

def top_sources(lignes, n=5):
    return Counter(l["src"] for l in lignes).most_common(n)

def exporter_csv(top, out="top_sources.csv"):
    with open(out, "w", encoding="utf-8", newline="") as f:
        w = csv.writer(f)
        w.writerow(["ip", "tentatives"])
        w.writerows(top)

lignes = charger("evenements.csv")
top = top_sources(lignes)
exporter_csv(top)
print("Top :", top)
Pourquoi c'est important : Charger un CSV, compter, croiser, exporter : isolément ce sont des exercices, assemblés ce sont un projet montrable en entretien. Chaque étape en fonction testable, c'est exactement ce qu'un recruteur veut voir sur GitHub.
L'analogie qui aide : C'est une mini-usine en trois postes : le poste 1 décharge le camion (charger lit le CSV), le poste 2 trie et compte (top_sources avec Counter), le poste 3 emballe (exporter_csv écrit le résultat).

Présentez ce projet avec un README (objectif, installation, exemple d'exécution) : c'est exactement ce qu'un recruteur ou un jury veut voir.

Démonstration pas à pas : Trace : charger("evenements.csv") → liste de dicts, ex. 100 lignes.
top_sources(lignes, 5)Counter(l["src"] for l in lignes).most_common(5) → ex. [(« 1.1.1.1 », 42), ...].
exporter_csv(top) → écrit en-tête ip,tentatives + une ligne par tuple. État affiché : « Top : [...] ».
Pièges classiques : Tout charger avec list(DictReader) sur un fichier géant sature la RAM : traite en streaming quand c'est massif. Écrire avec writerows sans en-tête : le CSV est inexploitable. Fonctions jamais testées seules : l'erreur se cache dans l'assemblage. Chemins de fichiers en dur au lieu de paramètres.
À vous de jouer : Avec top = [("a", 42), ("b", 3)], que renvoie filtrer_seuil(top, seuil=10) qui ne garde que les IP au-dessus du seuil ?
Voir la réponse

Réponse : [('a', 42)].
Explication : 42 > 10 est gardé, 3 est écarté. La compréhension est [(ip, n) for ip, n in top if n > seuil].

Exercice 12.2

À faire : ajoutez une fonction filtrer_seuil(top, seuil=10) qui ne garde que les IP dépassant le seuil, et intégrez-la avant l'export.
12.3 Qualité : relire, tester et documenter
  • Nommage : fonctions en verbes (extraire_ip), constantes en MAJUSCULES (SEUIL = 10).
  • Fonctions courtes à tâche unique, avec docstring d'une ligne.
  • Gestion d'erreurs : try/except ciblés + messages de log explicites.
  • Secrets : jamais en dur — variables d'environnement ou fichiers exclus du Git.
  • Tests : vérifiez chaque fonction avec 2-3 exemples avant d'assembler le script.
  • Style : python -m pyflakes ou flake8 pour traquer les erreurs et le style.

Checklist avant mise en production : testé en lab, sauvegardes prévues, logs horodatés, plan de retour arrière, revue par un pair.

Pourquoi c'est important : Un script jetable devient un outil d'équipe quand il est nommé clairement, testé sur 2-3 exemples, sans secret en dur et vérifié au linter. Cette checklist, c'est la différence entre bidouille et production.
L'analogie qui aide : C'est le contrôle technique avant l'autoroute : niveaux (nommage), freins (erreurs gérées), papiers (docstring, README), et surtout pas de passager clandestin (le token en dur). Sans lui, pas de mise en circulation.
Démonstration pas à pas : Application à « rapport_soc.py » :
1. Fonctions en verbes (charger, filtrer_seuil) + constante SEUIL = 10.
2. Chaque fonction testée avec 2 exemples avant assemblage.
3. Token lu via os.environ["API_TOKEN"], jamais en dur.
4. python -m pyflakes rapport_soc.py → aucune erreur. État : script présentable et déployable en lab.
Pièges classiques : Token ou mot de passe en dur versionné dans Git : il faut le révoquer dès qu'il a fuité. Fonction de 100 lignes qui fait tout : indébuggable. Aucun test avant assemblage : on ne sait plus d'où vient l'erreur. Style jamais vérifié : les fautes simples (variable inutilisée, import mort) restent.
À vous de jouer : Où doit vivre le token d'API, et quelle commande traque les erreurs et le style avant livraison ?
Voir la réponse

Réponse : dans une variable d'environnement (ou un fichier exclu du Git), jamais en dur ; vérification avec python -m pyflakes ou flake8.
Explication : le secret reste hors du code versionné, et le linter attrape les erreurs avant la revue par un pair.

Exercice 12.3 (projet final)

À faire : assemblez un script « rapport_soc.py » complet (chargement CSV, top 5, croisement IOC, rapport texte) en réutilisant les fonctions des leçons 11.1 à 12.2, puis validez avec l'examen blanc.