diff --git a/ui/src/pages/CreateMatrix.tsx b/ui/src/pages/CreateMatrix.tsx index 33e0645..0ad0170 100644 --- a/ui/src/pages/CreateMatrix.tsx +++ b/ui/src/pages/CreateMatrix.tsx @@ -27,10 +27,8 @@ import DeleteIcon from "@mui/icons-material/Delete"; import { whoamiUpsert, createMatrix } from "./apiService"; import SendIcon from "@mui/icons-material/Send"; import { useNavigate } from "react-router-dom"; -import GroupsIcon from '@mui/icons-material/Groups'; -import PlaceIcon from '@mui/icons-material/Place'; - - +import GroupsIcon from "@mui/icons-material/Groups"; +import PlaceIcon from "@mui/icons-material/Place"; export const CreateMatrix: React.FC = () => { const [RoleBasedChecked, setRoleBasedChecked] = useState(true); @@ -44,7 +42,8 @@ export const CreateMatrix: React.FC = () => { const [snackbarErrorOpen, setSnackbarErrorOpen] = useState(false); const [titleError, setTitleError] = useState(false); const [snackbarTargetErrorOpen, setSnackbarTargetErrorOpen] = useState(false); - const [snackbarParticipantErrorOpen, setSnackbarParticipantErrorOpen] = useState(false); + const [snackbarParticipantErrorOpen, setSnackbarParticipantErrorOpen] = + useState(false); const [participantsData, setParticipantsData] = useState< { email: string; role: string }[] >([]); @@ -69,20 +68,29 @@ export const CreateMatrix: React.FC = () => { { key: "Recognizability", header: "R" }, ] as const; - const carverTooltips: Record = { - Criticality: "The primary measure of target value and importance. Higher values indicate greater significance to the system or organization.", - Accessibility: "The ease of reaching and accessing the target. Higher values suggest easier access with fewer security measures.", - Recoverability: "The time and resources needed to restore functionality after an incident. Higher values mean longer recovery times.", - Vulnerability: "The susceptibility to attack or disruption. Higher values indicate greater weaknesses or vulnerabilities.", - Effect: "The immediate impact of a successful attack. Higher values represent more severe immediate consequences.", - Recognizability: "How easily the target can be identified. Higher values mean the target is more recognizable and requires less preparation to identify.", + const carverTooltips: Record<(typeof carverOrder)[number]["key"], string> = { + Criticality: + "The primary measure of target value and importance. Higher values indicate greater significance to the system or organization.", + Accessibility: + "The ease of reaching and accessing the target. Higher values suggest easier access with fewer security measures.", + Recoverability: + "The time and resources needed to restore functionality after an incident. Higher values mean longer recovery times.", + Vulnerability: + "The susceptibility to attack or disruption. Higher values indicate greater weaknesses or vulnerabilities.", + Effect: + "The immediate impact of a successful attack. Higher values represent more severe immediate consequences.", + Recognizability: + "How easily the target can be identified. Higher values mean the target is more recognizable and requires less preparation to identify.", }; // Participant management const handleAddParticipant = () => { setParticipantsData([ ...participantsData, - { email: "", role: RoleBasedChecked ? "Participant" : "Host and Participant" }, + { + email: "", + role: RoleBasedChecked ? "Participant" : "Host and Participant", + }, ]); }; @@ -146,13 +154,15 @@ export const CreateMatrix: React.FC = () => { ) => { const newValue = event.target.checked; setRoleBasedChecked(newValue); - + // Update all existing participants to "Host and Participant" when disabling role restrictions if (!newValue) { - setParticipantsData(participantsData.map(participant => ({ - ...participant, - role: "Host and Participant" - }))); + setParticipantsData( + participantsData.map((participant) => ({ + ...participant, + role: "Host and Participant", + })) + ); } }; @@ -185,7 +195,7 @@ export const CreateMatrix: React.FC = () => { // Check for at least one participant (either Participant or Host and Participant role) const participantCount = participantsData.filter( - p => p.role === "Participant" || p.role === "Host and Participant" + (p) => p.role === "Participant" || p.role === "Host and Participant" ).length; if (participantCount === 0) { @@ -207,21 +217,25 @@ export const CreateMatrix: React.FC = () => { recoverability: {}, vulnerability: {}, effect: {}, - recognizability: {} + recognizability: {}, })); const hosts = [ email, ...participantsData .map((p) => - p.role === "Host" || p.role === "Host and Participant" ? p.email : null + p.role === "Host" || p.role === "Host and Participant" + ? p.email + : null ) .filter((email): email is string => email !== null), ]; const participantEmails = participantsData .map((p) => - p.role === "Participant" || p.role === "Host and Participant" ? p.email : null + p.role === "Participant" || p.role === "Host and Participant" + ? p.email + : null ) .filter((email): email is string => email !== null); @@ -241,17 +255,14 @@ export const CreateMatrix: React.FC = () => { fivePointScoring: value === 5 ? true : false, items: items, }; - if (targets.length != 0) - { + if (targets.length != 0) { const response = await createMatrix(matrixData, userId); console.log("Matrix Created:", response.data); setSnackbarOpen(true); setTimeout(() => { navigate("/ViewMatrix"); }, 1500); - } - else - { + } else { setSnackbarOpen(false); setSnackbarTargetErrorOpen(true); } @@ -280,7 +291,8 @@ export const CreateMatrix: React.FC = () => { left: 0, right: 0, bottom: 0, - backgroundImage: "linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), url('/military-pattern.svg')", + backgroundImage: + "linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), url('/military-pattern.svg')", backgroundSize: "100px 100px", backgroundPosition: "center", opacity: 0.1, @@ -335,18 +347,18 @@ export const CreateMatrix: React.FC = () => { disableUnderline: false, style: { fontSize: "3rem", color: "#ffffff" }, }} - sx={{ + sx={{ flexGrow: 1, - '& .MuiInput-root': { - color: '#ffffff', - '&:before': { - borderBottomColor: 'rgba(255, 255, 255, 0.23)', + "& .MuiInput-root": { + color: "#ffffff", + "&:before": { + borderBottomColor: "rgba(255, 255, 255, 0.23)", }, - '&:hover:before': { - borderBottomColor: '#014093', + "&:hover:before": { + borderBottomColor: "#014093", }, - '&.Mui-focused:before': { - borderBottomColor: '#014093', + "&.Mui-focused:before": { + borderBottomColor: "#014093", }, }, }} @@ -358,8 +370,8 @@ export const CreateMatrix: React.FC = () => { variant="contained" onClick={handleCreateMatrix} endIcon={} - sx={{ - borderRadius: "20px", + sx={{ + borderRadius: "20px", width: "140px", height: "55px", backgroundColor: "#014093", @@ -368,11 +380,11 @@ export const CreateMatrix: React.FC = () => { fontWeight: "bold", letterSpacing: "0.5px", fontSize: "0.9rem", - boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)', + boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)", flexShrink: 0, - '&:hover': { + "&:hover": { backgroundColor: "#012B61", - boxShadow: '0 6px 8px rgba(0, 0, 0, 0.2)' + boxShadow: "0 6px 8px rgba(0, 0, 0, 0.2)", }, }} > @@ -424,26 +436,26 @@ export const CreateMatrix: React.FC = () => { variant="outlined" fullWidth placeholder="Enter Matrix description..." - sx={{ - '& .MuiOutlinedInput-root': { - color: '#ffffff', - '& fieldset': { - borderColor: 'rgba(255, 255, 255, 0.23)', + sx={{ + "& .MuiOutlinedInput-root": { + color: "#ffffff", + "& fieldset": { + borderColor: "rgba(255, 255, 255, 0.23)", }, - '&:hover fieldset': { - borderColor: '#ffffff', + "&:hover fieldset": { + borderColor: "#ffffff", }, - '&.Mui-focused fieldset': { - borderColor: '#014093', + "&.Mui-focused fieldset": { + borderColor: "#014093", }, - '& textarea': { - fontSize: '0.95rem', - lineHeight: '1.5', + "& textarea": { + fontSize: "0.95rem", + lineHeight: "1.5", }, }, - '& .MuiInputBase-input::placeholder': { - color: 'rgba(255, 255, 255, 0.5)', - fontSize: '0.95rem', + "& .MuiInputBase-input::placeholder": { + color: "rgba(255, 255, 255, 0.5)", + fontSize: "0.95rem", }, mb: 2, }} @@ -458,10 +470,13 @@ export const CreateMatrix: React.FC = () => { marginTop: "20px", }} > - - + { > - - + Score Range @@ -502,11 +523,11 @@ export const CreateMatrix: React.FC = () => { borderRadius: "20px", mb: 1, backgroundColor: "rgba(255, 255, 255, 0.05)", - '&:hover': { - borderColor: '#014093', + "&:hover": { + borderColor: "#014093", }, - '&.Mui-focused': { - borderColor: '#014093', + "&.Mui-focused": { + borderColor: "#014093", }, }} > @@ -514,8 +535,15 @@ export const CreateMatrix: React.FC = () => { 10-Point Scoring - - Enforce Role Restrictions + + + Enforce Role Restrictions + { sx={{ "& .MuiSwitch-track": { borderRadius: "20px", - backgroundColor: RoleBasedChecked ? "#014093" : "rgba(255, 255, 255, 0.23)", + backgroundColor: RoleBasedChecked + ? "#014093" + : "rgba(255, 255, 255, 0.23)", opacity: 1, }, "& .MuiSwitch-thumb": { @@ -544,38 +574,46 @@ export const CreateMatrix: React.FC = () => { - - + Random Target Distribution - } - label={ - - {dataEntryMethod ? "On" : "Off"} - - } - sx={{ mb: 1 }} - /> + control={ + + } + label={ + + {dataEntryMethod ? "On" : "Off"} + + } + sx={{ mb: 1 }} + /> @@ -587,10 +625,13 @@ export const CreateMatrix: React.FC = () => { mt: 4, }} > - - + { - - + + {item.header} @@ -634,8 +680,8 @@ export const CreateMatrix: React.FC = () => { {carverOrder.map((item) => ( - { > )} - handleDeleteParticipant(index)} sx={{ color: "rgba(255, 255, 255, 0.5)", padding: "6px", transition: "all 0.2s ease", - '&:hover': { + "&:hover": { color: "#ff4444", backgroundColor: "rgba(255, 68, 68, 0.08)", }, @@ -1000,77 +1073,81 @@ export const CreateMatrix: React.FC = () => { open={snackbarOpen} autoHideDuration={3000} onClose={() => setSnackbarOpen(false)} - anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }} + anchorOrigin={{ vertical: "bottom", horizontal: "left" }} sx={{ - '& .MuiPaper-root': { - width: '100%', - maxWidth: '400px', - } + "& .MuiPaper-root": { + width: "100%", + maxWidth: "400px", + }, }} > - - - + + Matrix Created Successfully! - Redirecting to Matrix Explorer - + @@ -1087,16 +1164,16 @@ export const CreateMatrix: React.FC = () => { onClose={() => setSnackbarErrorOpen(false)} severity="error" variant="filled" - sx={{ + sx={{ width: "100%", - backgroundColor: '#7C0B02', - color: '#ffffff', - '& .MuiAlert-icon': { - color: '#ffffff' + backgroundColor: "#7C0B02", + color: "#ffffff", + "& .MuiAlert-icon": { + color: "#ffffff", }, - boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)', - border: '1px solid rgba(255, 255, 255, 0.1)', - backdropFilter: 'blur(10px)', + boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)", + border: "1px solid rgba(255, 255, 255, 0.1)", + backdropFilter: "blur(10px)", }} > You cannot have duplicate target names @@ -1112,16 +1189,16 @@ export const CreateMatrix: React.FC = () => { onClose={() => setSnackbarTargetErrorOpen(false)} severity="error" variant="filled" - sx={{ + sx={{ width: "100%", - backgroundColor: '#7C0B02', - color: '#ffffff', - '& .MuiAlert-icon': { - color: '#ffffff' + backgroundColor: "#7C0B02", + color: "#ffffff", + "& .MuiAlert-icon": { + color: "#ffffff", }, - boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)', - border: '1px solid rgba(255, 255, 255, 0.1)', - backdropFilter: 'blur(10px)', + boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)", + border: "1px solid rgba(255, 255, 255, 0.1)", + backdropFilter: "blur(10px)", }} > Must have at least one target @@ -1137,16 +1214,16 @@ export const CreateMatrix: React.FC = () => { onClose={() => setSnackbarParticipantErrorOpen(false)} severity="error" variant="filled" - sx={{ + sx={{ width: "100%", - backgroundColor: '#7C0B02', - color: '#ffffff', - '& .MuiAlert-icon': { - color: '#ffffff' + backgroundColor: "#7C0B02", + color: "#ffffff", + "& .MuiAlert-icon": { + color: "#ffffff", }, - boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)', - border: '1px solid rgba(255, 255, 255, 0.1)', - backdropFilter: 'blur(10px)', + boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)", + border: "1px solid rgba(255, 255, 255, 0.1)", + backdropFilter: "blur(10px)", }} > Must have at least one user with Participant role @@ -1157,4 +1234,3 @@ export const CreateMatrix: React.FC = () => { }; export default CreateMatrix; - diff --git a/ui/src/pages/ViewMatrix.tsx b/ui/src/pages/ViewMatrix.tsx index 704a719..2e3aaf8 100644 --- a/ui/src/pages/ViewMatrix.tsx +++ b/ui/src/pages/ViewMatrix.tsx @@ -18,11 +18,16 @@ import { import { useNavigate } from "react-router-dom"; import axios from "axios"; import { ROUTES } from "../helpers/helpers"; -import SearchIcon from '@mui/icons-material/Search'; -import AddIcon from '@mui/icons-material/Add'; -import FilterListIcon from '@mui/icons-material/FilterList'; -import OpenInNewIcon from '@mui/icons-material/OpenInNew'; -import { ExportPdfButton } from '../components/custom/pdfExport/ExportPdfButton'; +import SearchIcon from "@mui/icons-material/Search"; +import AddIcon from "@mui/icons-material/Add"; +import FilterListIcon from "@mui/icons-material/FilterList"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; +import DeleteIcon from "@mui/icons-material/Delete"; +import Dialog from "@mui/material/Dialog"; +import DialogTitle from "@mui/material/DialogTitle"; +import DialogContent from "@mui/material/DialogContent"; +import DialogActions from "@mui/material/DialogActions"; +import { ExportPdfButton } from "../components/custom/pdfExport/ExportPdfButton"; interface CarverMatrix { matrixId: number; @@ -61,19 +66,27 @@ const ViewMatrix: React.FC = () => { participant: false, both: false, }); - const [roleBasedFilter, setRoleBasedFilter] = useState<'all' | 'enabled' | 'disabled'>('all'); + const [roleBasedFilter, setRoleBasedFilter] = useState< + "all" | "enabled" | "disabled" + >("all"); const [userEmail, setUserEmail] = useState(null); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [matrixToDelete, setMatrixToDelete] = useState( + null + ); useEffect(() => { const fetchUserEmail = async () => { try { - const response = await axios.get('/api/user2/whoami-upsert', { withCredentials: true }); - if (response.data.includes('{')) { - const userData = JSON.parse(response.data.split('}{')[0] + '}'); + const response = await axios.get("/api/user2/whoami-upsert", { + withCredentials: true, + }); + if (response.data.includes("{")) { + const userData = JSON.parse(response.data.split("}{")[0] + "}"); setUserEmail(userData.email); } } catch (error) { - console.error('Error fetching user email:', error); + console.error("Error fetching user email:", error); } }; @@ -97,9 +110,9 @@ const ViewMatrix: React.FC = () => { }, []); const handleRoleFilterChange = (role: keyof typeof roleFilters) => { - setRoleFilters(prev => ({ + setRoleFilters((prev) => ({ ...prev, - [role]: !prev[role] + [role]: !prev[role], })); }; @@ -107,15 +120,16 @@ const ViewMatrix: React.FC = () => { const filteredMatrices = matrices.filter((matrix) => { // Text search filter const term = searchTerm.toLowerCase(); - const matchesSearch = matrix.name.toLowerCase().includes(term) || + const matchesSearch = + matrix.name.toLowerCase().includes(term) || matrix.description.toLowerCase().includes(term); // Role-based matrix filter - if (roleBasedFilter !== 'all') { - if (roleBasedFilter === 'enabled' && !matrix.roleBased) { + if (roleBasedFilter !== "all") { + if (roleBasedFilter === "enabled" && !matrix.roleBased) { return false; } - if (roleBasedFilter === 'disabled' && matrix.roleBased) { + if (roleBasedFilter === "disabled" && matrix.roleBased) { return false; } } @@ -132,7 +146,7 @@ const ViewMatrix: React.FC = () => { // Role-based filtering if (!userEmail) { - console.log('No user email available'); + console.log("No user email available"); return false; } @@ -156,26 +170,30 @@ const ViewMatrix: React.FC = () => { } } - console.log('Matrix:', matrix.name); - console.log('Current user email:', userEmail); - console.log('Hosts:', matrix.hosts); - console.log('Is host:', isHost); - console.log('Is participant:', isParticipant); - console.log('Is both:', isBoth); - console.log('Role filters:', roleFilters); - console.log('Matches role:', matchesRole); - console.log('Matches search:', matchesSearch); - console.log('Final result:', matchesSearch && matchesRole); + console.log("Matrix:", matrix.name); + console.log("Current user email:", userEmail); + console.log("Hosts:", matrix.hosts); + console.log("Is host:", isHost); + console.log("Is participant:", isParticipant); + console.log("Is both:", isBoth); + console.log("Role filters:", roleFilters); + console.log("Matches role:", matchesRole); + console.log("Matches search:", matchesSearch); + console.log("Final result:", matchesSearch && matchesRole); return matchesSearch && matchesRole; }); - const transformItemsForPdf = (items: CarverMatrix['items']) => { - return items.map(item => { + const transformItemsForPdf = (items: CarverMatrix["items"]) => { + return items.map((item) => { const getAverageScore = (scores: any) => { - if (typeof scores === 'number') return scores; - const values = Object.values(scores || {}).filter(score => score !== undefined && score !== null) as number[]; - return values.length > 0 ? values.reduce((sum, score) => sum + score, 0) / values.length : 0; + if (typeof scores === "number") return scores; + const values = Object.values(scores || {}).filter( + (score) => score !== undefined && score !== null + ) as number[]; + return values.length > 0 + ? values.reduce((sum, score) => sum + score, 0) / values.length + : 0; }; return { @@ -185,13 +203,39 @@ const ViewMatrix: React.FC = () => { recoverability: { default: getAverageScore(item.recoverability) }, vulnerability: { default: getAverageScore(item.vulnerability) }, effect: { default: getAverageScore(item.effect) }, - recognizability: { default: getAverageScore(item.recognizability) } + recognizability: { default: getAverageScore(item.recognizability) }, }; }); }; + const handleOpenDeleteDialog = (matrix: CarverMatrix) => { + setMatrixToDelete(matrix); + setDeleteDialogOpen(true); + }; + + const handleCloseDeleteDialog = () => { + setDeleteDialogOpen(false); + setMatrixToDelete(null); + }; + + const handleConfirmDelete = async () => { + if (!matrixToDelete) return; + + try { + await axios.delete(`/api/carvermatrices/${matrixToDelete.matrixId}`, { + withCredentials: true, + }); + setMatrices((prev) => + prev.filter((m) => m.matrixId !== matrixToDelete.matrixId) + ); + handleCloseDeleteDialog(); + } catch (error) { + console.error("Failed to delete matrix:", error); + } + }; + return ( - { left: 0, right: 0, bottom: 0, - backgroundImage: "linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), url('/military-pattern.svg')", + backgroundImage: + "linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), url('/military-pattern.svg')", backgroundSize: "100px 100px", backgroundPosition: "center", opacity: 0.1, @@ -256,33 +301,35 @@ const ViewMatrix: React.FC = () => { InputProps={{ startAdornment: ( - + ), }} sx={{ - '& .MuiOutlinedInput-root': { - color: '#ffffff', - '& fieldset': { - borderColor: 'rgba(255, 255, 255, 0.23)', + "& .MuiOutlinedInput-root": { + color: "#ffffff", + "& fieldset": { + borderColor: "rgba(255, 255, 255, 0.23)", }, - '&:hover fieldset': { - borderColor: '#014093', + "&:hover fieldset": { + borderColor: "#014093", }, - '&.Mui-focused fieldset': { - borderColor: '#014093', + "&.Mui-focused fieldset": { + borderColor: "#014093", }, }, }} /> - - + + { Filter by Role - + handleRoleFilterChange('host')} + onChange={() => handleRoleFilterChange("host")} sx={{ - color: 'rgba(255, 255, 255, 0.7)', - '&.Mui-checked': { - color: '#014093', + color: "rgba(255, 255, 255, 0.7)", + "&.Mui-checked": { + color: "#014093", }, }} /> } label={ - Host + Host } /> handleRoleFilterChange('participant')} + onChange={() => handleRoleFilterChange("participant")} sx={{ - color: 'rgba(255, 255, 255, 0.7)', - '&.Mui-checked': { - color: '#014093', + color: "rgba(255, 255, 255, 0.7)", + "&.Mui-checked": { + color: "#014093", }, }} /> } label={ - Participant + + Participant + } /> handleRoleFilterChange('both')} + onChange={() => handleRoleFilterChange("both")} sx={{ - color: 'rgba(255, 255, 255, 0.7)', - '&.Mui-checked': { - color: '#014093', + color: "rgba(255, 255, 255, 0.7)", + "&.Mui-checked": { + color: "#014093", }, }} /> } label={ - Host & Participant + + Host & Participant + } /> @@ -349,12 +400,14 @@ const ViewMatrix: React.FC = () => { - - + + { Role-Based Matrix Filter - + setRoleBasedFilter(e.target.value as 'all' | 'enabled' | 'disabled')} + onChange={(e) => + setRoleBasedFilter( + e.target.value as "all" | "enabled" | "disabled" + ) + } sx={{ - '& .MuiRadio-root': { - color: 'rgba(255, 255, 255, 0.7)', - '&.Mui-checked': { - color: '#014093', + "& .MuiRadio-root": { + color: "rgba(255, 255, 255, 0.7)", + "&.Mui-checked": { + color: "#014093", }, }, }} @@ -380,21 +437,27 @@ const ViewMatrix: React.FC = () => { value="all" control={} label={ - All Matrices + + All Matrices + } /> } label={ - Role-Based Only + + Role-Based Only + } /> } label={ - Non-Role-Based Only + + Non-Role-Based Only + } /> @@ -402,21 +465,23 @@ const ViewMatrix: React.FC = () => { - + + + + ); };