From dc5bb7c705b32c6c2aecc192b13bc0defd2f7aad Mon Sep 17 00:00:00 2001 From: Tatsuya28 Date: Sat, 12 Apr 2025 13:13:35 -0400 Subject: [PATCH 01/18] Fix CVE + Add marked and primevue dependencies to package.json --- frontend/package-lock.json | 95 ++++++++++++++++++++++++++++++++++++-- frontend/package.json | 2 + 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5ad73a9..5f7fe1d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,9 +16,11 @@ "chart.js": "^4.4.8", "express-validator": "^7.2.1", "jsonwebtoken": "^9.0.2", + "marked": "^15.0.8", "pinia": "^3.0.1", "postcss": "^8.5.3", "primeicons": "^7.0.0", + "primevue": "^4.3.3", "tailwindcss": "^4.0.12", "vue": "^3.5.13", "vue-router": "^4.5.0", @@ -1304,6 +1306,65 @@ "dev": true, "license": "MIT" }, + "node_modules/@primeuix/styled": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.5.1.tgz", + "integrity": "sha512-5Ftw/KSauDPClQ8F2qCyCUF7cIUEY4yLNikf0rKV7Vsb8zGYNK0dahQe7CChaR6M2Kn+NA2DSBSk76ZXqj6Uog==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.5.3" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primeuix/styles": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-1.0.3.tgz", + "integrity": "sha512-yHj/Q+fosJ1736Ty5lRbpqhKa9piou+xZPPppNHUDshq0+XhrFwDGggvPGmDAJyUIM+ChM/Nj8lPY/AwTNXAkg==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.5.1" + } + }, + "node_modules/@primeuix/utils": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.5.3.tgz", + "integrity": "sha512-7SGh7734wcF1/uK6RzO6Z6CBjGQ97GDHfpyl2F1G/c7R0z9hkT/V72ypDo82AWcCS7Ta07oIjDpOCTkSVZuEGQ==", + "license": "MIT", + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/core": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.3.3.tgz", + "integrity": "sha512-kSkN5oourG7eueoFPIqiNX3oDT/f0I5IRK3uOY/ytz+VzTZp5yuaCN0Nt42ZQpVXjDxMxDvUhIdaXVrjr58NhQ==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.5.0", + "@primeuix/utils": "^0.5.1" + }, + "engines": { + "node": ">=12.11.0" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@primevue/icons": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.3.3.tgz", + "integrity": "sha512-ouQaxHyeFB6MSfEGGbjaK5Qv9efS1xZGetZoU5jcPm090MSYLFtroP1CuK3lZZAQals06TZ6T6qcoNukSHpK5w==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.5.1", + "@primevue/core": "4.3.3" + }, + "engines": { + "node": ">=12.11.0" + } + }, "node_modules/@rollup/pluginutils": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", @@ -4163,6 +4224,18 @@ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "license": "MIT" }, + "node_modules/marked": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.8.tgz", + "integrity": "sha512-rli4l2LyZqpQuRve5C0rkn6pj3hT8EWPC+zkAxFTAJLxRbENfTAhEQq9itrmf1Y81QtAX5D/MYlGlIomNgj9lA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4806,6 +4879,22 @@ "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==", "license": "MIT" }, + "node_modules/primevue": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.3.3.tgz", + "integrity": "sha512-nooYVoEz5CdP3EhUkD6c3qTdRmpLHZh75fBynkUkl46K8y5rksHTjdSISiDijwTA5STQIOkyqLb+RM+HQ6nC1Q==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.5.0", + "@primeuix/styles": "^1.0.0", + "@primeuix/utils": "^0.5.1", + "@primevue/core": "4.3.3", + "@primevue/icons": "4.3.3" + }, + "engines": { + "node": ">=12.11.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -5342,9 +5431,9 @@ } }, "node_modules/vite": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz", - "integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==", + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", + "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", diff --git a/frontend/package.json b/frontend/package.json index e79ee3c..3ebaeb8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,9 +21,11 @@ "chart.js": "^4.4.8", "express-validator": "^7.2.1", "jsonwebtoken": "^9.0.2", + "marked": "^15.0.8", "pinia": "^3.0.1", "postcss": "^8.5.3", "primeicons": "^7.0.0", + "primevue": "^4.3.3", "tailwindcss": "^4.0.12", "vue": "^3.5.13", "vue-router": "^4.5.0", From 666a16e6b3df5cd36e9c9d8ee924fefdd2ed70c6 Mon Sep 17 00:00:00 2001 From: Tatsuya28 Date: Sat, 12 Apr 2025 13:18:06 -0400 Subject: [PATCH 02/18] Implement Mistral integration for email analysis and explanations; add MistralResponseModal component and related services --- .../src/components/MailTableComponent.vue | 75 +++++++- .../src/components/MistralResponseModal.vue | 170 ++++++++++++++++++ frontend/src/composables/useMailTable.js | 83 +++++++-- frontend/src/main.js | 6 + frontend/src/services/mistralService.js | 120 +++++++++++++ frontend/src/views/QuarantineView.vue | 36 +++- src/main.py | 61 ++++++- src/mistral_explain.py | 98 ++++++++-- 8 files changed, 610 insertions(+), 39 deletions(-) create mode 100644 frontend/src/components/MistralResponseModal.vue create mode 100644 frontend/src/services/mistralService.js diff --git a/frontend/src/components/MailTableComponent.vue b/frontend/src/components/MailTableComponent.vue index 807bcec..556320a 100644 --- a/frontend/src/components/MailTableComponent.vue +++ b/frontend/src/components/MailTableComponent.vue @@ -1,8 +1,11 @@ diff --git a/frontend/src/components/MistralResponseModal.vue b/frontend/src/components/MistralResponseModal.vue new file mode 100644 index 0000000..46f4eab --- /dev/null +++ b/frontend/src/components/MistralResponseModal.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/frontend/src/composables/useMailTable.js b/frontend/src/composables/useMailTable.js index 4d4e7e2..b3c4ebf 100644 --- a/frontend/src/composables/useMailTable.js +++ b/frontend/src/composables/useMailTable.js @@ -1,5 +1,6 @@ import { ref, computed } from 'vue'; import mailService from '@/services/mailService'; +import axios from 'axios'; export function useMailTable() { // États @@ -23,6 +24,12 @@ export function useMailTable() { dateTo: '' }); + // États pour Mistral + const mistralLoading = ref(false); + const mistralResponse = ref(null); + const mistralError = ref(null); + const mistralEmailId = ref(null); + // Computed property pour vérifier si tous les mails sont sélectionnés const allSelected = computed(() => { return filteredMails.value.length > 0 && selectedMails.value.length === filteredMails.value.length; @@ -48,24 +55,10 @@ export function useMailTable() { // Filtrer par destinataire (ID_Utilisateur ou email) if (searchQuery.value.recipient) { const searchTerm = searchQuery.value.recipient.toLowerCase(); - - // Récupérer différentes propriétés possibles pour le destinataire const userId = String(mail.ID_Utilisateur || ''); const destinataire = String(mail.Destinataire || ''); - - // Debug temporaire - voir dans la console ce qui est disponible - // console.log('Mail destinataire debug:', { - // mailId: mail.ID_Mail, - // userId: userId, - // destinataire: destinataire, - // searchTerm: searchTerm - // }); - - // Vérifier les correspondances const idMatch = userId.toLowerCase().includes(searchTerm); const emailMatch = destinataire.toLowerCase().includes(searchTerm); - - // Si ni l'ID ni l'email ne correspondent, exclure ce mail if (!idMatch && !emailMatch) { return false; } @@ -281,6 +274,58 @@ export function useMailTable() { } }; + // Méthode modifiée pour appeler directement l'API Mistral + const askMistral = async (emailId) => { + mistralLoading.value = true; + mistralError.value = null; + mistralResponse.value = null; + mistralEmailId.value = emailId; + + try { + // D'abord, récupérez toutes les données détaillées de l'email + const emailDetails = await mailService.getMailDetails(emailId); + + // Ensuite, envoyez ces données détaillées à l'API Mistral + const response = await axios.post('/analyse/mistral', { + emailId: emailId, + emailData: emailDetails // Envoi des données complètes de l'email + }, { + headers: { + 'Content-Type': 'application/json' + } + }); + + if (response.data && response.data.explanation) { + mistralResponse.value = response.data.explanation; + } else { + mistralResponse.value = "Aucune explication disponible."; + } + } catch (error) { + console.error("Erreur lors de la demande d'explication à Mistral:", error); + + mistralResponse.value = `# Problème lors de l'analyse de l'email + +Nous n'avons pas pu générer une explication satisfaisante pour cet email. + +## Causes possibles: +- Données insuffisantes pour l'analyse +- Le service d'IA n'a pas pu traiter correctement les informations +- Problème de connexion avec le service d'analyse + +Veuillez consulter les détails de l'email manuellement pour comprendre pourquoi il a été filtré.`; + + } finally { + mistralLoading.value = false; + } + }; + + const resetMistral = () => { + mistralLoading.value = false; + mistralResponse.value = null; + mistralError.value = null; + mistralEmailId.value = null; + }; + return { // État mails, @@ -306,6 +351,14 @@ export function useMailTable() { updateMailStatus, updateSearchQuery, resetSearch, - refreshWithCurrentFilters + refreshWithCurrentFilters, + + // États et méthodes pour Mistral + mistralLoading, + mistralResponse, + mistralError, + mistralEmailId, + askMistral, + resetMistral }; } diff --git a/frontend/src/main.js b/frontend/src/main.js index 01dfaa3..038d6da 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -4,6 +4,11 @@ import Toast from "vue-toastification"; // Import the CSS or use your own! import "vue-toastification/dist/index.css"; +// Import PrimeVue +import PrimeVue from 'primevue/config'; + +// Supprimez temporairement les imports CSS de PrimeVue problématiques + import { createApp } from 'vue'; import { createPinia } from 'pinia'; import App from './App.vue'; @@ -14,6 +19,7 @@ const pinia = createPinia(); app.use(pinia); app.use(router); +app.use(PrimeVue); const toastOptions = { position: "top-right", diff --git a/frontend/src/services/mistralService.js b/frontend/src/services/mistralService.js new file mode 100644 index 0000000..ef1a6f1 --- /dev/null +++ b/frontend/src/services/mistralService.js @@ -0,0 +1,120 @@ +import axios from 'axios'; + +/** + * Service pour interagir avec l'API Mistral + */ +export default { + /** + * Demande une explication à Mistral pour un email spécifique + * Cette implémentation envoie une requête directe au service Python + * @param {number|string} emailId - L'identifiant de l'email + * @param {Object} mailDetails - Les détails complets de l'email + * @returns {Promise} Promise contenant l'explication + */ + async getExplanation(emailId, mailDetails) { + try { + // Préparer les données à envoyer + const requestData = { + emailId: emailId, + // Envoyer les détails complets du mail + emailData: mailDetails + }; + + // Appel direct au service Python sans passer par /api/ + const response = await axios.post('/analyse/mistral/', requestData, { + headers: { + 'Content-Type': 'application/json' + } + }); + + // Si la réponse est réussie mais sans explication, utiliser une explication par défaut + if (response.data && !response.data.explanation) { + return { + explanation: "Aucune explication détaillée n'a été fournie par le service d'analyse.", + status: response.data.status || 'success' + }; + } + + return response.data; + } catch (error) { + console.error('Error getting Mistral explanation:', error); + + // En cas d'erreur, revenir à une explication simulée pour une meilleure UX + return { + explanation: this._generateFallbackExplanation(emailId, error, mailDetails), + status: 'fallback' + }; + } + }, + + /** + * Génère une explication de fallback en cas d'échec de l'appel à l'API + * @private + * @param {number|string} emailId - L'identifiant de l'email + * @param {Error} error - L'erreur rencontrée + * @param {Object} mailDetails - Les détails du mail si disponibles + * @returns {string} Explication au format markdown + */ + _generateFallbackExplanation(emailId, error, mailDetails) { + // Si nous avons des détails du mail, générer une explication plus pertinente + if (mailDetails) { + const subject = mailDetails.Sujet || 'Sans objet'; + const sender = mailDetails.Emetteur || 'inconnu'; + const status = mailDetails.Statut || 'UNKNOWN'; + + return `# Analyse de l'email + +## Informations sur l'email +- **Sujet**: ${subject} +- **Expéditeur**: ${sender} +- **Statut**: ${status} + +## Explication provisoire +Nous n'avons pas pu obtenir une analyse automatisée complète pour cet email. Voici quelques points qui pourraient expliquer son statut: + +${status === 'QUARANTINED' ? ` +- L'email pourrait contenir des éléments suspects +- L'expéditeur pourrait ne pas être confirmé +- Des problèmes de vérification SPF, DKIM ou DMARC pourraient être présents +` : ''} + +${status === 'SPAM' ? ` +- L'email a été identifié comme spam potentiel +- Il pourrait contenir des mots-clés associés au spam +- L'expéditeur pourrait être sur une liste de surveillance +` : ''} + +${status === 'DELIVERED' ? ` +- L'email a passé toutes les vérifications de sécurité +- Aucun élément suspect n'a été détecté +` : ''} + +## Message technique +\`\`\` +${error?.message || "Erreur de communication avec le service d'analyse"} +\`\`\` +`; + } + + // Fallback si pas de détails + return `# Problème de communication avec le service d'analyse + +Nous n'avons pas pu obtenir une explication automatisée pour cet email (ID: ${emailId}). + +## Raison possible + +Le service d'analyse Mistral n'est pas accessible actuellement. Cela peut être dû à: + +- Le service est en cours de maintenance +- Un problème de connectivité réseau +- Une erreur de configuration + +## Message d'erreur technique + +\`\`\` +${error?.message || "Erreur inconnue"} +\`\`\` + +Veuillez réessayer ultérieurement ou contacter l'administrateur système si le problème persiste.`; + } +}; diff --git a/frontend/src/views/QuarantineView.vue b/frontend/src/views/QuarantineView.vue index 13a7871..da863f8 100644 --- a/frontend/src/views/QuarantineView.vue +++ b/frontend/src/views/QuarantineView.vue @@ -3,11 +3,15 @@ import { ref, onMounted } from 'vue'; import { useAuthStore } from '@/stores/authStore'; import { useRouter } from 'vue-router'; import { useMailTable } from '@/composables/useMailTable'; +// Correct import for vue-toastification +import { useToast } from 'vue-toastification'; import MailTableComponent from '@/components/MailTableComponent.vue'; import FileDropZone from '@/components/FileDropZone.vue'; +import MistralResponseModal from '@/components/MistralResponseModal.vue'; const router = useRouter(); const authStore = useAuthStore(); +const toast = useToast(); // État pour contrôler la visibilité de la modal de drag & drop const isUploadModalOpen = ref(false); @@ -40,7 +44,13 @@ const { bulkUpdateStatus, updateMailStatus, updateSearchQuery, - resetSearch + resetSearch, + mistralLoading, + mistralResponse, + mistralError, + mistralEmailId, + askMistral, + resetMistral } = useMailTable(); // Fonctions spécifiques à la vue Quarantine @@ -106,6 +116,19 @@ const handleUploadError = ({ fileName, error }) => { console.error(`Error uploading ${fileName}:`, error); }; +const handleAskMistral = async (mailId) => { + try { + await askMistral(mailId); + } catch (error) { + toast.add({ + severity: 'error', + summary: 'Erreur', + detail: "Impossible d'obtenir une explication pour cet email", + life: 3000 + }); + } +}; + // Vérifier l'authentification et charger les données au montage onMounted(async () => { authStore.initialize(); @@ -167,6 +190,7 @@ onMounted(async () => { @refresh="loadQuarantineMails" @search="handleSearch" @reset-search="handleResetSearch" + @ask-mistral="handleAskMistral" > + + + diff --git a/src/main.py b/src/main.py index 0907eae..2aa29e7 100644 --- a/src/main.py +++ b/src/main.py @@ -19,13 +19,16 @@ from database import Database from analysis.mail_analyzer import load_email, analyze_email, load_raw_email -from fastapi import FastAPI, UploadFile +from fastapi import FastAPI, UploadFile, BackgroundTasks, Body from fastapi.responses import JSONResponse -from starlette.background import BackgroundTasks +from fastapi.middleware.cors import CORSMiddleware +from typing import Dict, Any import uvicorn import logging import tempfile import mistral_explain +import json +import os # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') @@ -34,6 +37,15 @@ # Initialize FastAPI app app = FastAPI() +# Configuration CORS pour permettre les requêtes du frontend +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # En production, limitez aux origines spécifiques + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + db = Database() async def process_email(filename: str, email_content: bytes): @@ -85,7 +97,50 @@ def health_check(): async def ai_answer(file_to_explain): return mistral_explain.answer(file_to_explain) - +@app.post("/analyse/mistral/") +async def generate_mistral_explanation(data: Dict[str, Any]): + """ + Endpoint pour générer une explication Mistral pour un email + + Args: + data: Données contenant l'ID de l'email et ses détails complets + """ + try: + email_id = data.get("emailId") + email_data = data.get("emailData") + + if not email_id: + return {"error": "Email ID manquant", "status": "error"} + + # Utiliser les données détaillées de l'email si disponibles + if email_data: + print(f"Données complètes reçues pour l'email ID {email_id}") + + # Générer l'explication à partir des données détaillées + explanation = mistral_explain.generate_explanation(email_data) + + return { + "explanation": explanation, + "status": "success" + } + else: + # Fallback: récupérer les données depuis la base de données + print(f"Aucune donnée détaillée reçue pour l'email ID {email_id}, récupération depuis la base de données") + email_data = {"id": email_id, "message": "Données non disponibles"} + + explanation = "Je n'ai pas reçu suffisamment d'informations pour analyser cet email. Veuillez vérifier la configuration du système." + + return { + "explanation": explanation, + "status": "limited" + } + except Exception as e: + print(f"Erreur lors de la génération de l'explication: {str(e)}") + return { + "explanation": f"Une erreur s'est produite lors de l'analyse: {str(e)}", + "status": "error" + } + if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=6969) diff --git a/src/mistral_explain.py b/src/mistral_explain.py index 519f704..7feb131 100644 --- a/src/mistral_explain.py +++ b/src/mistral_explain.py @@ -1,9 +1,10 @@ -import os import subprocess import os +import json from mistralai import Mistral from dotenv import load_dotenv +# Charger les variables d'environnement load_dotenv() def send_curl_requests(filename): @@ -16,37 +17,98 @@ def send_curl_requests(filename): ], capture_output=True, text=True) return result.stdout - -def answer(filename): +def generate_explanation(email_data): + """ + Génère une explication structurée pour un email en utilisant Mistral + + Args: + email_data (dict): Les données complètes de l'email et son analyse + + Returns: + str: L'explication générée par Mistral + """ try: model = "mistral-large-latest" api_key = os.environ["MISTRAL_API_KEY"] client = Mistral(api_key=api_key) + + # Extraire et organiser les informations importantes pour l'analyse + organized_data = { + "email": { + "subject": email_data.get("subject") or "Sans objet", + "sender": email_data.get("sender") or "Inconnu", + "receiver": email_data.get("user", {}).get("email") or "Inconnu", + "date": email_data.get("receivedDate") or "Inconnue", + "status": email_data.get("status") or "Inconnu" + }, + "analysis": { + "spf": next((a.get("result") for a in email_data.get("analyses", []) if a.get("type") == "SPF"), "Inconnu"), + "dkim": next((a.get("result") for a in email_data.get("analyses", []) if a.get("type") == "DKIM"), "Inconnu"), + "dmarc": next((a.get("result") for a in email_data.get("analyses", []) if a.get("type") == "DMARC"), "Inconnu"), + "malware_scan": next((a.get("result") for a in email_data.get("analyses", []) if a.get("type") == "CLAMAV"), "Inconnu"), + "phishing_score": next((a.get("result") for a in email_data.get("analyses", []) if a.get("type") == "AI"), "Inconnu"), + "urls": [link.get("url") for link in email_data.get("links", [])] + }#, + #"detailed_analysis": email_data.get("analysis") or email_data.get("analyse_details") or [] + } + + # Convertir les données organisées en JSON pour le prompt + analysis_json = json.dumps(organized_data, indent=2, ensure_ascii=False) + + # Construire un prompt plus riche et structuré + system_prompt = """ +Tu es un expert en sécurité informatique qui explique en français simple pourquoi un email a été filtré. +Ta mission est d'analyser les données techniques et d'expliquer de manière claire et accessible les raisons pour lesquelles l'email a été considéré comme suspect. - # Préparation des messages de contexte - context_messages = [ - {"role": "system", "content": "Tu es une IA qui explique en français pourquoi les mails n'ont pas été reçu par l'utilisateur. Ton objectif est de fournir des explications claires et compréhensibles aux utilisateurs qui ne sont pas familiers avec les concepts informatiques. Les données qui suivent indiquent les résultats de l'analyse du mail."}, - {"role": "system", "content": send_curl_requests(filename)}, - {"role": "system", "content": "Tu devras expliquer l'élément considéré comme dangereux dans le message précédent."}, - ] - - user_question = "Pourquoi n'ai-je pas reçu le mail ?" - context_messages.append({"role": "user", "content": user_question}) +Voici la structure des données que tu vas analyser: +- email: les informations de base sur l'email (expéditeur, destinataire, sujet, date) +- analysis: les résultats des différentes analyses de sécurité (SPF, DKIM, DMARC, analyse antivirus, score de phishing) +- detailed_analysis: analyses détaillées complémentaires +Règles à suivre: +1. Ton explication doit être claire et compréhensible pour des non-experts +2. Identifie les principaux problèmes de sécurité détectés +3. Explique pourquoi ces problèmes sont préoccupants +4. Si les informations sont insuffisantes, indique-le clairement +5. Utilise un format structuré avec des titres et sous-titres +6. Évite le jargon technique sauf si tu l'expliques + """ + + # Préparer les messages pour Mistral + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "system", "content": f"Voici les données de l'email à analyser: {analysis_json}"}, + {"role": "user", "content": "Pourquoi cet email a-t-il été filtré? Explique-moi les problèmes de sécurité détectés de façon claire et précise."} + ] + # Appel à l'API Mistral chat_response = client.chat.complete( model=model, - messages=context_messages -) - - # Affichage de la réponse - print(chat_response.choices[0].message.content) + messages=messages, + max_tokens=2000 + ) + + # Retourner la réponse + explanation = chat_response.choices[0].message.content + + # Si l'explication est trop générique, ajouter un avertissement + if "je n'ai pas d'informations suffisantes" in explanation.lower() or "informations manquantes" in explanation.lower(): + explanation += "\n\n---\n\n**Note technique**: Les données fournies pour l'analyse étaient insuffisantes ou incomplètes. Assurez-vous que l'ensemble des analyses de sécurité sont correctement configurées et disponibles." + + return explanation + except Exception as e: + print(f"Une erreur s'est produite lors de l'appel à Mistral: {e}") + return f"Impossible de générer une explication pour cet email. Erreur: {str(e)}" +def answer(filename): + try: + email_data = json.loads(send_curl_requests(filename)) + explanation = generate_explanation(email_data) + print(explanation) except Exception as e: print(f"Une erreur s'est produite : {e}") - if __name__ == '__main__': filename="test_SPF.eml" print(send_curl_requests(filename)) From dba817b273129b6be7e986be8b5df5a10d679d9d Mon Sep 17 00:00:00 2001 From: Tatsuya28 Date: Sat, 12 Apr 2025 13:55:37 -0400 Subject: [PATCH 03/18] Reload quarantine emails after bulk actions and individual updates --- frontend/src/views/QuarantineView.vue | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/src/views/QuarantineView.vue b/frontend/src/views/QuarantineView.vue index da863f8..3f062c5 100644 --- a/frontend/src/views/QuarantineView.vue +++ b/frontend/src/views/QuarantineView.vue @@ -66,6 +66,8 @@ const bulkMarkAsSafe = async () => { return; } await bulkUpdateStatus('SAFE'); + // Recharger spécifiquement les mails en quarantaine après la mise à jour + await loadQuarantineMails(); }; const bulkDelete = async () => { @@ -76,16 +78,22 @@ const bulkDelete = async () => { if (confirm(`Are you sure you want to delete ${selectedMails.value.length} email(s)?`)) { await bulkUpdateStatus('DELETED'); + // Recharger spécifiquement les mails en quarantaine après la mise à jour + await loadQuarantineMails(); } }; const markAsSafe = async (mailId) => { await updateMailStatus(mailId, 'SAFE'); + // Recharger spécifiquement les mails en quarantaine après la mise à jour + await loadQuarantineMails(); }; const deleteMail = async (mailId) => { if (confirm('Are you sure you want to delete this mail?')) { await updateMailStatus(mailId, 'DELETED'); + // Recharger spécifiquement les mails en quarantaine après la mise à jour + await loadQuarantineMails(); } }; From 7e34fd52155df7c7be5abfeb439f8c6997f81cad Mon Sep 17 00:00:00 2001 From: Tatsuya28 Date: Sat, 12 Apr 2025 14:07:12 -0400 Subject: [PATCH 04/18] Remove threatsByCategory from statistics and related UI components --- backend/controllers/statisticsController.js | 35 --------------------- frontend/src/views/StatisticsView.vue | 27 +--------------- 2 files changed, 1 insertion(+), 61 deletions(-) diff --git a/backend/controllers/statisticsController.js b/backend/controllers/statisticsController.js index 40a9906..8686905 100644 --- a/backend/controllers/statisticsController.js +++ b/backend/controllers/statisticsController.js @@ -35,7 +35,6 @@ exports.getStatistics = async (req, res) => { const statistics = { totalMails: mails.length, mailsByStatus: countMailsByStatus(mails), - threatsByCategory: countThreatsByCategory(mails), topSenders: getTopSenders(mails, 5), detectRatio: calculateDetectRatio(mails) }; @@ -176,40 +175,6 @@ function countMailsByStatus(mails) { return statusCounts; } -/** - * Compte les menaces par catégorie - * @param {Array} mails - Liste des mails - * @returns {Object} Comptage par catégorie de menace - */ -function countThreatsByCategory(mails) { - const threatCounts = { - 'Phishing': 0, - 'Malware': 0, - 'Spam': 0, - 'Other': 0 - }; - - // Pour cette démo, nous attribuons des catégories en fonction du sujet - // En production, il faudrait se baser sur des analyses plus précises - mails.forEach(mail => { - if (mail.Statut !== 'QUARANTINE' && mail.Statut !== 'ERROR') return; - - const subject = (mail.Sujet || '').toLowerCase(); - - if (subject.includes('account') || subject.includes('password') || subject.includes('login')) { - threatCounts['Phishing']++; - } else if (subject.includes('virus') || subject.includes('malware')) { - threatCounts['Malware']++; - } else if (subject.includes('offer') || subject.includes('discount') || subject.includes('sale')) { - threatCounts['Spam']++; - } else { - threatCounts['Other']++; - } - }); - - return threatCounts; -} - /** * Identifie les principaux expéditeurs * @param {Array} mails - Liste des mails diff --git a/frontend/src/views/StatisticsView.vue b/frontend/src/views/StatisticsView.vue index aa1000e..33b6929 100644 --- a/frontend/src/views/StatisticsView.vue +++ b/frontend/src/views/StatisticsView.vue @@ -16,7 +16,6 @@ const error = ref(null); const statistics = ref({ totalMails: 0, mailsByStatus: {}, - threatsByCategory: {}, mailsOverTime: [], topSenders: [], detectRatio: 0 @@ -220,11 +219,6 @@ const exportStatistics = () => { data.push([`Status: ${status}`, count]); }); - // Catégories de menaces - Object.entries(statistics.value.threatsByCategory || {}).forEach(([category, count]) => { - data.push([`Threat: ${category}`, count]); - }); - // Convertir en CSV const csvContent = data.map(row => row.join(',')).join('\n'); @@ -396,26 +390,7 @@ onMounted(async () => { -
-

Threat Categories

-
-
-
-
{{ count }}
-
{{ category }}
-
-
-
- -
+
From 2fd8e2024e1738cd0e53524aecebf49bea7430f4 Mon Sep 17 00:00:00 2001 From: Tatsuya28 Date: Sat, 12 Apr 2025 14:27:41 -0400 Subject: [PATCH 05/18] Enhance error handling in login process; add toast notifications for form validation and login success/error messages --- frontend/src/services/api.js | 11 ++++++++++- frontend/src/views/LoginView.vue | 26 +++++++++++++++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 2fca7bd..0c490cf 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -24,11 +24,20 @@ api.interceptors.response.use( error => { // Gestion des erreurs globales (ex: 401 Unauthorized, etc.) if (error.response?.status === 401) { + // Ne pas rediriger vers login si on est déjà sur la page login + // Vérifier si l'URL actuelle contient "/login" + const isLoginPage = window.location.pathname.includes('/login'); + // Créer une nouvelle instance du store const authStore = useAuthStore(); + // Appeler l'action logout du store authStore.logout(); - window.location.href = '/login'; + + // Ne rediriger que si nous ne sommes pas déjà sur la page de connexion + if (!isLoginPage) { + window.location.href = '/login'; + } } return Promise.reject(error); } diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index 50f2396..64c19a4 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -3,9 +3,11 @@ import { reactive } from 'vue' import { RouterLink, useRouter } from 'vue-router'; import api from '@/services/api'; // Importez le service API import { useAuthStore } from '@/stores/authStore'; +import { useToast } from 'vue-toastification'; // Importez useToast const router = useRouter(); const authStore = useAuthStore(); +const toast = useToast(); // Initialiser le toast const loginForm = reactive({ email: '', @@ -52,7 +54,8 @@ const handleSubmit = async () => { const isPasswordValid = validatePassword() if (!isEmailValid || !isPasswordValid) { - return + toast.error("Please correct the errors in the form"); + return; } try { @@ -70,16 +73,29 @@ const handleSubmit = async () => { // Store token in sessionStorage (au lieu de localStorage) authStore.login(data.user, data.token); + // Afficher un toast de succès + toast.success("Login successful! Welcome back!"); + // Redirect to / router.push('/'); } catch (error) { + console.error('Login error:', error); + // Gestion des erreurs améliorée avec Axios - loginForm.error = error.response?.data?.message || 'Failed to sign in. Please check your credentials and try again.' - console.error('Login error:', error) + const errorMessage = error.response?.data?.message || 'Failed to sign in. Please check your credentials and try again.'; + loginForm.error = errorMessage; + + // Afficher un toast d'erreur + toast.error(errorMessage); + + // NE PAS rediriger après une erreur } finally { - loginForm.isLoading = false + loginForm.isLoading = false; } + + // Retourner false pour être absolument sûr que rien ne provoque une soumission + return false; } @@ -106,7 +122,7 @@ const handleSubmit = async () => {
- +
From 22d42129a2dd36e7d0344bd1ae0da493d165e954 Mon Sep 17 00:00:00 2001 From: Tatsuya28 Date: Sat, 12 Apr 2025 14:33:36 -0400 Subject: [PATCH 06/18] Add toast notifications for form validation errors and success messages in registration --- frontend/src/views/RegisterView.vue | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/RegisterView.vue b/frontend/src/views/RegisterView.vue index ff35c76..6aac016 100644 --- a/frontend/src/views/RegisterView.vue +++ b/frontend/src/views/RegisterView.vue @@ -3,9 +3,11 @@ import { reactive } from 'vue' import { RouterLink, useRouter } from 'vue-router'; import api from '@/services/api'; // Importez le service API import { useAuthStore } from '@/stores/authStore'; // Importez le store d'authentification +import { useToast } from 'vue-toastification'; // Import du service de toast const router = useRouter(); const authStore = useAuthStore(); // Utilisez le store d'authentification +const toast = useToast(); // Initialisation du toast const registerForm = reactive({ firstName: '', @@ -97,6 +99,7 @@ const handleSubmit = async () => { const isConfirmPasswordValid = validateConfirmPassword() if (!isFirstNameValid || !isLastNameValid || !isEmailValid || !isPasswordValid || !isConfirmPasswordValid) { + toast.error("Please correct the errors in the form"); // Notification d'erreur de validation return } @@ -117,16 +120,26 @@ const handleSubmit = async () => { // Utiliser authStore au lieu de sessionStorage authStore.login(data.user, data.token); + // Notification de succès + toast.success("Account created successfully! Welcome to Detectish!"); + // Redirect to / router.push('/'); } catch (error) { // Gestion des erreurs améliorée avec Axios - registerForm.error = error.response?.data?.message || 'Failed to create account. Please try again later.' + const errorMessage = error.response?.data?.message || 'Failed to create account. Please try again later.'; + registerForm.error = errorMessage; + + // Notification d'erreur + toast.error(errorMessage); + console.error('Registration error:', error) } finally { registerForm.isLoading = false } + + return false; // Pour s'assurer que la soumission du formulaire est complètement contrôlée } @@ -176,7 +189,7 @@ const handleSubmit = async () => { placeholder="Enter your first name" :class="{ 'ring-red-300': registerForm.firstNameError }" />

{{ registerForm.firstNameError - }}

+ }}

From 49e04dee7a089ca706c89efb5456c54ec98a1d21 Mon Sep 17 00:00:00 2001 From: Tatsuya28 Date: Sat, 12 Apr 2025 14:47:19 -0400 Subject: [PATCH 07/18] Add auto-focus and validation on blur for login and registration forms --- frontend/src/views/LoginView.vue | 48 +++++++++++++++--- frontend/src/views/RegisterView.vue | 79 ++++++++++++++++++++++++++--- 2 files changed, 113 insertions(+), 14 deletions(-) diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index 64c19a4..f5fae7f 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -1,5 +1,5 @@ diff --git a/frontend/src/views/QuarantineView.vue b/frontend/src/views/QuarantineView.vue index 3f062c5..09f237d 100644 --- a/frontend/src/views/QuarantineView.vue +++ b/frontend/src/views/QuarantineView.vue @@ -62,38 +62,67 @@ const loadQuarantineMails = async () => { // Actions spécifiques à la vue Quarantine const bulkMarkAsSafe = async () => { if (selectedMails.value.length === 0) { - alert('Please select at least one email to mark as safe.'); + toast.warning('Please select at least one email to mark as safe.'); return; } - await bulkUpdateStatus('SAFE'); - // Recharger spécifiquement les mails en quarantaine après la mise à jour - await loadQuarantineMails(); + + const selectedCount = selectedMails.value.length; // Stocker le nombre avant l'action + + try { + await bulkUpdateStatus('SAFE'); + toast.success(`${selectedCount} email(s) marked as safe successfully!`); + // Recharger spécifiquement les mails en quarantaine après la mise à jour + await loadQuarantineMails(); + } catch (error) { + toast.error('Failed to mark emails as safe. Please try again.'); + console.error(error); + } }; const bulkDelete = async () => { if (selectedMails.value.length === 0) { - alert('Please select at least one email to delete.'); + toast.warning('Please select at least one email to delete.'); return; } - if (confirm(`Are you sure you want to delete ${selectedMails.value.length} email(s)?`)) { - await bulkUpdateStatus('DELETED'); - // Recharger spécifiquement les mails en quarantaine après la mise à jour - await loadQuarantineMails(); + const selectedCount = selectedMails.value.length; // Stocker le nombre avant l'action + + if (confirm(`Are you sure you want to delete ${selectedCount} email(s)?`)) { + try { + await bulkUpdateStatus('DELETED'); + toast.success(`${selectedCount} email(s) deleted successfully!`); + // Recharger spécifiquement les mails en quarantaine après la mise à jour + await loadQuarantineMails(); + } catch (error) { + toast.error('Failed to delete emails. Please try again.'); + console.error(error); + } } }; const markAsSafe = async (mailId) => { - await updateMailStatus(mailId, 'SAFE'); - // Recharger spécifiquement les mails en quarantaine après la mise à jour - await loadQuarantineMails(); + try { + await updateMailStatus(mailId, 'SAFE'); + toast.success('Email marked as safe successfully!'); + // Recharger spécifiquement les mails en quarantaine après la mise à jour + await loadQuarantineMails(); + } catch (error) { + toast.error('Failed to mark email as safe. Please try again.'); + console.error(error); + } }; const deleteMail = async (mailId) => { - if (confirm('Are you sure you want to delete this mail?')) { - await updateMailStatus(mailId, 'DELETED'); - // Recharger spécifiquement les mails en quarantaine après la mise à jour - await loadQuarantineMails(); + if (confirm('Are you sure you want to delete this email?')) { + try { + await updateMailStatus(mailId, 'DELETED'); + toast.success('Email deleted successfully!'); + // Recharger spécifiquement les mails en quarantaine après la mise à jour + await loadQuarantineMails(); + } catch (error) { + toast.error('Failed to delete email. Please try again.'); + console.error(error); + } } }; @@ -112,6 +141,7 @@ const handleResetSearch = () => { // Fonction pour gérer les uploads réussis const handleUploadSuccess = async ({ fileName }) => { console.log(`File ${fileName} uploaded successfully`); + toast.success(`File ${fileName} uploaded and analyzed successfully!`); // Recharger la liste après un délai setTimeout(() => { @@ -122,6 +152,7 @@ const handleUploadSuccess = async ({ fileName }) => { // Fonction pour gérer les erreurs d'upload const handleUploadError = ({ fileName, error }) => { console.error(`Error uploading ${fileName}:`, error); + toast.error(`Error uploading ${fileName}: ${error}`); }; const handleAskMistral = async (mailId) => { @@ -142,7 +173,12 @@ onMounted(async () => { authStore.initialize(); if (authStore.isLoggedIn) { - await loadQuarantineMails(); + try { + await loadQuarantineMails(); + } catch (error) { + toast.error('Failed to load quarantined emails. Please refresh the page.'); + console.error(error); + } } else { // Rediriger vers login si non connecté router.push('/login'); diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 6da175e..6c407a3 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -2,10 +2,29 @@ import { onMounted } from 'vue'; import { useAuthStore } from '@/stores/authStore'; import { useRouter } from 'vue-router'; +import { useToast } from 'vue-toastification'; // Import pour toast import FilterRulesList from '@/components/settings/FilterRulesList.vue'; const router = useRouter(); const authStore = useAuthStore(); +const toast = useToast(); // Initialiser toast + +// Handler pour les événements du composant enfant +const handleRuleAdded = () => { + toast.success('Filtering rule added successfully!'); +}; + +const handleRuleUpdated = () => { + toast.success('Filtering rule updated successfully!'); +}; + +const handleRuleDeleted = () => { + toast.success('Filtering rule deleted successfully!'); +}; + +const handleRuleError = (errorMessage) => { + toast.error(errorMessage || 'An error occurred with filtering rules'); +}; // Check authentication on mount onMounted(async () => { @@ -39,7 +58,12 @@ onMounted(async () => { Configure which email senders will be automatically blocked. Emails from these senders won't appear in your inbox.

- +
diff --git a/frontend/src/views/StatisticsView.vue b/frontend/src/views/StatisticsView.vue index 33b6929..a37ced0 100644 --- a/frontend/src/views/StatisticsView.vue +++ b/frontend/src/views/StatisticsView.vue @@ -2,6 +2,7 @@ import { ref, onMounted, computed } from 'vue'; import { useAuthStore } from '@/stores/authStore'; import { useRouter } from 'vue-router'; +import { useToast } from 'vue-toastification'; // Import pour toast import statisticsService from '@/services/statisticsService'; import StatisticsCard from '@/components/statistics/StatisticsCard.vue'; import LineChartComponent from '@/components/statistics/LineChartComponent.vue'; @@ -11,6 +12,7 @@ import NoDataMessage from '@/components/statistics/NoDataMessage.vue'; const router = useRouter(); const authStore = useAuthStore(); +const toast = useToast(); // Initialiser toast const loading = ref(true); const error = ref(null); const statistics = ref({ @@ -32,9 +34,11 @@ const loadStatistics = async () => { try { // Utiliser le service dédié au lieu de l'API directement statistics.value = await statisticsService.getStatistics(timePeriod.value); + toast.info(`Statistics for ${timePeriod.value} period loaded successfully`); } catch (err) { console.error('Failed to load statistics:', err); error.value = 'Failed to load statistics. Please try again later.'; + toast.error('Failed to load statistics. Please try again later.'); } finally { loading.value = false; } @@ -204,36 +208,43 @@ const changePeriod = (period) => { // Exporter les statistiques en CSV const exportStatistics = () => { - // Préparer les données - const data = []; + try { + // Préparer les données + const data = []; + + // En-têtes + data.push(['Category', 'Value']); - // En-têtes - data.push(['Category', 'Value']); + // Données générales + data.push(['Total Emails', statistics.value.totalMails]); + data.push(['Threat Percentage', `${threatPercentage.value}%`]); - // Données générales - data.push(['Total Emails', statistics.value.totalMails]); - data.push(['Threat Percentage', `${threatPercentage.value}%`]); + // Statuts + Object.entries(statistics.value.mailsByStatus || {}).forEach(([status, count]) => { + data.push([`Status: ${status}`, count]); + }); - // Statuts - Object.entries(statistics.value.mailsByStatus || {}).forEach(([status, count]) => { - data.push([`Status: ${status}`, count]); - }); + // Convertir en CSV + const csvContent = data.map(row => row.join(',')).join('\n'); - // Convertir en CSV - const csvContent = data.map(row => row.join(',')).join('\n'); + // Créer un lien de téléchargement + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); - // Créer un lien de téléchargement - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); - const link = document.createElement('a'); - const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', `email-statistics-${timePeriod.value}-${new Date().toISOString().split('T')[0]}.csv`); + link.style.visibility = 'hidden'; - link.setAttribute('href', url); - link.setAttribute('download', `email-statistics-${timePeriod.value}-${new Date().toISOString().split('T')[0]}.csv`); - link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + toast.success('Statistics exported successfully!'); + } catch (error) { + toast.error('Failed to export statistics. Please try again.'); + console.error('Export error:', error); + } }; // Initialiser la page From 4f0fc37c3e711c7c5be0259fe06af6de6ec22e97 Mon Sep 17 00:00:00 2001 From: Tatsuya28 Date: Sat, 12 Apr 2025 15:40:48 -0400 Subject: [PATCH 09/18] Enhance MailTableComponent: add click handlers for row expansion, improve document click management, and adjust column widths for better layout --- .../src/components/MailTableComponent.vue | 125 +++++++++++++----- 1 file changed, 94 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/MailTableComponent.vue b/frontend/src/components/MailTableComponent.vue index 556320a..553a3b6 100644 --- a/frontend/src/components/MailTableComponent.vue +++ b/frontend/src/components/MailTableComponent.vue @@ -1,11 +1,11 @@