Hackathon Groupe 8 - Red Worm - IOC Dashboard#12
Hackathon Groupe 8 - Red Worm - IOC Dashboard#12MokashSahi wants to merge 1 commit intothalesgroup-cert:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a Flask-based IOC Dashboard UI and backend pipeline to upload incident reports, run vt_tools.py, persist extracted IPs into MISP (as the source of truth), and display results/history via a dark-themed dashboard.
Changes:
- Added Flask backend (
IOC_Dashboard/app.py) to orchestrate upload → VT analysis → MISP event creation → dashboard rendering. - Added Jinja templates + CSS for upload, results, and history pages.
- Updated Dockerfile to run the dashboard containerized on port 5000.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| IOC_Dashboard/templates/index.html | Upload page + flash-message UI and minimal client-side filename display. |
| IOC_Dashboard/templates/results.html | Results view with KPI cards, MISP event link, and per-IP table. |
| IOC_Dashboard/templates/history.html | Lists MISP events with links back to MISP. |
| IOC_Dashboard/static/style.css | Dark SOC-themed styling for all pages (header, cards, tables). |
| IOC_Dashboard/app.py | Flask app wiring: env loading, MISP integration, CSV parsing, subprocess execution, KPI computation. |
| IOC_Dashboard/back.py | Additional/duplicate backend implementation (appears unused). |
| IOC_Dashboard/README.md | Run instructions and quick stack description. |
| Dockerfile | Container build/run configuration for the dashboard. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| cwd=VT_TOOL_DIR, env=env, check=True, timeout=120, capture_output=True, | ||
| ) | ||
| log.info(f"analyze() → vt_tools.py stdout: {proc.stdout.decode()[:500]}") | ||
| except subprocess.TimeoutExpired: | ||
| log.error("analyze() → vt_tools.py TIMEOUT after 120s") | ||
| flash("vt_tools.py a dépassé le délai (120s). Vérifiez votre connexion.", "error") | ||
| return redirect(url_for("index")) | ||
| except subprocess.CalledProcessError as exc: | ||
| log.error(f"analyze() → vt_tools.py FAILED (code {exc.returncode})") | ||
| log.error(f"analyze() → stderr: {exc.stderr.decode()[:500]}") | ||
| log.error(f"analyze() → stdout: {exc.stdout.decode()[:500]}") |
There was a problem hiding this comment.
Les logs décodent stdout/stderr via .decode() sans gestion d’erreur. Si vt_tools.py renvoie des octets non UTF‑8, cela peut lever UnicodeDecodeError et casser la requête. Utiliser text=True dans subprocess.run(...) ou décoder avec errors="replace"/errors="ignore".
| cwd=VT_TOOL_DIR, env=env, check=True, timeout=120, capture_output=True, | |
| ) | |
| log.info(f"analyze() → vt_tools.py stdout: {proc.stdout.decode()[:500]}") | |
| except subprocess.TimeoutExpired: | |
| log.error("analyze() → vt_tools.py TIMEOUT after 120s") | |
| flash("vt_tools.py a dépassé le délai (120s). Vérifiez votre connexion.", "error") | |
| return redirect(url_for("index")) | |
| except subprocess.CalledProcessError as exc: | |
| log.error(f"analyze() → vt_tools.py FAILED (code {exc.returncode})") | |
| log.error(f"analyze() → stderr: {exc.stderr.decode()[:500]}") | |
| log.error(f"analyze() → stdout: {exc.stdout.decode()[:500]}") | |
| cwd=VT_TOOL_DIR, | |
| env=env, | |
| check=True, | |
| timeout=120, | |
| capture_output=True, | |
| text=True, | |
| errors="replace", | |
| ) | |
| log.info(f"analyze() → vt_tools.py stdout: {proc.stdout[:500]}") | |
| except subprocess.TimeoutExpired: | |
| log.error("analyze() → vt_tools.py TIMEOUT after 120s") | |
| flash("vt_tools.py a dépassé le délai (120s). Vérifiez votre connexion.", "error") | |
| return redirect(url_for("index")) | |
| except subprocess.CalledProcessError as exc: | |
| log.error(f"analyze() → vt_tools.py FAILED (code {exc.returncode})") | |
| log.error(f"analyze() → stderr: {exc.stderr[:500]}") | |
| log.error(f"analyze() → stdout: {exc.stdout[:500]}") |
| MISPURL = os.environ.get("MISPURL", "https://localhost") | ||
| MISPKEY = os.environ.get("MISPKEY", "") | ||
| MISPSSLVERIFY = os.environ.get("MISPSSLVERIFY", "False").lower() not in ("true", "1") | ||
| VT_TOOL_DIR = os.path.expanduser(os.environ.get("VT_TOOL_DIR", "~/p/vt_tool")) | ||
| RESULTS_DIR = os.path.join(VT_TOOL_DIR, "Results") | ||
| UPLOAD_DIR = os.path.join(VT_TOOL_DIR, "Up") | ||
|
|
||
|
|
||
| log.info(f"VT_TOOL_DIR : {VT_TOOL_DIR}") | ||
| log.info(f"RESULTS_DIR : {RESULTS_DIR}") | ||
| log.info(f"MISPURL : {MISPURL}") | ||
| log.info(f"MISPKEY : {'SET (' + MISPKEY[:6] + '...)' if MISPKEY else '⚠ NOT SET'}") | ||
| log.info(f"MISPSSLVERIFY : {MISPSSLVERIFY}") | ||
|
|
||
|
|
||
| def get_misp(): | ||
| """Instancie PyMISP. Retourne None si MISP indisponible ou clé absente.""" | ||
| if not MISPKEY: | ||
| log.error("get_misp() → MISPKEY is empty — check your .env file") | ||
| return None | ||
| try: | ||
| log.debug(f"get_misp() → connecting to {MISPURL} (ssl={not MISPSSLVERIFY})") | ||
| misp = PyMISP(MISPURL, MISPKEY, ssl=not MISPSSLVERIFY) | ||
| log.info("get_misp() → connection OK") |
There was a problem hiding this comment.
MISPSSLVERIFY est calculé avec une négation (... not in ("true","1")) puis ré-inversé dans ssl=not MISPSSLVERIFY. Le comportement final peut être correct, mais le nom/valeur loggée devient trompeuse (ex: MISPSSLVERIFY=True alors que ssl=False). Simplifier en un booléen “verify” direct (ex: verify_ssl = env in ("true","1")) et le passer tel quel à PyMISP pour éviter les erreurs de configuration.
| <a href="{{ ioc.link }}" target="_blank" class="vt-link"> | ||
| Voir sur VT ↗ | ||
| </a> |
There was a problem hiding this comment.
Les liens avec target="_blank" devraient ajouter rel="noopener noreferrer" pour éviter le reverse-tabnabbing.
| <span class="badge badge-unknown">{{ event.nb_attributes }}</span> | ||
| </td> | ||
| <td class="col-link"> | ||
| <a href="{{ event.misp_link }}" target="_blank" class="vt-link"> |
There was a problem hiding this comment.
Les liens avec target="_blank" devraient ajouter rel="noopener noreferrer" pour éviter le reverse-tabnabbing.
| <a href="{{ event.misp_link }}" target="_blank" class="vt-link"> | |
| <a href="{{ event.misp_link }}" target="_blank" rel="noopener noreferrer" class="vt-link"> |
| from flask import Flask, request, render_template, redirect, url_for, flash | ||
| from pymisp import PyMISP, MISPEvent, MISPAttribute | ||
|
|
||
| # ───────────────────────────────────────────── | ||
| # LOGGING — visible dans le terminal Docker | ||
| # ───────────────────────────────────────────── | ||
|
|
||
| logging.basicConfig( | ||
| level=logging.DEBUG, | ||
| format="[%(levelname)s] %(message)s" | ||
| ) | ||
| log = logging.getLogger(__name__) | ||
|
|
||
| # ───────────────────────────────────────────── | ||
| # BLOC A — Configuration & connexion MISP | ||
| # ───────────────────────────────────────────── | ||
|
|
||
| _vt_tool_dir = os.environ.get("VT_TOOL_DIR", os.path.expanduser("~/p/vt_tool")) | ||
| _env_path = os.path.join(_vt_tool_dir, ".env") | ||
| log.info(f"Loading .env from : {_env_path}") | ||
| load_dotenv(dotenv_path=_env_path) | ||
|
|
||
| app = Flask(__name__) | ||
| app.secret_key = os.urandom(24) | ||
|
|
||
| MISPURL = os.environ.get("MISPURL", "https://localhost") | ||
| MISPKEY = os.environ.get("MISPKEY", "") | ||
| MISPSSLVERIFY = os.environ.get("MISPSSLVERIFY", "False").lower() not in ("true", "1") | ||
| VT_TOOL_DIR = os.path.expanduser(os.environ.get("VT_TOOL_DIR", "~/p/vt_tool")) | ||
| RESULTS_DIR = os.path.join(VT_TOOL_DIR, "Results") | ||
| UPLOAD_DIR = os.path.join(VT_TOOL_DIR, "Up") | ||
|
|
There was a problem hiding this comment.
MISPAttribute est importé mais n’est pas utilisé dans ce module (idem UPLOAD_DIR). Supprimer ces éléments non utilisés pour éviter la confusion et faciliter le linting.
| filename = uploaded.filename | ||
| report_path = os.path.join(VT_TOOL_DIR, filename) | ||
|
|
||
| log.info(f"analyze() → saving uploaded file to: {report_path}") | ||
| os.makedirs(VT_TOOL_DIR, exist_ok=True) | ||
| os.makedirs(RESULTS_DIR, exist_ok=True) | ||
| uploaded.save(report_path) |
There was a problem hiding this comment.
uploaded.filename est utilisé directement pour construire report_path puis sauvegarder sur disque. Un nom de fichier contenant des séquences de type ../ peut provoquer une traversée de répertoires et écrire en dehors de VT_TOOL_DIR. Utiliser werkzeug.utils.secure_filename() et/ou forcer l’écriture dans un répertoire dédié (ex: UPLOAD_DIR) en vérifiant que le chemin final reste bien sous ce répertoire.
| app = Flask(__name__) | ||
| app.secret_key = os.urandom(24) |
There was a problem hiding this comment.
app.secret_key = os.urandom(24) change à chaque redémarrage : les sessions/flash existants deviennent invalides et en mode multi-process (gunicorn) chaque worker aura une clé différente. Charger une clé stable via variable d’environnement (ex: SECRET_KEY) ou un fichier de config.
| app = Flask(__name__) | |
| app.secret_key = os.urandom(24) | |
| # Flask application & secret key | |
| SECRET_KEY = os.environ.get("SECRET_KEY") | |
| if not SECRET_KEY: | |
| log.warning( | |
| "SECRET_KEY is not set in environment/.env — using insecure " | |
| "development key. Configure SECRET_KEY for production." | |
| ) | |
| SECRET_KEY = "dev-insecure-secret-key-change-me" | |
| app = Flask(__name__) | |
| app.secret_key = SECRET_KEY |
| log.info(f"MISP URL : {MISPURL}") | ||
| log.info(f"MISP KEY : {'OK (' + MISPKEY[:6] + '...)' if MISPKEY else '⚠ NOT SET'}") | ||
| log.info("=" * 50) | ||
| app.run(host="0.0.0.0", port=5000, debug=True) |
There was a problem hiding this comment.
Le serveur est lancé avec debug=True. En environnement déployé/Docker cela expose des informations sensibles via les pages d’erreur et peut activer le reloader. Mettre debug=False par défaut et le piloter via une variable d’environnement (ex: FLASK_DEBUG).
| app.run(host="0.0.0.0", port=5000, debug=True) | |
| debug_mode = os.getenv("FLASK_DEBUG", "").lower() in ("1", "true", "yes") | |
| app.run(host="0.0.0.0", port=5000, debug=debug_mode) |
| <section class="misp-link-section"> | ||
| <span class="misp-label">Source de vérité :</span> | ||
| <a href="{{ misp_url }}/events/view/{{ event_info.id }}" | ||
| target="_blank" class="misp-event-link"> |
There was a problem hiding this comment.
Les liens avec target="_blank" devraient ajouter rel="noopener noreferrer" pour éviter le reverse-tabnabbing (la page ouverte peut modifier window.opener).
| target="_blank" class="misp-event-link"> | |
| target="_blank" rel="noopener noreferrer" class="misp-event-link"> |
| """ | ||
| IOC Dashboard SOC — app.py | ||
| Pipeline : upload .txt → vt_tools.py → CSV IP → MISP → dashboard | ||
| Source de vérité : MISP uniquement (jamais les CSV directement) | ||
| """ | ||
|
|
||
| import os | ||
| import re | ||
| import csv | ||
| import glob | ||
| import subprocess | ||
| from dotenv import load_dotenv | ||
| from flask import Flask, request, render_template, redirect, url_for, flash | ||
| from pymisp import PyMISP, MISPEvent, MISPAttribute | ||
|
|
||
| # ───────────────────────────────────────────── | ||
| # BLOC A — Configuration & connexion MISP | ||
| # ───────────────────────────────────────────── | ||
|
|
||
| load_dotenv(dotenv_path='../vt_tool/.env') | ||
|
|
||
| app = Flask(__name__) | ||
| app.secret_key = os.urandom(24) |
There was a problem hiding this comment.
Ce fichier semble être un doublon de app.py (mêmes routes/pipeline) et n’est référencé nulle part dans le dépôt. De plus la docstring indique “app.py” et load_dotenv('../vt_tool/.env') dépend du répertoire courant. Pour réduire la confusion/maintenance, supprimer back.py ou expliquer clairement son usage et aligner le chargement de config sur app.py.
Hackaton Lyceén Jacquard "Groupe 8 Red Worm" IOC Dashboard for vt_tool