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/docker-compose.yml b/docker-compose.yml index f59e1f9..4873a69 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,9 @@ services: detectish: image: matthl2002/detectish:latest + # build: + # context: . + # dockerfile: Dockerfile container_name: detectish depends_on: mysql: @@ -22,6 +25,7 @@ services: DB_PORT: 3306 CLAMAV_HOST: clamav CLAMAV_PORT: 3310 + MISTRAL_API_KEY: your_mistral_api_key # Replace with your actual Mistral API key healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:6969/health || exit 1"] interval: 30s @@ -32,6 +36,9 @@ services: backend: image: matthl2002/detectish-backend:latest + # build: + # context: ./backend + # dockerfile: Dockerfile container_name: detectish-backend depends_on: mysql: @@ -62,6 +69,9 @@ services: frontend: image: matthl2002/detectish-frontend:latest + # build: + # context: ./frontend + # dockerfile: Dockerfile container_name: detectish-frontend depends_on: backend: 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", diff --git a/frontend/src/components/MailComponent.vue b/frontend/src/components/MailComponent.vue index bccc061..792dbb3 100644 --- a/frontend/src/components/MailComponent.vue +++ b/frontend/src/components/MailComponent.vue @@ -32,7 +32,7 @@ const loadMailDetails = async () => { const response = await api.get(`/mails/${props.mail.ID_Mail}/complete`); mailDetails.value = response.data; } catch (error) { - console.error('Erreur lors du chargement des détails du mail:', error); + console.error('Error loading email details:', error); } finally { loading.value = false; } @@ -85,7 +85,7 @@ onMounted(() => {
- +
{ />
- +
@@ -112,7 +112,7 @@ onMounted(() => {
- +

Email Content

@@ -130,31 +130,31 @@ onMounted(() => {
- +
Secure View: Links and forms are disabled
- + - +
{{ mailDetails.content || "The content of this email is not available or is empty." }}
- +
Warning: This email may contain unsafe content
- +
diff --git a/frontend/src/composables/useMailTable.js b/frontend/src/composables/useMailTable.js index 4d4e7e2..fb72e85 100644 --- a/frontend/src/composables/useMailTable.js +++ b/frontend/src/composables/useMailTable.js @@ -1,19 +1,20 @@ import { ref, computed } from 'vue'; import mailService from '@/services/mailService'; +import axios from 'axios'; export function useMailTable() { - // États + // State const mails = ref([]); const loading = ref(false); const error = ref(null); const expandedMailId = ref(null); const selectedMails = ref([]); - // Tri + // Sorting const sortColumn = ref('Date_Reception'); const sortDirection = ref('desc'); - // Recherche + // Search const searchQuery = ref({ status: '', sender: '', @@ -23,71 +24,63 @@ export function useMailTable() { dateTo: '' }); - // Computed property pour vérifier si tous les mails sont sélectionnés + // Mistral states + const mistralLoading = ref(false); + const mistralResponse = ref(null); + const mistralError = ref(null); + const mistralEmailId = ref(null); + + // Check if all emails are selected const allSelected = computed(() => { return filteredMails.value.length > 0 && selectedMails.value.length === filteredMails.value.length; }); - // Mails filtrés selon les critères de recherche + // Emails filtered by search criteria const filteredMails = computed(() => { if (!mails.value?.length) return []; return mails.value.filter(mail => { - // Filtrer par statut + // Filter by status if (searchQuery.value.status && mail.Statut?.toLowerCase() !== searchQuery.value.status.toLowerCase()) { return false; } - // Filtrer par expéditeur + // Filter by sender if (searchQuery.value.sender && !mail.Emetteur?.toLowerCase().includes(searchQuery.value.sender.toLowerCase())) { return false; } - // Filtrer par destinataire (ID_Utilisateur ou email) + // Filter by recipient (User ID or 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; } } - // Filtrer par sujet + // Filter by subject if (searchQuery.value.subject && !mail.Sujet?.toLowerCase().includes(searchQuery.value.subject.toLowerCase())) { return false; } - // Filtrer par date (de) + // Filter by date (from) if (searchQuery.value.dateFrom) { const dateFrom = new Date(searchQuery.value.dateFrom); const mailDate = new Date(mail.Date_Reception); if (mailDate < dateFrom) return false; } - // Filtrer par date (à) + // Filter by date (to) if (searchQuery.value.dateTo) { const dateTo = new Date(searchQuery.value.dateTo); - dateTo.setHours(23, 59, 59, 999); // Fin de journée + dateTo.setHours(23, 59, 59, 999); // End of day const mailDate = new Date(mail.Date_Reception); if (mailDate > dateTo) return false; } @@ -96,7 +89,7 @@ export function useMailTable() { }); }); - // Mails triés selon les critères actuels (après filtrage) + // Emails sorted by current criteria (after filtering) const sortedMails = computed(() => { if (!filteredMails.value?.length) return []; @@ -105,7 +98,7 @@ export function useMailTable() { sorted.sort((a, b) => { let valA, valB; - // Déterminer les valeurs à comparer selon la colonne + // Determine values to compare based on column switch(sortColumn.value) { case 'Statut': valA = a.Statut || ''; @@ -132,7 +125,7 @@ export function useMailTable() { valB = b[sortColumn.value] || ''; } - // Comparaison selon la direction + // Compare based on direction if (sortDirection.value === 'asc') { return valA > valB ? 1 : valA < valB ? -1 : 0; } else { @@ -143,23 +136,23 @@ export function useMailTable() { return sorted; }); - // Mettre à jour les critères de recherche + // Update search criteria const updateSearchQuery = (query) => { - // Convertir explicitement les valeurs de status en majuscules pour correspondre au format du backend + // Explicitly convert status values to uppercase to match backend format if (query.status) { query.status = query.status.toUpperCase(); } - // Date range - s'assurer que les dates sont dans le bon format + // Ensure dates are in the correct format if (query.dateFrom && typeof query.dateFrom === 'string') { - // S'assurer que dateFrom commence au début de la journée + // Ensure dateFrom starts at the beginning of the day const dateFrom = new Date(query.dateFrom); dateFrom.setHours(0, 0, 0, 0); query.dateFrom = dateFrom.toISOString(); } if (query.dateTo && typeof query.dateTo === 'string') { - // S'assurer que dateTo va jusqu'à la fin de la journée + // Ensure dateTo extends to the end of the day const dateTo = new Date(query.dateTo); dateTo.setHours(23, 59, 59, 999); query.dateTo = dateTo.toISOString(); @@ -168,7 +161,7 @@ export function useMailTable() { searchQuery.value = { ...searchQuery.value, ...query }; }; - // Réinitialiser les critères de recherche + // Reset search criteria const resetSearch = () => { searchQuery.value = { status: '', @@ -180,14 +173,14 @@ export function useMailTable() { }; }; - // Charger les mails par statut + // Load emails by status const loadMails = async (statusList, keepSearch = false) => { loading.value = true; error.value = null; selectedMails.value = []; expandedMailId.value = null; - // Ne réinitialise la recherche que si le paramètre keepSearch est false + // Only reset search if keepSearch parameter is false if (!keepSearch) { resetSearch(); } @@ -202,12 +195,12 @@ export function useMailTable() { } }; - // Puis ajouter une méthode pour rafraîchir les données tout en conservant la recherche + // Method to refresh data while preserving search criteria const refreshWithCurrentFilters = async (statusList) => { - await loadMails(statusList, true); // true pour garder les critères de recherche + await loadMails(statusList, true); // true to keep search criteria }; - // Actions de sélection + // Selection actions const toggleSelectAll = () => { if (allSelected.value) { selectedMails.value = []; @@ -229,12 +222,12 @@ export function useMailTable() { return selectedMails.value.includes(mailId); }; - // Actions d'expansion + // Expansion actions const toggleExpand = (mailId) => { expandedMailId.value = expandedMailId.value === mailId ? null : mailId; }; - // Actions de tri + // Sorting actions const toggleSort = (column) => { if (sortColumn.value === column) { sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc'; @@ -253,14 +246,14 @@ export function useMailTable() { : 'pi-sort-amount-down text-blue-500'; }; - // Actions sur les mails + // Email actions const bulkUpdateStatus = async (status) => { if (selectedMails.value.length === 0) return; try { loading.value = true; await mailService.bulkUpdateMailStatus(selectedMails.value, status); - // Recharger les mails du même statut + // Reload emails with the same status await loadMails(mails.value[0]?.Statut || ''); } catch (err) { console.error(`Failed to update mail status to ${status}:`, err); @@ -273,7 +266,7 @@ export function useMailTable() { const updateMailStatus = async (mailId, status) => { try { await mailService.updateMailStatus(mailId, status); - // Recharger les mails du même statut + // Reload emails with the same status await loadMails(mails.value[0]?.Statut || ''); } catch (err) { console.error(`Failed to update mail status to ${status}:`, err); @@ -281,8 +274,60 @@ export function useMailTable() { } }; + // Method to call Mistral API directly + const askMistral = async (emailId) => { + mistralLoading.value = true; + mistralError.value = null; + mistralResponse.value = null; + mistralEmailId.value = emailId; + + try { + // First, retrieve all detailed email data + const emailDetails = await mailService.getMailDetails(emailId); + + // Then send these detailed data to Mistral API + const response = await axios.post('/analyse/mistral', { + emailId: emailId, + emailData: emailDetails // Send complete email data + }, { + headers: { + 'Content-Type': 'application/json' + } + }); + + if (response.data && response.data.explanation) { + mistralResponse.value = response.data.explanation; + } else { + mistralResponse.value = "No explanation available."; + } + } catch (error) { + console.error("Error requesting explanation from Mistral:", error); + + mistralResponse.value = `# Problem analyzing the email + +We couldn't generate a satisfactory explanation for this email. + +## Possible causes: +- Insufficient data for analysis +- The AI service couldn't process the information correctly +- Connection issue with the analysis service + +Please review email details manually to understand why it was filtered.`; + + } finally { + mistralLoading.value = false; + } + }; + + const resetMistral = () => { + mistralLoading.value = false; + mistralResponse.value = null; + mistralError.value = null; + mistralEmailId.value = null; + }; + return { - // État + // State mails, loading, error, @@ -294,7 +339,7 @@ export function useMailTable() { allSelected, searchQuery, - // Méthodes + // Methods loadMails, toggleSelectAll, toggleSelect, @@ -306,6 +351,14 @@ export function useMailTable() { updateMailStatus, updateSearchQuery, resetSearch, - refreshWithCurrentFilters + refreshWithCurrentFilters, + + // Mistral states and methods + mistralLoading, + mistralResponse, + mistralError, + mistralEmailId, + askMistral, + resetMistral }; } diff --git a/frontend/src/main.js b/frontend/src/main.js index 01dfaa3..1691cf5 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,10 +19,11 @@ const pinia = createPinia(); app.use(pinia); app.use(router); +app.use(PrimeVue); const toastOptions = { position: "top-right", - timeout: 5000, + timeout: 3000, closeOnClick: true, pauseOnFocusLoss: true, pauseOnHover: true, 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/services/mistralService.js b/frontend/src/services/mistralService.js new file mode 100644 index 0000000..8b1767d --- /dev/null +++ b/frontend/src/services/mistralService.js @@ -0,0 +1,120 @@ +import axios from 'axios'; + +/** + * Service to interact with Mistral API + */ +export default { + /** + * Request an explanation from Mistral for a specific email + * This implementation sends a direct request to the Python service + * @param {number|string} emailId - Email identifier + * @param {Object} mailDetails - Complete email details + * @returns {Promise} Promise containing the explanation + */ + async getExplanation(emailId, mailDetails) { + try { + // Prepare data to send + const requestData = { + emailId: emailId, + // Send complete mail details + emailData: mailDetails + }; + + // Direct call to Python service without going through /api/ + const response = await axios.post('/analyse/mistral/', requestData, { + headers: { + 'Content-Type': 'application/json' + } + }); + + // If response is successful but without explanation, use a default explanation + if (response.data && !response.data.explanation) { + return { + explanation: "No detailed explanation was provided by the analysis service.", + status: response.data.status || 'success' + }; + } + + return response.data; + } catch (error) { + console.error('Error getting Mistral explanation:', error); + + // In case of error, fall back to a simulated explanation for better UX + return { + explanation: this._generateFallbackExplanation(emailId, error, mailDetails), + status: 'fallback' + }; + } + }, + + /** + * Generate a fallback explanation if the API call fails + * @private + * @param {number|string} emailId - Email identifier + * @param {Error} error - The error encountered + * @param {Object} mailDetails - Mail details if available + * @returns {string} Explanation in markdown format + */ + _generateFallbackExplanation(emailId, error, mailDetails) { + // If we have mail details, generate a more relevant explanation + if (mailDetails) { + const subject = mailDetails.Sujet || 'No subject'; + const sender = mailDetails.Emetteur || 'unknown'; + const status = mailDetails.Statut || 'UNKNOWN'; + + return `# Email Analysis + +## Email Information +- **Subject**: ${subject} +- **Sender**: ${sender} +- **Status**: ${status} + +## Provisional Explanation +We couldn't get a complete automated analysis for this email. Here are some points that could explain its status: + +${status === 'QUARANTINED' ? ` +- The email may contain suspicious elements +- The sender may not be confirmed +- SPF, DKIM or DMARC verification issues may be present +` : ''} + +${status === 'SPAM' ? ` +- The email has been identified as potential spam +- It may contain keywords associated with spam +- The sender may be on a watchlist +` : ''} + +${status === 'DELIVERED' ? ` +- The email passed all security checks +- No suspicious elements were detected +` : ''} + +## Technical Message +\`\`\` +${error?.message || "Communication error with analysis service"} +\`\`\` +`; + } + + // Fallback if no details + return `# Communication problem with analysis service + +We couldn't get an automated explanation for this email (ID: ${emailId}). + +## Possible reason + +The Mistral analysis service is currently unavailable. This may be due to: + +- The service is undergoing maintenance +- A network connectivity issue +- A configuration error + +## Technical error message + +\`\`\` +${error?.message || "Unknown error"} +\`\`\` + +Please try again later or contact your system administrator if the problem persists.`; + } +}; diff --git a/frontend/src/views/HistoryView.vue b/frontend/src/views/HistoryView.vue index 9a6b6ab..aacf146 100644 --- a/frontend/src/views/HistoryView.vue +++ b/frontend/src/views/HistoryView.vue @@ -3,10 +3,12 @@ import { onMounted } from 'vue'; import { useAuthStore } from '@/stores/authStore'; import { useRouter } from 'vue-router'; import { useMailTable } from '@/composables/useMailTable'; +import { useToast } from 'vue-toastification'; // Import pour toast import MailTableComponent from '@/components/MailTableComponent.vue'; const router = useRouter(); const authStore = useAuthStore(); +const toast = useToast(); // Initialiser toast // Utiliser le composable useMailTable pour la logique de gestion des mails const { @@ -31,30 +33,50 @@ const { // Fonctions spécifiques à la vue History const loadHistoryMails = async () => { - // Charger les mails avec statut SAFE, DELETED ou PASS - await loadMails('SAFE,DELETED,PASS'); + try { + // Charger les mails avec statut SAFE, DELETED ou PASS + await loadMails('SAFE,DELETED,PASS'); + } catch (error) { + toast.error('Failed to load email history. Please try again.'); + console.error(error); + } }; // Actions spécifiques à la vue History const bulkRestoreToQuarantine = async () => { if (selectedMails.value.length === 0) { - alert('Please select at least one email to move to quarantine.'); + toast.warning('Please select at least one email to move to quarantine.'); return; } - // Attendre que l'action se termine avant de recharger - await bulkUpdateStatus('QUARANTINE'); + // Stocker le nombre d'emails sélectionnés avant l'action + const selectedCount = selectedMails.value.length; + + try { + // Attendre que l'action se termine avant de recharger + await bulkUpdateStatus('QUARANTINE'); + toast.success(`${selectedCount} email(s) moved to quarantine!`); - // Recharger la liste pour refléter les changements - await loadHistoryMails(); + // Recharger la liste pour refléter les changements + await loadHistoryMails(); + } catch (error) { + toast.error('Failed to move emails to quarantine. Please try again.'); + console.error(error); + } }; const restoreToQuarantine = async (mailId) => { - // Attendre que l'action se termine avant de recharger - await updateMailStatus(mailId, 'QUARANTINE'); + try { + // Attendre que l'action se termine avant de recharger + await updateMailStatus(mailId, 'QUARANTINE'); + toast.success('Email moved to quarantine successfully!'); - // Recharger la liste pour refléter les changements - await loadHistoryMails(); + // Recharger la liste pour refléter les changements + await loadHistoryMails(); + } catch (error) { + toast.error('Failed to move email to quarantine. Please try again.'); + console.error(error); + } }; // Méthodes pour la recherche @@ -93,6 +115,7 @@ onMounted(async () => { :sort-direction="sortDirection" :status-types="['SAFE', 'DELETED', 'PASS']" :search-query="searchQuery" + :show-mistral-button="false" @toggle-select-all="toggleSelectAll" @toggle-select="toggleSelect" @toggle-expand="toggleExpand" diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 4a0a741..b64d93d 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -3,10 +3,12 @@ import HeroComponent from '@/components/HeroComponent.vue'; import { onMounted, ref } 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'; const authStore = useAuthStore(); const router = useRouter(); +const toast = useToast(); // Initialiser toast // État pour les statistiques const stats = ref(null); @@ -21,6 +23,7 @@ const loadBasicStats = async () => { stats.value = await statisticsService.getStatistics('month'); } catch (err) { console.error('Failed to load basic statistics:', err); + toast.error('Failed to load dashboard statistics'); } finally { loading.value = false; } @@ -36,6 +39,7 @@ onMounted(() => { // Logout function if needed locally in the component const logout = () => { authStore.logout(); + toast.success('You have been signed out successfully'); router.push('/'); }; diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index 50f2396..f5fae7f 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -1,11 +1,14 @@ @@ -106,7 +157,7 @@ const handleSubmit = async () => {
- +
@@ -127,7 +178,8 @@ const handleSubmit = async () => {
-
@@ -145,7 +197,7 @@ const handleSubmit = async () => {
diff --git a/frontend/src/views/RegisterView.vue b/frontend/src/views/RegisterView.vue index ff35c76..0f14fca 100644 --- a/frontend/src/views/RegisterView.vue +++ b/frontend/src/views/RegisterView.vue @@ -1,11 +1,14 @@ @@ -170,13 +243,14 @@ const handleSubmit = async () => {
-

{{ registerForm.firstNameError - }}

+ }}

@@ -190,7 +264,8 @@ const handleSubmit = async () => {
@@ -208,7 +283,8 @@ const handleSubmit = async () => {
-
@@ -227,7 +303,7 @@ const handleSubmit = async () => {
-
-

Threat Categories

-
-
-
-
{{ count }}
-
{{ category }}
-
-
-
- -
+
@@ -477,6 +476,12 @@ onMounted(async () => { ERROR: Emails that encountered processing errors during analysis.
+
+
+
+ Detection Rate: Percentage of emails identified as suspicious or malicious. +
+
diff --git a/src/main.py b/src/main.py index 0907eae..ea25ae2 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() +# CORS configuration to allow requests from the frontend +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # In production, limit to specific origins + 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 to generate a Mistral explanation for an email + + Args: + data: Data containing the email ID and its complete details + """ + try: + email_id = data.get("emailId") + email_data = data.get("emailData") + + if not email_id: + return {"error": "Missing email ID", "status": "error"} + + # Use the detailed email data if available + if email_data: + print(f"Complete data received for email ID {email_id}") + + # Generate explanation from the detailed data + explanation = mistral_explain.generate_explanation(email_data) + + return { + "explanation": explanation, + "status": "success" + } + else: + # Fallback: retrieve data from the database + print(f"No detailed data received for email ID {email_id}, retrieving from database") + email_data = {"id": email_id, "message": "Data not available"} + + explanation = "I did not receive enough information to analyze this email. Please verify the system configuration." + + return { + "explanation": explanation, + "status": "limited" + } + except Exception as e: + print(f"Error generating explanation: {str(e)}") + return { + "explanation": f"An error occurred during analysis: {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..cfd5894 100644 --- a/src/mistral_explain.py +++ b/src/mistral_explain.py @@ -1,9 +1,11 @@ -import os import subprocess import os +import json from mistralai import Mistral from dotenv import load_dotenv +# Load environment variables from .env file if available +# (useful for local development) load_dotenv() def send_curl_requests(filename): @@ -16,36 +18,101 @@ def send_curl_requests(filename): ], capture_output=True, text=True) return result.stdout - -def answer(filename): +def generate_explanation(email_data): + """ + Generates a structured explanation for an email using Mistral + + Args: + email_data (dict): Complete email data and its analysis + + Returns: + str: The explanation generated by Mistral + """ try: model = "mistral-large-latest" - api_key = os.environ["MISTRAL_API_KEY"] + # Get API key from environment variables + api_key = os.environ.get("MISTRAL_API_KEY") + if not api_key: + raise ValueError("Mistral API key is not configured in environment variables") + client = Mistral(api_key=api_key) + + # Extract and organize important information for analysis + organized_data = { + "email": { + "subject": email_data.get("subject") or "No subject", + "sender": email_data.get("sender") or "Unknown", + "receiver": email_data.get("user", {}).get("email") or "Unknown", + "date": email_data.get("receivedDate") or "Unknown", + "status": email_data.get("status") or "Unknown" + }, + "analysis": { + "spf": next((a.get("result") for a in email_data.get("analyses", []) if a.get("type") == "SPF"), "Unknown"), + "dkim": next((a.get("result") for a in email_data.get("analyses", []) if a.get("type") == "DKIM"), "Unknown"), + "dmarc": next((a.get("result") for a in email_data.get("analyses", []) if a.get("type") == "DMARC"), "Unknown"), + "malware_scan": next((a.get("result") for a in email_data.get("analyses", []) if a.get("type") == "CLAMAV"), "Unknown"), + "phishing_score": next((a.get("result") for a in email_data.get("analyses", []) if a.get("type") == "AI"), "Unknown"), + "urls": [link.get("url") for link in email_data.get("links", [])] + }#, + #"detailed_analysis": email_data.get("analysis") or email_data.get("analyse_details") or [] + } + + # Convert organized data to JSON for the prompt + analysis_json = json.dumps(organized_data, indent=2, ensure_ascii=False) + + # Build a richer and structured prompt + system_prompt = """ +You are a cybersecurity expert explaining in simple English why an email was filtered. +Your mission is to analyze the technical data and explain clearly and accessibly why the email was considered suspicious. - # 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}) +Here is the structure of the data you will analyze: +- email: basic information about the email (sender, receiver, subject, date) +- analysis: results of various security analyses (SPF, DKIM, DMARC, antivirus scan, phishing score) +- detailed_analysis: additional detailed analyses - # Appel à l'API Mistral +Rules to follow: +1. Your explanation must be clear and understandable for non-experts +2. Identify the main security issues detected +3. Explain why these issues are concerning +4. If the information is insufficient, indicate it clearly +5. Use a structured format with titles and subtitles +6. Avoid technical jargon unless you explain it + """ + + # Prepare messages for Mistral + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "system", "content": f"Here is the email data to analyze: {analysis_json}"}, + {"role": "user", "content": "Why was this email filtered? Explain the detected security issues clearly and precisely."} + ] + + # Call Mistral API 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 + ) + + # Return the response + explanation = chat_response.choices[0].message.content + + # If the explanation is too generic, add a warning + if "I don't have enough information" in explanation.lower() or "missing information" in explanation.lower(): + explanation += "\n\n---\n\n**Technical Note**: The data provided for the analysis was insufficient or incomplete. Ensure that all security analyses are properly configured and available." + + return explanation except Exception as e: - print(f"Une erreur s'est produite : {e}") + print(f"An error occurred while calling Mistral: {e}") + return f"Unable to generate an explanation for this email. Error: {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"An error occurred: {e}") if __name__ == '__main__': filename="test_SPF.eml"