Débutant · 8 chapitres · 16 schémas · 24 exercices corrigés. Un parcours progressif pour comprendre les risques numériques et construire vos premiers réflexes de protection.

À qui s’adresse ce cours ?
Ce cours s’adresse aux étudiants, enseignants et utilisateurs qui débutent en cybersécurité. Il suffit de savoir utiliser un navigateur, des fichiers et des comptes en ligne. Les deux ateliers Python sont expliqués pas à pas et utilisent uniquement la bibliothèque standard ; leur exécution est facultative pour comprendre les autres chapitres.
Ce que vous apprendrez
Vous apprendrez à décrire un risque, analyser un message suspect, comprendre un échange Web, limiter les droits, distinguer les outils cryptographiques, vérifier une sauvegarde et organiser un signalement. Un projet final vous fera relier ces notions dans un plan de sécurité cohérent.
Une méthode pour travailler
- Lisez chaque chapitre dans l’ordre et observez les schémas avec leurs légendes.
- Reformulez les notions avec les exemples de GeniusCampus, notre centre de formation fictif.
- Répondez aux trois exercices avant de déplier leurs corrigés.
- Reprenez vos réponses et justifiez les mesures par un besoin et une preuve.
- Terminez par le projet du chapitre 8, puis utilisez le quiz pour réviser.
Les ateliers utilisent vos propres fichiers ou des données fictives. Les tests sur un système tiers exigent un périmètre explicitement autorisé. Aucune infrastructure d’attaque n’est nécessaire pour suivre ce cours.
Pour pratiquer sur PandaCodeur
Cybersécurité pratique : huit laboratoires encadrés
Prolongez chaque chapitre par un laboratoire, un schéma et trois exercices corrigés supplémentaires. Le fil conducteur est le portail fictif NKILItech : comptes, dossiers, sauvegardes et traces d’activité.
- Installer le laboratoire et définir le périmètre : 45 à 60 minutes
- Construire un modèle de menaces et examiner un message suspect : 40 minutes
- Observer HTTP et les frontières de confiance avec WebGoat : 35 à 45 minutes
- Tester les autorisations avec deux identités : 45 minutes
- Vérifier une empreinte et comprendre HMAC : 30 minutes
- Restaurer une sauvegarde et vérifier le service rendu : 35 minutes
- Corriger une injection SQL et traiter une sortie HTML : 60 minutes
- Qualifier une alerte et conduire le projet de défense : 60 à 90 minutes
Préparer les exercices hors ligne
Installer Python 3.10 ou ultérieur avec sqlite3, extraire l’archive dans un dossier de travail puis exécuter la commande ci-dessous. Les six ateliers ne réalisent aucune connexion réseau. La restauration utilise uniquement ses propres fichiers temporaires. Les données sont fictives.
python atelier.py --atelier tousTélécharger le kit complet de cybersécurité pratique
Le parcours WebGoat
L’installation locale est détaillée au chapitre 1. Les chapitres 3, 4 et 7 proposent ensuite des parcours guidés sur HTTP, les accès aux objets et SQL. WebGoat est volontairement vulnérable : réserver une machine de laboratoire isolée, des comptes fictifs et une publication sur 127.0.0.1. Aucun test sur le site PandaCodeur ou sur un système tiers n’est nécessaire.
Consulter le programme Python complet
"""Laboratoires NKILItech, Python 3.10+, bibliothèque standard, aucune connexion réseau.
python atelier.py --atelier tous
Jeux de données fictifs ; le code illustre des contrôles et n'est pas un serveur de production.
"""
import argparse,base64,hashlib,hmac,html,json,sqlite3,tempfile,shutil
from pathlib import Path
from collections import Counter
def autorisation():
dossiers={101:{'proprietaire':'amina','texte':'Dossier A'},202:{'proprietaire':'boris','texte':'Dossier B'}}
def lire(utilisateur, identifiant):
dossier=dossiers.get(identifiant)
if utilisateur is None or dossier is None or dossier['proprietaire']!=utilisateur:return None
return dossier['texte']
# La session est simulée ici. En production, l'identité provient d'une session vérifiée côté serveur.
scenarios=[('amina',101,'Dossier A'),('amina',202,None),('boris',202,'Dossier B'),(None,101,None),('amina',999,None)]
for user,ident,expected in scenarios:assert lire(user,ident)==expected
return {'tests':5,'propre_dossier':'autorisé','dossier_autrui':'refusé','anonyme':'refusé','inexistant':'refusé'}
def integrite():
original=b'dossier=101;montant=15000';modifie=b'dossier=101;montant=95000'
cle=b'cle-publique-de-demonstration-ne-pas-utiliser-en-production'
empreinte=hashlib.sha256(original).hexdigest()
signature=hmac.new(cle,original,hashlib.sha256).digest()
def verifier(message,tag):return hmac.compare_digest(hmac.new(cle,message,hashlib.sha256).digest(),tag)
assert empreinte!=hashlib.sha256(modifie).hexdigest()
assert verifier(original,signature) and not verifier(modifie,signature)
# Un acteur qui possède la clé peut produire un nouveau tag : ce n'est pas une signature asymétrique.
assert verifier(modifie,hmac.new(cle,modifie,hashlib.sha256).digest())
encoded=base64.b64encode(original);assert base64.b64decode(encoded)==original
return {'sha256_original':empreinte,'modification_detectee':True,'hmac_original_valide':True,'hmac_modifie_valide':False,'base64_reversible':True}
def restauration():
# Le dossier temporaire est créé sous le dossier courant ; aucun fichier utilisateur n'est écrasé.
with tempfile.TemporaryDirectory(prefix='nkilitech-lab-',dir=Path.cwd()) as folder:
root=Path(folder);source=root/'source.json';backup=root/'copie.json';restored=root/'restaure.json'
contenu={'dossiers':[{'id':101,'statut':'valide'},{'id':202,'statut':'attente'}]}
source.write_text(json.dumps(contenu),encoding='utf-8');shutil.copy2(source,backup)
reference=hashlib.sha256(source.read_bytes()).hexdigest()
source.write_text('{incomplet',encoding='utf-8')
shutil.copy2(backup,restored)
assert hashlib.sha256(restored.read_bytes()).hexdigest()==reference
assert json.loads(restored.read_text(encoding='utf-8'))==contenu
assert len(json.loads(restored.read_text(encoding='utf-8'))['dossiers'])==2
return {'empreinte_identique':True,'json_lisible':True,'dossiers_restaures':2,'fichiers_utilisateur_modifies':False}
def injection():
db=sqlite3.connect(':memory:')
try:
db.execute('CREATE TABLE dossiers(id INTEGER, proprietaire TEXT)')
db.executemany('INSERT INTO dossiers VALUES (?, ?)',[(101,'amina'),(202,'boris')])
saisie="amina' OR 1=1 --"
# Anti-exemple local : concaténer transforme la saisie en syntaxe SQL.
fragile=db.execute("SELECT id FROM dossiers WHERE proprietaire = '"+saisie+"'").fetchall()
corrige=db.execute('SELECT id FROM dossiers WHERE proprietaire = ?',(saisie,)).fetchall()
normal=db.execute('SELECT id FROM dossiers WHERE proprietaire = ?',('amina',)).fetchall()
assert len(fragile)==2 and corrige==[] and normal==[(101,)]
# Contrôle distinct : la requête autorisée tire son propriétaire d'une identité de confiance.
identite_session='amina'
interdit=db.execute('SELECT id FROM dossiers WHERE id = ? AND proprietaire = ?',(202,identite_session)).fetchall()
assert interdit==[]
return {'anti_exemple_lignes':len(fragile),'requete_parametree_lignes':len(corrige),'recherche_normale':[r[0] for r in normal],'acces_autrui_lignes':len(interdit)}
finally:db.close()
def sortie_html():
saisie='<b>Atelier NKILItech</b>'
resultat=html.escape(saisie)
assert resultat=='<b>Atelier NKILItech</b>'
return {'texte_non_fiable':saisie,'texte_encode_pour_html':resultat,'limite':'Encodage de texte HTML, pas une protection universelle pour URL ou JavaScript.'}
JOURNAL=[
{'heure':'2026-10-29T08:00:00Z','type':'connexion_refusee','compte':'amina','ip':'192.0.2.10'},
{'heure':'2026-10-29T08:01:00Z','type':'connexion_refusee','compte':'amina','ip':'192.0.2.10'},
{'heure':'2026-10-29T08:02:00Z','type':'connexion_refusee','compte':'amina','ip':'192.0.2.10'},
{'heure':'2026-10-29T08:03:00Z','type':'connexion_reussie','compte':'amina','ip':'192.0.2.10'},
{'heure':'2026-10-29T08:04:00Z','type':'acces_refuse','compte':'amina','ressource':202,'ip':'192.0.2.10'},
{'heure':'2026-10-29T08:05:00Z','type':'lecture_autorisee','compte':'boris','ressource':202,'ip':'198.51.100.20'}]
def incidents():
# Jeu fixe de six minutes, pas une règle de détection de production sans fenêtre temporelle.
echecs=Counter(e['compte'] for e in JOURNAL if e['type']=='connexion_refusee')
candidats=sorted(k for k,n in echecs.items() if n>=3)
assert candidats==['amina']
assert [e['heure'] for e in JOURNAL]==sorted(e['heure'] for e in JOURNAL)
return {'echecs_par_compte':dict(echecs),'comptes_a_examiner':candidats,'evenements':len(JOURNAL),'conclusion':'Signal à qualifier ; compromission et exfiltration non démontrées.'}
ATELIERS={'acces':autorisation,'integrite':integrite,'restauration':restauration,'sql':injection,'sortie-html':sortie_html,'incidents':incidents}
if __name__=='__main__':
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--atelier',choices=['tous',*ATELIERS],default='tous');args=parser.parse_args()
selection=ATELIERS if args.atelier=='tous' else {args.atelier:ATELIERS[args.atelier]}
print(json.dumps({nom:fonction() for nom,fonction in selection.items()},ensure_ascii=False,indent=2))
Livrable final
Rendre le modèle de menaces, la matrice des accès, les preuves de correction, le procès-verbal de restauration et une chronologie d’incident. Le modèle de rapport fourni aide à distinguer faits, hypothèses, limites et décision de clôture.