Skip to content

Hackathon Groupe 8 - Red Worm - IOC Dashboard#12

Open
MokashSahi wants to merge 1 commit intothalesgroup-cert:masterfrom
MokashSahi:master
Open

Hackathon Groupe 8 - Red Worm - IOC Dashboard#12
MokashSahi wants to merge 1 commit intothalesgroup-cert:masterfrom
MokashSahi:master

Conversation

@MokashSahi
Copy link
Copy Markdown

Hackaton Lyceén Jacquard "Groupe 8 Red Worm" IOC Dashboard for vt_tool

Copilot AI review requested due to automatic review settings March 25, 2026 09:22
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread IOC_Dashboard/app.py
Comment on lines +293 to +303
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]}")
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Suggested change
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]}")

Copilot uses AI. Check for mistakes.
Comment thread IOC_Dashboard/app.py
Comment on lines +39 to +62
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")
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +96
<a href="{{ ioc.link }}" target="_blank" class="vt-link">
Voir sur VT ↗
</a>
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Les liens avec target="_blank" devraient ajouter rel="noopener noreferrer" pour éviter le reverse-tabnabbing.

Copilot uses AI. Check for mistakes.
<span class="badge badge-unknown">{{ event.nb_attributes }}</span>
</td>
<td class="col-link">
<a href="{{ event.misp_link }}" target="_blank" class="vt-link">
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Les liens avec target="_blank" devraient ajouter rel="noopener noreferrer" pour éviter le reverse-tabnabbing.

Suggested change
<a href="{{ event.misp_link }}" target="_blank" class="vt-link">
<a href="{{ event.misp_link }}" target="_blank" rel="noopener noreferrer" class="vt-link">

Copilot uses AI. Check for mistakes.
Comment thread IOC_Dashboard/app.py
Comment on lines +14 to +45
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")

Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread IOC_Dashboard/app.py
Comment on lines +273 to +279
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)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread IOC_Dashboard/app.py
Comment on lines +36 to +37
app = Flask(__name__)
app.secret_key = os.urandom(24)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment thread IOC_Dashboard/app.py
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)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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)

Copilot uses AI. Check for mistakes.
<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">
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Les liens avec target="_blank" devraient ajouter rel="noopener noreferrer" pour éviter le reverse-tabnabbing (la page ouverte peut modifier window.opener).

Suggested change
target="_blank" class="misp-event-link">
target="_blank" rel="noopener noreferrer" class="misp-event-link">

Copilot uses AI. Check for mistakes.
Comment thread IOC_Dashboard/back.py
Comment on lines +1 to +23
"""
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)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants