| Fonction | Entrée | Sortie |
|---|---|---|
count_failures(lines) | la liste des lignes du journal | un dictionnaire {IP: nombre d'échecs} |
detect_brute_force(lines, threshold=5) | les lignes et un seuil | une liste d'IP |
print_report(lines, threshold=5) | les lignes et un seuil | rien : elle affiche une alerte par IP |
Oct 7 10:02:11 srv-web sshd[2210]: Failed password for root from 203.0.113.45 port 51122 ssh2
Oct 7 10:02:18 srv-web sshd[2215]: Failed password for invalid user admin from 203.0.113.45 port 51141 ssh2
>>> line = AUTH_LOG[3]
>>> words = line.split()
>>> words.index("from")
9
>>> words[words.index("from") + 1]
'203.0.113.45'
split() découpe la ligne en mots, quel que soit le nombre d'espacesfrom. On cherche donc la position de from puis on prend le mot suivant
extraire l'IP d'une ligne :
découper la ligne en mots, renvoyer le mot qui suit « from »
compter les échecs :
créer un dictionnaire vide
pour chaque ligne qui contient « Failed password » :
extraire l'IP puis ajouter 1 à son compteur
renvoyer le dictionnaire
détecter le brute force :
pour chaque IP et son nombre d'échecs : si le nombre atteint le seuil, garder l'IP
afficher le rapport :
pour chaque IP détectée : afficher une alerte avec son nombre d'échecs
from log_data import AUTH_LOG
def extract_ip(line):
"""Renvoie l'IP qui suit le mot « from »."""
words = line.split()
return words[words.index("from") + 1]
print(extract_ip(AUTH_LOG[3])) # 203.0.113.45
print(extract_ip(AUTH_LOG[5])) # 203.0.113.45
log_data.py et importé. Le fichier du défi reste lisible
def count_failures(lines):
"""Compte les échecs par adresse IP."""
failures = {}
for line in lines:
if "Failed password" in line:
ip = extract_ip(line)
failures[ip] = failures.get(ip, 0) + 1
return failures
"Failed password" in line garde seulement les échecs. Les lignes Accepted ou Connection closed sont ignoréesfailures.get(ip, 0) renvoie le compteur actuel ou 0 si l'IP est nouvellereturn est après la boucle, une fois toutes les lignes lues
def detect_brute_force(lines, threshold=5):
"""Renvoie les IP ayant au moins threshold échecs."""
suspects = []
for ip, count in count_failures(lines).items():
if count >= threshold:
suspects.append(ip)
return suspects
count_failures() au lieu de recompteritems() donne chaque paire clé et valeur : l'IP et son nombre d'échecs>= et non > : l'énoncé dit « au moins » threshold échecs
def print_report(lines, threshold=5):
"""Affiche une alerte par IP détectée."""
failures = count_failures(lines)
for ip in detect_brute_force(lines, threshold):
message = f"{failures[ip]} échecs d'authentification"
print(f"[ALERTE] {ip} : {message}")
detect_brute_force(). Sinon, print_report(lines, 2) utiliserait toujours 5
print(count_failures(AUTH_LOG))
# {'192.0.2.25': 1, '203.0.113.45': 7, '198.51.100.23': 5, '198.51.100.77': 2}
print(detect_brute_force(AUTH_LOG))
# ['203.0.113.45', '198.51.100.23']
print(detect_brute_force(AUTH_LOG, threshold=2))
# ['203.0.113.45', '198.51.100.23', '198.51.100.77']
print_report(AUTH_LOG)
# [ALERTE] 203.0.113.45 : 7 échecs d'authentification
# [ALERTE] 198.51.100.23 : 5 échecs d'authentification
Si un résultat diffère, on relance la fonction concernée seule. Comme chaque fonction est courte, l'erreur se trouve vite.
| Piège | Correction |
|---|---|
l'IP prise à une position fixe (words[9]) | chercher la position de from avec index() |
failures[ip] += 1 sur une IP nouvelle | KeyError. Utiliser get(ip, 0) |
count > threshold | l'énoncé dit « au moins ». Utiliser >= |
| tout le code dans une seule fonction | une fonction par tâche, qui réutilise les autres |
le seuil oublié dans print_report() | le transmettre à detect_brute_force() |