diff --git a/frontend/src/components/Analysis.jsx b/frontend/src/components/Analysis.jsx index b67c633..a1b9f2c 100644 --- a/frontend/src/components/Analysis.jsx +++ b/frontend/src/components/Analysis.jsx @@ -1,90 +1,16 @@ -import jsPDF from 'jspdf'; -import html2canvas from 'html2canvas'; -import { FaTrash, FaFilePdf, FaEdit, FaEye, FaChevronDown, FaChevronUp, FaTable, FaThLarge, FaEyeSlash } from "react-icons/fa"; +import React from 'react'; import { useEffect, useState } from "react"; -import { Link, useParams } from "react-router-dom"; -import { Flex, TableContainer, Table, Tr, Td, Thead, Th, Tbody, Card, CardHeader, CardBody, Grid, GridItem, Text, Image as ChakraImage, Modal, ModalOverlay, ModalContent, ModalCloseButton, ModalBody, useDisclosure, Button, Icon, useToast, Tabs, TabList, TabPanels, Tab, TabPanel, Accordion, AccordionItem, AccordionButton, AccordionPanel, AccordionIcon, VStack, HStack, Divider, Badge, Collapse, useColorModeValue, Box, Tooltip, Switch, FormControl, FormLabel } from "@chakra-ui/react"; -import { fetchInformationSystemById, updateThreatsRiskBatch, createThreatForSystem, updateThreatResidualRisk, updateThreatsResidualRiskBatch, updateThreatRisk, deleteThreat } from "../services/index"; +import { useParams } from "react-router-dom"; +import { FaTrash, FaFilePdf, FaEdit, FaEye, FaTable, FaThLarge, FaEyeSlash } from "react-icons/fa"; +import { Flex, TableContainer, Table, Tr, Td, Thead, Th, Tbody, Card, CardHeader, CardBody, Grid, GridItem, Text, Image as ChakraImage, Modal, ModalOverlay, ModalContent, ModalCloseButton, ModalBody, useDisclosure, Button, useToast, Tabs, TabList, TabPanels, Tab, TabPanel, Accordion, AccordionItem, AccordionButton, AccordionPanel, AccordionIcon, VStack, HStack, Divider, Badge, useColorModeValue, Box, Tooltip, Switch, FormControl, FormLabel } from "@chakra-ui/react"; +import { fetchInformationSystemById, updateThreatsRiskBatch, createThreatForSystem, deleteThreat } from "../services/index"; import { useLocalization, getOwaspSelectOptions } from '../hooks/useLocalization'; import OwaspSelector from './OwaspSelector'; import ReportGenerator from './ReportGenerator'; - -// Función para calcular la altura del textarea basada en el contenido -const calculateTextareaHeight = (text, width) => { - const lineHeight = 20; // altura de línea en px - const padding = 8; // padding vertical - const baseHeight = 40; // altura mínima - - if (!text) return baseHeight; - - // Estimar el número de líneas basado en la longitud del texto y el ancho - const averageCharsPerLine = Math.floor(width / 8); // aproximadamente 8px por carácter - const estimatedLines = Math.ceil(text.length / averageCharsPerLine); - const newLines = (text.match(/\n/g) || []).length; - - const totalLines = Math.max(estimatedLines, newLines + 1); - const calculatedHeight = Math.max(baseHeight, totalLines * lineHeight + padding * 2); - - return Math.min(calculatedHeight, 120); // máximo 120px de altura -}; - -// Función para auto-redimensionar textarea cuando el usuario escribe -const handleTextareaResize = (event) => { - const textarea = event.target; - textarea.style.height = 'auto'; - textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px'; -}; - -// Función para calcular el riesgo inherente usando OWASP Risk Rating completo -const calculateInherentRisk = (risk) => { - if (!risk) return 0; - - // OWASP Risk Rating: (Likelihood + Impact) / 2 - const likelihoodFactors = [ - // Threat Agent Factors - risk.skill_level || 0, - risk.motive || 0, - risk.opportunity || 0, - risk.size || 0, - // Vulnerability Factors - risk.ease_of_discovery || 0, - risk.ease_of_exploit || 0, - risk.awareness || 0, - risk.intrusion_detection || 0 - ]; - - const impactFactors = [ - // Technical Impact - risk.loss_of_confidentiality || 0, - risk.loss_of_integrity || 0, - risk.loss_of_availability || 0, - risk.loss_of_accountability || 0, - // Business Impact - risk.financial_damage || 0, - risk.reputation_damage || 0, - risk.non_compliance || 0, - risk.privacy_violation || 0 - ]; - - const likelihood = likelihoodFactors.reduce((acc, val) => acc + val, 0) / likelihoodFactors.length; - const impact = impactFactors.reduce((acc, val) => acc + val, 0) / impactFactors.length; - - const overallRisk = (likelihood + impact) / 2; - return overallRisk.toFixed(2); -}; - -// Función para calcular el riesgo residual basado en el riesgo inherente y la remediación - -// Función para obtener el color del riesgo inherente según OWASP Risk Rating Scale -// Esta función ha sido reemplazada por la definida dentro del componente - -// Función para obtener el label del riesgo según OWASP -const getRiskLabel = (riskValue, t) => { - const value = parseFloat(riskValue); - if (value < 3) return t?.ui?.risk_low || "LOW"; - if (value < 6) return t?.ui?.risk_medium || "MEDIUM"; - return t?.ui?.risk_high || "HIGH"; -}; +import RiskDisplay from './RiskDisplay'; +import ResidualRiskSelector from './ResidualRiskSelector'; +import { calculateInherentRisk, getRiskColorCSS, getRiskLabel } from '../utils/riskCalculations'; +import { handleTextareaResize, calculateTextareaHeight } from '../utils/textareaHelpers'; const Analysis = () => { const [isLoading, setIsLoading] = useState(true); @@ -94,25 +20,17 @@ const Analysis = () => { const [deletedThreats, setDeletedThreats] = useState([]); const [inherentRisks, setInherentRisks] = useState({}); const [residualRisks, setResidualRisks] = useState({}); - const { locale, t, changeLanguage } = useLocalization(); + const { locale, t } = useLocalization(); const { isOpen, onOpen, onClose } = useDisclosure(); const toast = useToast(); // Hook para el generador de reportes const reportGenerator = ReportGenerator(); - // Hook para colores según el modo de color + // Hook for colors based on color mode const controlBg = useColorModeValue('gray.50', 'gray.700'); - // Función para obtener el label del riesgo según OWASP (dentro del componente para acceder a t) - const getRiskLabel = (riskValue) => { - const value = parseFloat(riskValue); - if (value < 3) return t?.ui?.risk_low || "BAJO"; - if (value < 6) return t?.ui?.risk_medium || "MEDIO"; - return t?.ui?.risk_high || "ALTO"; - }; - - // Función para renderizar los tipos como badges separados + // Function to render types as separate badges const renderTypeBadges = (typeString) => { if (!typeString) return null; @@ -129,89 +47,54 @@ const Analysis = () => { ); }; - // Estados para controlar la vista de la tabla + // States to control table view const [viewMode, setViewMode] = useState('compact'); // 'compact', 'detailed', 'tabs' - const [showRiskAssessment, setShowRiskAssessment] = useState(true); // Controla visibilidad de evaluación OWASP - const [expandedSections, setExpandedSections] = useState({ - 'threat_agent': true, - 'vulnerability': false, - 'technical': false, - 'business': false - }); - - // Función helper para obtener colores de riesgo (mejorada) - const getRiskColor = (risk) => { - const numericRisk = typeof risk === 'string' ? parseFloat(risk) : risk; - // Usar los mismos umbrales que getRiskLabel para consistencia - if (numericRisk >= 6) return 'red.500'; - if (numericRisk >= 3) return 'orange.500'; - return 'green.500'; - }; - - // Función helper para obtener el colorScheme de Chakra UI - const getRiskColorScheme = (risk) => { - const numericRisk = typeof risk === 'string' ? parseFloat(risk) : risk; - if (numericRisk >= 6) return 'red'; - if (numericRisk >= 3) return 'orange'; - return 'green'; - }; - - // Función helper para obtener el color CSS del riesgo - const getRiskColorCSS = (risk) => { - const numericRisk = typeof risk === 'string' ? parseFloat(risk) : risk; - if (numericRisk >= 6) return '#e53e3e'; // red.500 - if (numericRisk >= 3) return '#dd6b20'; // orange.500 - return '#38a169'; // green.500 - }; + const [showRiskAssessment, setShowRiskAssessment] = useState(true); // Controls OWASP evaluation visibility - // Función helper para obtener el valor numérico del riesgo inherente - const getRiskValue = (threatId) => { - const risk = inherentRisks[threatId]; - if (risk === undefined || risk === null) return 0; - return typeof risk === 'number' ? risk : parseFloat(risk) || 0; - }; - - // Función helper para obtener el valor numérico del riesgo residual - const getResidualRiskValue = (threatId) => { - const risk = residualRisks[threatId]; - if (risk === undefined || risk === null) return 1; - return typeof risk === 'number' ? risk : parseFloat(risk) || 1; - }; - - // Función helper para obtener el riesgo actual (inherente si no hay remediación aplicada, residual si hay) - const getCurrentRiskValue = (threatId) => { - const threat = threats.find(t => t.id === threatId); - const isRemediationApplied = threat?.remediation?.status === true; - - console.log(`getCurrentRiskValue para threat ${threatId}:`, { - threatFound: !!threat, - remediationStatus: threat?.remediation?.status, - isRemediationApplied - }); + // Generic function to get risk values formatted to 1 decimal + const getRiskValue = (threatId, riskType = 'inherent') => { + let risk; + let defaultValue = 0; - if (isRemediationApplied) { - // Si la remediación está aplicada, usar el riesgo residual - const residualRisk = getResidualRiskValue(threatId); - console.log(`Usando riesgo residual: ${residualRisk}`); - return residualRisk; - } else { - // Si la remediación no está aplicada, usar el riesgo inherente - const inherentRisk = getRiskValue(threatId); - console.log(`Usando riesgo inherente: ${inherentRisk}`); - return inherentRisk; + switch (riskType) { + case 'inherent': + risk = inherentRisks[threatId]; + defaultValue = 0; + break; + case 'residual': + risk = residualRisks[threatId]; + defaultValue = 1; + break; + case 'current': + const threat = threats.find(t => t.id === threatId); + const isRemediationApplied = threat?.remediation?.status === true; + return isRemediationApplied ? + getRiskValue(threatId, 'residual') : + getRiskValue(threatId, 'inherent'); + default: + risk = inherentRisks[threatId]; + defaultValue = 0; } + + if (risk === undefined || risk === null) return defaultValue; + const numericRisk = typeof risk === 'number' ? risk : parseFloat(risk) || defaultValue; + return parseFloat(numericRisk.toFixed(1)); }; - - // Función para actualizar manualmente el riesgo residual desde un select + + // Helper functions for compatibility + const getResidualRiskValue = (threatId) => getRiskValue(threatId, 'residual'); + const getCurrentRiskValue = (threatId) => getRiskValue(threatId, 'current'); + + // Function to manually update residual risk from a select const updateResidualRisk = (threatId, value) => { - const numericValue = parseFloat(value); + const numericValue = parseFloat(parseFloat(value).toFixed(1)); setResidualRisks(prev => ({ ...prev, [threatId]: numericValue })); }; - // Función helper para crear un switch de remediación + // Helper function to create a remediation switch const createRemediationSwitch = (threat, size = "md") => { return ( { ); }; - // Función helper para crear un selector de riesgo residual con apariencia mejorada + // Helper function to create residual risk selector with improved appearance // Estilos centralizados para los componentes de riesgo - const riskDisplayStyles = { - container: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - gap: '2px', - width: '100%' - }, - containerWithSelector: { - position: 'relative', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - gap: '2px', - width: '100%', - cursor: 'pointer' - }, - numberValue: { - fontWeight: 'bold', - fontSize: '16px', - textAlign: 'center' - }, - riskLabel: { - fontSize: '10px', - fontWeight: 'bold', - color: 'white', - padding: '2px 6px', - borderRadius: '8px', - textAlign: 'center', - minWidth: '50px' - }, - invisibleSelect: { - position: 'absolute', - top: 0, - left: 0, - width: '100%', - height: '100%', - opacity: 0, - cursor: 'pointer', - zIndex: 10 - }, - dropdownIcon: { - position: 'absolute', - top: '2px', - right: '2px', - fontSize: '8px', - pointerEvents: 'none', - zIndex: 5 - } - }; - - const createResidualRiskSelector = (threatId, currentValue) => { - // Crear opciones más granulares (de 1 a 9 con incrementos de 0.5) - const options = []; - for (let i = 1; i <= 9; i += 0.5) { - options.push(i); - } - - const roundedValue = Math.round(currentValue * 2) / 2; // Redondea a múltiplos de 0.5 - const riskColor = getRiskColorCSS(roundedValue); - const riskLabel = getRiskLabel(roundedValue); - - return ( -
- {/* Valor numérico grande */} -
- {roundedValue.toFixed(1)} -
- - {/* Label con color de fondo */} -
- {riskLabel} -
- - {/* Select invisible superpuesto */} - - - {/* Icono de dropdown */} -
- ▼ -
-
- ); - }; - - // Función para mostrar riesgo inherente con el mismo diseño que residual pero sin edición - const createInherentRiskDisplay = (riskValue) => { - const roundedValue = Math.round(riskValue * 2) / 2; // Redondea a múltiplos de 0.5 - const riskColor = getRiskColorCSS(roundedValue); - const riskLabel = getRiskLabel(roundedValue); - - return ( -
- {/* Valor numérico grande */} -
- {roundedValue.toFixed(1)} -
- - {/* Label con color de fondo */} -
- {riskLabel} -
-
- ); - }; - - // Función para mostrar riesgo actual con el mismo diseño que residual pero sin edición - const createCurrentRiskDisplay = (riskValue) => { - const roundedValue = Math.round(riskValue * 2) / 2; // Redondea a múltiplos de 0.5 - const riskColor = getRiskColorCSS(roundedValue); - const riskLabel = getRiskLabel(roundedValue); - - return ( -
- {/* Valor numérico grande */} -
- {roundedValue.toFixed(1)} -
- - {/* Label con color de fondo */} -
- {riskLabel} -
-
- ); - }; - - // Función helper para crear selectores OWASP optimizados + // Helper function to create optimized OWASP selectors const createOwaspSelector = (threat, fieldName, labelKey) => { const options = getOwaspSelectOptions(fieldName, locale); return ( @@ -444,31 +166,24 @@ const Analysis = () => { ); }; - // Función para alternar secciones - const toggleSection = (section) => { - setExpandedSections(prev => ({ - ...prev, - [section]: !prev[section] - })); - }; - - // Función helper para mostrar notificaciones + // Function to toggle sections + // Helper function to show notifications const showNotification = (title, description, status = 'info') => { toast({ title, description, status, // 'success', 'error', 'warning', 'info' - duration: status === 'error' ? 6000 : 4000, // Más tiempo para errores + duration: status === 'error' ? 6000 : 4000, // More time for errors isClosable: true, position: 'top-right', - variant: 'left-accent', // Estilo más elegante + variant: 'left-accent', // More elegant style containerStyle: { maxWidth: '400px' } }); }; - // Función para actualizar un campo específico de riesgo de amenaza + // Function to update a specific threat risk field const updateThreatRisk = (threatId, fieldName, value) => { // Actualizar el campo en la amenaza setThreats(prevThreats => @@ -481,7 +196,7 @@ const Analysis = () => { const newInherentRisk = calculateInherentRisk(updatedRisk); setInherentRisks(prev => ({ ...prev, - [threatId]: newInherentRisk + [threatId]: parseFloat(newInherentRisk.toFixed(1)) })); return updatedThreat; @@ -490,7 +205,7 @@ const Analysis = () => { }) ); - // También actualizar en serviceData + // Also update in serviceData setServiceData(prevData => ({ ...prevData, threats: prevData.threats.map(threat => { @@ -521,16 +236,16 @@ const Analysis = () => { console.log(`Processing threat ${threat.id}, risk.residual_risk:`, threat.risk?.residual_risk); const inherentRiskValue = calculateInherentRisk(threat.risk); - initialInherentRisks[threat.id] = inherentRiskValue; + initialInherentRisks[threat.id] = parseFloat(inherentRiskValue.toFixed(1)); // Usar el valor guardado del backend si existe, sino usar riesgo inherente como inicial let residualRiskValue; if (threat.risk && threat.risk.residual_risk !== null && threat.risk.residual_risk !== undefined) { - // Usar el valor guardado del backend - residualRiskValue = threat.risk.residual_risk; + // Usar el valor guardado del backend (asegurar 1 decimal) + residualRiskValue = parseFloat(parseFloat(threat.risk.residual_risk).toFixed(1)); } else { // Si no hay valor guardado, usar el riesgo inherente como valor inicial - residualRiskValue = inherentRiskValue; + residualRiskValue = parseFloat(inherentRiskValue.toFixed(1)); } initialResidualRisks[threat.id] = residualRiskValue; @@ -542,91 +257,68 @@ const Analysis = () => { setIsLoading(false); }; - // Función para actualizar el riesgo inherente cuando cambian los valores OWASP - const updateInherentRisk = async (threatId) => { - // Threat Agent Factors - const skill_level = Number(document.getElementById(`skill_level-${threatId}`)?.value || 0); - const motive = Number(document.getElementById(`motive-${threatId}`)?.value || 0); - const opportunity = Number(document.getElementById(`opportunity-${threatId}`)?.value || 0); - const size = Number(document.getElementById(`size-${threatId}`)?.value || 0); + // Function to update inherent risk when OWASP values change + const updateInherentRisk = (threatId, factorName, newValue) => { + console.log(`updateInherentRisk called for threat ${threatId}, factor ${factorName}, new value ${newValue}`); - // Vulnerability Factors - const ease_of_discovery = Number(document.getElementById(`ease_of_discovery-${threatId}`)?.value || 0); - const ease_of_exploit = Number(document.getElementById(`ease_of_exploit-${threatId}`)?.value || 0); - const awareness = Number(document.getElementById(`awareness-${threatId}`)?.value || 0); - const intrusion_detection = Number(document.getElementById(`intrusion_detection-${threatId}`)?.value || 0); - - // Technical Impact - const loss_of_confidentiality = Number(document.getElementById(`loss_of_confidentiality-${threatId}`)?.value || 0); - const loss_of_integrity = Number(document.getElementById(`loss_of_integrity-${threatId}`)?.value || 0); - const loss_of_availability = Number(document.getElementById(`loss_of_availability-${threatId}`)?.value || 0); - const loss_of_accountability = Number(document.getElementById(`loss_of_accountability-${threatId}`)?.value || 0); - - // Business Impact - const financial_damage = Number(document.getElementById(`financial_damage-${threatId}`)?.value || 0); - const reputation_damage = Number(document.getElementById(`reputation_damage-${threatId}`)?.value || 0); - const non_compliance = Number(document.getElementById(`non_compliance-${threatId}`)?.value || 0); - const privacy_violation = Number(document.getElementById(`privacy_violation-${threatId}`)?.value || 0); - - const likelihoodFactors = [skill_level, motive, opportunity, size, ease_of_discovery, ease_of_exploit, awareness, intrusion_detection]; - const impactFactors = [loss_of_confidentiality, loss_of_integrity, loss_of_availability, loss_of_accountability, financial_damage, reputation_damage, non_compliance, privacy_violation]; - - const likelihood = likelihoodFactors.reduce((acc, val) => acc + val, 0) / likelihoodFactors.length; - const impact = impactFactors.reduce((acc, val) => acc + val, 0) / impactFactors.length; - const overallRisk = (likelihood + impact) / 2; - - setInherentRisks(prev => ({ - ...prev, - [threatId]: overallRisk - })); - - // Encontrar el threat para obtener el estado de remediación - const currentThreat = threats.find(threat => threat.id === threatId); - if (currentThreat) { - // No actualizar automáticamente el riesgo residual, mantener el valor manual del usuario - // El riesgo residual ahora se maneja completamente de forma manual - } - - // Actualizar el riesgo en el backend - const riskData = { - skill_level, - motive, - opportunity, - size, - ease_of_discovery, - ease_of_exploit, - awareness, - intrusion_detection, - loss_of_confidentiality, - loss_of_integrity, - loss_of_availability, - loss_of_accountability, - financial_damage, - reputation_damage, - non_compliance, - privacy_violation - }; - - try { - await updateThreatRisk(threatId, riskData); - - // Actualizar el estado local de las amenazas - setThreats(prevThreats => - prevThreats.map(threat => - threat.id === threatId - ? { - ...threat, - risk: { ...threat.risk, ...riskData } - } - : threat - ) - ); - } catch (error) { - console.error('Error updating risk:', error); - } + // Actualizar el threat en el estado con el nuevo valor + setThreats(prevThreats => { + const updatedThreats = prevThreats.map(threat => { + if (threat.id === threatId) { + const updatedRisk = { + ...threat.risk, + [factorName]: newValue + }; + + // Calcular el nuevo riesgo inherente con los valores actualizados + const likelihoodFactors = [ + updatedRisk.skill_level || 0, + updatedRisk.motive || 0, + updatedRisk.opportunity || 0, + updatedRisk.size || 0, + updatedRisk.ease_of_discovery || 0, + updatedRisk.ease_of_exploit || 0, + updatedRisk.awareness || 0, + updatedRisk.intrusion_detection || 0 + ]; + + const impactFactors = [ + updatedRisk.loss_of_confidentiality || 0, + updatedRisk.loss_of_integrity || 0, + updatedRisk.loss_of_availability || 0, + updatedRisk.loss_of_accountability || 0, + updatedRisk.financial_damage || 0, + updatedRisk.reputation_damage || 0, + updatedRisk.non_compliance || 0, + updatedRisk.privacy_violation || 0 + ]; + + const likelihood = likelihoodFactors.reduce((acc, val) => acc + val, 0) / likelihoodFactors.length; + const impact = impactFactors.reduce((acc, val) => acc + val, 0) / impactFactors.length; + const overallRisk = (likelihood + impact) / 2; + + console.log(`Calculated risk for threat ${threatId}: Likelihood=${likelihood.toFixed(3)}, Impact=${impact.toFixed(3)}, Overall=${overallRisk.toFixed(3)}`); + + // Actualizar el riesgo inherente inmediatamente + setInherentRisks(prev => ({ + ...prev, + [threatId]: parseFloat(overallRisk.toFixed(1)) + })); + + return { + ...threat, + risk: updatedRisk + }; + } + return threat; + }); + + console.log(`Updated threats state for threat ${threatId}`); + return updatedThreats; + }); }; - // Función para actualizar el estado de la remediación + // Function to update remediation status const updateRemediationStatus = (threatId, status) => { console.log(`Actualizando remediación para threat ${threatId}: ${status ? 'aplicada' : 'removida'}`); @@ -642,7 +334,7 @@ const Analysis = () => { ) ); - // También actualizar el estado de serviceData para mantener consistencia + // Also update serviceData state to maintain consistency setServiceData(prevData => ({ ...prevData, threats: prevData.threats.map(threat => @@ -656,14 +348,14 @@ const Analysis = () => { })); // El riesgo residual ahora se maneja completamente de forma manual - // No se actualiza automáticamente cuando cambia el estado de remediación + // Does not update automatically when remediation status changes console.log(`Remediación ${status ? 'aplicada' : 'removida'} para threat ${threatId}. Riesgo residual se mantiene manual.`); - // Forzar re-render al actualizar el estado (esto debería triggerear getCurrentRiskValue) + // Force re-render by updating state (this should trigger getCurrentRiskValue) console.log('Estados actualizados, debería re-renderizar getCurrentRiskValue'); }; - // Función para generar informe PDF (usando el componente ReportGenerator) + // Function to generate PDF report (using ReportGenerator component) const generateReport = async () => { await reportGenerator.generateReport( serviceData, @@ -679,6 +371,7 @@ const Analysis = () => { useEffect(() => { fetchData(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [id, locale]); if (isLoading || !serviceData){ @@ -786,7 +479,7 @@ const Analysis = () => { - {/* Toggle para ocultar/mostrar evaluación del riesgo (solo en vista detallada) */} + {/* Toggle to hide/show risk assessment (only in detailed view) */} {viewMode === 'detailed' && ( @@ -836,27 +529,36 @@ const Analysis = () => { {t?.ui?.inherent_risk || 'IR'}: - - {getRiskValue(threat.id).toFixed(1)} - + {t?.ui?.residual_risk || 'RR'}: - - {getResidualRiskValue(threat.id).toFixed(1)} - + {t?.ui?.current_risk || 'CR'}: - - {getCurrentRiskValue(threat.id).toFixed(1)} - + @@ -930,15 +632,27 @@ const Analysis = () => { {threat.title} - - IR: {getRiskValue(threat.id).toFixed(1)} - - - RR: {getResidualRiskValue(threat.id).toFixed(1)} - - - CR: {getCurrentRiskValue(threat.id).toFixed(1)} - + + + @@ -1072,19 +786,24 @@ const Analysis = () => { {t?.ui?.inherent_risk || 'Inherent Risk'}: - {createInherentRiskDisplay(getRiskValue(threat.id))} + {t?.ui?.residual_risk || 'Residual Risk'}: - {createResidualRiskSelector(threat.id, getResidualRiskValue(threat.id))} + {t?.ui?.current_risk || 'Current Risk'}: - {createCurrentRiskDisplay(getCurrentRiskValue(threat.id))} + @@ -1101,7 +820,7 @@ const Analysis = () => { - {/* Fila de título principal */} + {/* Main title row */} @@ -1116,7 +835,7 @@ const Analysis = () => { - {/* Fila de categorías padre */} + {/* Parent categories row */} {showRiskAssessment && ( <> @@ -1127,7 +846,7 @@ const Analysis = () => { )} - {/* Fila de parámetros específicos */} + {/* Specific parameters row */} {showRiskAssessment && ( <> @@ -1384,7 +1103,7 @@ const Analysis = () => { color: getRiskColorCSS(getRiskValue(threat.id)), fontSize: "16px" }}> - {getRiskValue(threat.id).toFixed(2)} + {getRiskValue(threat.id).toFixed(1)} { borderRadius: "8px", textAlign: "center" }}> - {getRiskLabel(getRiskValue(threat.id))} + {getRiskLabel(getRiskValue(threat.id), t)} @@ -1463,7 +1187,7 @@ const Analysis = () => { return { ...prev, threats: newThreats }; }); - // También actualizar el estado threats independiente + // Also update independent threats state setThreats(prev => prev.filter(t => t.id !== threat.id)); // Eliminar el riesgo inherente del threat eliminado @@ -1473,7 +1197,7 @@ const Analysis = () => { return updated; }); - // Mostrar notificación de eliminación + // Show deletion notification showNotification( t?.ui?.threat_deleted_title || "Threat Marked for Deletion", t?.ui?.threat_deleted || `"${threatTitle}" will be deleted when you save changes`, @@ -1531,13 +1255,13 @@ const Analysis = () => { threats: [...prev.threats, createdThreat] })); - // También actualizar el estado threats independiente + // Also update independent threats state setThreats(prev => [...prev, createdThreat]); // Inicializar el riesgo inherente para el nuevo threat setInherentRisks(prev => ({ ...prev, - [createdThreat.id]: calculateInherentRisk(createdThreat.risk) + [createdThreat.id]: parseFloat(calculateInherentRisk(createdThreat.risk).toFixed(1)) })); showNotification( @@ -1646,7 +1370,7 @@ const Analysis = () => { >{t?.ui?.save_all || 'Save All'} - {/* Modal para mostrar la imagen en tamaño completo */} + {/* Modal to show image in full size */} diff --git a/frontend/src/components/OwaspSelector.jsx b/frontend/src/components/OwaspSelector.jsx index 1f1683d..f415a66 100644 --- a/frontend/src/components/OwaspSelector.jsx +++ b/frontend/src/components/OwaspSelector.jsx @@ -12,8 +12,10 @@ const OwaspSelector = ({ const options = getOwaspSelectOptions(factorName, locale) || []; const handleChange = (event) => { + const newValue = event.target.value; if (onChange) { - onChange(threatId); + // Pass threatId, new value and factor name + onChange(threatId, factorName, parseInt(newValue)); } }; diff --git a/frontend/src/components/ResidualRiskSelector.jsx b/frontend/src/components/ResidualRiskSelector.jsx new file mode 100644 index 0000000..97c9d9a --- /dev/null +++ b/frontend/src/components/ResidualRiskSelector.jsx @@ -0,0 +1,115 @@ +import React from 'react'; +import { getRiskColorCSS, getRiskLabel } from '../utils/riskCalculations'; +import { useLocalization } from '../hooks/useLocalization'; + +/** + * ResidualRiskSelector - Component that combines risk display with editable selector + */ +const ResidualRiskSelector = ({ + threatId, + currentValue, + onUpdate, + size = 'md' +}) => { + const { t } = useLocalization(); + + // Generate options from 0.5 to 9 in 0.5 increments + const options = []; + for (let i = 0.5; i <= 9; i += 0.5) { + options.push(i); + } + + const roundedValue = Math.round(currentValue * 2) / 2; + const riskColor = getRiskColorCSS(roundedValue); + const riskLabel = getRiskLabel(roundedValue, t); + + const riskDisplayStyles = { + container: { + position: 'relative', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: '2px', + width: '100%', + cursor: 'pointer' + }, + numberValue: { + fontSize: size === 'sm' ? '18px' : '24px', + fontWeight: 'bold', + lineHeight: '1.2', + marginBottom: '4px' + }, + riskLabel: { + fontSize: '10px', + fontWeight: 'bold', + color: 'white', + padding: '2px 8px', + borderRadius: '12px', + textTransform: 'uppercase', + letterSpacing: '0.5px' + }, + invisibleSelect: { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + opacity: 0, + cursor: 'pointer', + zIndex: 10, + appearance: 'none', + background: 'transparent', + border: 'none' + }, + dropdownIcon: { + position: 'absolute', + top: '2px', + right: '2px', + fontSize: '8px', + pointerEvents: 'none', + zIndex: 5 + } + }; + + return ( +
+ {/* Visual Risk Display */} +
+ {roundedValue.toFixed(1)} +
+ +
+ {riskLabel} +
+ + {/* Invisible Select Overlay */} + + + {/* Dropdown Icon */} +
+ ▼ +
+
+ ); +}; + +export default ResidualRiskSelector; diff --git a/frontend/src/components/RiskDisplay.jsx b/frontend/src/components/RiskDisplay.jsx new file mode 100644 index 0000000..f94dc41 --- /dev/null +++ b/frontend/src/components/RiskDisplay.jsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { Badge, VStack, Text, Box } from '@chakra-ui/react'; +import { getRiskColorScheme, getRiskColorCSS, getRiskLabel } from '../utils/riskCalculations'; +import { useLocalization } from '../hooks/useLocalization'; + +/** + * Component to display risk values consistently across the application + */ +const RiskDisplay = ({ + riskValue = 0, + label = 'Risk', + size = 'md', + showLabel = true, + variant = 'badge', // 'badge' or 'box' + prefix = null // For compact display like 'IR: 4.5' +}) => { + const { t } = useLocalization(); + + const formattedValue = parseFloat(riskValue || 0).toFixed(1); + const colorScheme = getRiskColorScheme(riskValue); + const riskLevelLabel = getRiskLabel(riskValue, t); + + if (variant === 'box') { + const riskDisplayStyles = { + container: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '8px', + borderRadius: '8px', + minWidth: '80px', + backgroundColor: '#f7fafc' + }, + numberValue: { + fontSize: size === 'sm' ? '18px' : '24px', + fontWeight: 'bold', + lineHeight: '1.2', + marginBottom: '4px' + }, + riskLabel: { + fontSize: '10px', + fontWeight: 'bold', + color: 'white', + padding: '2px 8px', + borderRadius: '12px', + textTransform: 'uppercase', + letterSpacing: '0.5px' + } + }; + + const roundedValue = Math.round(riskValue * 2) / 2; + const riskColor = getRiskColorCSS(roundedValue); + + return ( +
+ {showLabel && ( + + {label} + + )} +
+ {formattedValue} +
+
+ {riskLevelLabel} +
+
+ ); + } + + return ( + + {showLabel && !prefix && ( + + {label} + + )} + + {prefix ? `${prefix}: ${formattedValue}` : `${formattedValue} (${riskLevelLabel})`} + + + ); +}; + +export default RiskDisplay; diff --git a/frontend/src/components/ThreatCard.jsx b/frontend/src/components/ThreatCard.jsx new file mode 100644 index 0000000..e75e215 --- /dev/null +++ b/frontend/src/components/ThreatCard.jsx @@ -0,0 +1,236 @@ +import React from 'react'; +import { + Box, + VStack, + HStack, + Text, + Button, + Collapse, + Textarea, + useColorModeValue, + Flex, +} from '@chakra-ui/react'; +import { FaTrash, FaEdit, FaEye, FaEyeSlash } from 'react-icons/fa'; +import RiskDisplay from './RiskDisplay'; +import OwaspSelector from './OwaspSelector'; +import { calculateTextareaHeight } from '../utils/textareaHelpers'; +import { useLocalization } from '../hooks/useLocalization'; + +/** + * ThreatCard component - Individual threat card with OWASP risk assessment + */ +const ThreatCard = ({ + threat, + inherentRisk, + residualRisk, + currentRisk, + isEditing, + isCollapsed, + remediationValue, + onOwaspChange, + onToggleEdit, + onToggleCollapse, + onRemediationChange, + onThreatUpdate, + onThreatDelete, + renderTypeBadges, + createRemediationSwitch +}) => { + const { t } = useLocalization(); + const cardBg = useColorModeValue('white', 'gray.800'); + const borderColor = useColorModeValue('gray.200', 'gray.600'); + + return ( + + {/* Header */} + + + + + {threat.title} + + {threat.type && renderTypeBadges(threat.type)} + + + ID: {threat.id} + + + + + + + + + + + {/* Risk Summary */} + + + + + {t?.ui?.inherent_risk || 'IR'}: + + + + + + + {t?.ui?.residual_risk || 'RR'}: + + + + + + + {t?.ui?.current_risk || 'CR'}: + + + + + + + + {t?.ui?.remediation || 'Remediation'}: + + {createRemediationSwitch(threat, "sm")} + + + + {/* Collapsible Content */} + + + + {/* Description */} + + + {t?.ui?.description || 'Description'}: + + {isEditing ? ( +
{t?.ui?.title || 'Title'} {t?.ui?.type || 'Type'}{t?.ui?.applied || 'Remediada'} {t?.ui?.delete || 'Delete'}
- {createResidualRiskSelector(threat.id, getResidualRiskValue(threat.id))} +
{ borderRadius: '8px', textAlign: "center" }}> - {getRiskLabel(getCurrentRiskValue(threat.id))} + {getRiskLabel(getCurrentRiskValue(threat.id), t)}