diff --git a/app/public/manifest.json b/app/public/manifest.json index 7dd5b7c4..e324eea2 100644 --- a/app/public/manifest.json +++ b/app/public/manifest.json @@ -3,7 +3,7 @@ "name": "Gram - Threat Modelling", "icons": [ { - "src": "icon/gram_logo.svg", + "src": "gram_logo.svg", "sizes": "any" } ], diff --git a/app/src/components/elements/CollapsePaper.js b/app/src/components/elements/CollapsePaper.js new file mode 100644 index 00000000..71c1035e --- /dev/null +++ b/app/src/components/elements/CollapsePaper.js @@ -0,0 +1,60 @@ +import { + KeyboardArrowDownRounded, + KeyboardArrowUpRounded, +} from "@mui/icons-material"; +import { Badge, Box, Collapse, IconButton, Paper } from "@mui/material"; +import { useState } from "react"; + +export function CollapsePaper({ + title, + count, + children, + defaultExpanded = false, + sx, +}) { + const [expanded, setExpanded] = useState(defaultExpanded); + + return ( + + { + if (e.target === e.currentTarget) { + setExpanded(!expanded); + } + }} + > + setExpanded(!expanded)} + sx={{ + alignItems: "center", + gap: "10px", + "& span": { + position: "relative", + transform: "scale(1)", + backgroundColor: "dimgray", + }, + }} + > + {title} + + setExpanded(!expanded)} + > + {expanded ? : } + + + + {children} + + + ); +} diff --git a/app/src/components/elements/ColorSlider.js b/app/src/components/elements/ColorSlider.js index 36cf3ec6..90ed50fd 100644 --- a/app/src/components/elements/ColorSlider.js +++ b/app/src/components/elements/ColorSlider.js @@ -5,7 +5,7 @@ import { useState } from "react"; const baseMarks = [ { value: 0, - color: "green", + color: "grey", }, { value: 1, @@ -25,35 +25,49 @@ const baseMarks = [ }, ]; -export function ColorSlider({ marks, defaultValue, onChange }) { +export function ColorSlider({ + marks, + defaultValue, + onChange, + hideDescription, + ...props +}) { const joinedMarks = marks.map((m, i) => ({ ...(baseMarks.length > i ? baseMarks[i] : {}), ...m, })); - const defaultMark = joinedMarks.find((m) => m.value === defaultValue); + const defaultMark = joinedMarks.find((m) => m.textValue === defaultValue); const [selectedMark, setSelectedMark] = useState(defaultMark); return ( <> joinedMarks[i].label} onChange={(e) => { - setSelectedMark( - joinedMarks.find((m) => m.value === e.target.value) - ); - return onChange(e); + const mark = joinedMarks.find((m) => m.value === e.target.value); + setSelectedMark(mark); + return onChange(mark); + }} + sx={{ + color: selectedMark?.color || "primary", + ".MuiSlider-markLabel": { + fontSize: "9px", + }, + "&.Mui-disabled .MuiSlider-track": { + color: selectedMark?.color || "primary", + }, }} - sx={{ color: selectedMark?.color || "primary" }} + {...props} /> - {selectedMark?.description && ( + {!hideDescription && selectedMark?.description && ( theme.palette.review.text, + }} + variant="outlined" + label={user.name} + icon={ + <> + {user?.slackUrl && ( + + + + )} + {user?.mail && ( + + + + )} + + } + /> + ); +} diff --git a/app/src/components/elements/modal/ModalManager.js b/app/src/components/elements/modal/ModalManager.js index 1a2d1bae..38db7db6 100644 --- a/app/src/components/elements/modal/ModalManager.js +++ b/app/src/components/elements/modal/ModalManager.js @@ -10,7 +10,6 @@ import { ChangeReviewer } from "../../model/modals/ChangeReviewer"; import { Tutorial } from "../../model/tutorial/Tutorial"; import { CancelReview } from "../../reviews/modals/CancelReview"; import { DeclineReview } from "../../reviews/modals/DeclineReview"; -import { ViewActionItems } from "../../model/modals/ViewActionItems"; export const MODALS = { ChangeReviewer, @@ -23,7 +22,6 @@ export const MODALS = { DeleteSelected, DeclineReview, CancelReview, - ViewActionItems, }; export function ModalManager() { diff --git a/app/src/components/model/hooks/useActionItems.js b/app/src/components/model/hooks/useActionItems.js new file mode 100644 index 00000000..10546ea6 --- /dev/null +++ b/app/src/components/model/hooks/useActionItems.js @@ -0,0 +1,20 @@ +import { useListThreatsQuery } from "../../../api/gram/threats"; +import { useModelID } from "./useModelID"; + +export function useActionItems() { + const modelId = useModelID(); + const { data: threats } = useListThreatsQuery({ modelId }); + + const actionItems = threats?.threats + ? Object.keys(threats?.threats) + .map((componentId) => ({ + componentId, + threats: threats?.threats[componentId].filter( + (th) => th.isActionItem + ), + })) + .filter(({ threats }) => threats && threats.length > 0) + : []; + + return actionItems; +} diff --git a/app/src/components/model/modals/ApproveReview.js b/app/src/components/model/modals/ApproveReview.js index 20f99b14..d20782bc 100644 --- a/app/src/components/model/modals/ApproveReview.js +++ b/app/src/components/model/modals/ApproveReview.js @@ -9,6 +9,7 @@ import { DialogContentText, DialogTitle, Divider, + Paper, TextField, Typography, } from "@mui/material"; @@ -20,98 +21,14 @@ import { useGetReviewQuery, } from "../../../api/gram/review"; import { modalActions } from "../../../redux/modalSlice"; -import { ColorSlider } from "../../elements/ColorSlider"; import { LoadingPage } from "../../elements/loading/loading-page/LoadingPage"; import { PERMISSIONS } from "../constants"; -import { ActionItemList } from "./ActionItemList"; - -function LikelihoodSlider({ onChange }) { - const marks = [ - { - label: "Rare", - description: `➢ This will probably never happen/recur -➢ Every 25 years`, - }, - { - label: "Unlikely", - description: `➢ This is not likely to happen/recur but could -➢ Every 10 years`, - }, - { - label: "Occasional", - description: `➢ This is unexpected to happen/recur but is certainly possible to occur -➢ Every 5 years`, - }, - { - label: "Likely", - description: `➢ This will probably happen/recur but is not a persisting issue. -➢ Every 3 years`, - }, - { - label: "Almost certain", - description: `➢ This will undoubtedly happen/recur -➢ Every year`, - }, - ]; - - return ( - <> - onChange(marks[e.target.value])} - /> - - ); -} - -function ImpactSlider({ onChange }) { - const marks = [ - { - label: "Very low", - description: `➢ Users can not interact with the service <1h -➢ No regulatory sanctions/fines`, - }, - { - label: "Low", - description: `➢ Users can not interact with the service <1-4h -➢ Incident reviewed by authorities but dismissed`, - }, - { - label: "Medium", - description: `➢ Users can not interact with the service <4-10h -➢ Incident reviewed by authorities and regulatory warning`, - }, - { - label: "High", - description: `➢ Users can not interact with the service <10-16h -➢ Incident reviewed by authorities and sanctions/fines imposed`, - }, - { - label: "Very high", - description: `➢ Users can not interact with the service >16h -➢ Incident reviewed by authorities and sanctions/fines threaten operations / Loss of licence`, - }, - ]; - - return ( - onChange(marks[e.target.value])} - /> - ); -} +import { ActionItemList } from "../panels/left/ActionItemList"; export function ApproveReview({ modelId }) { const dispatch = useDispatch(); const { data: review } = useGetReviewQuery({ modelId }); - const [extras, setExtras] = useState({ - impact: "Low", - likelihood: "Unlikely", - }); const { data: permissions, isLoading: permissionsIsLoading } = useGetModelPermissionsQuery({ modelId }); @@ -145,44 +62,18 @@ export function ApproveReview({ modelId }) { {(isUninitialized || isLoading) && ( <> - Risk Evaluation - {/* - Every system threat model is connected to a risk ticket. When you - approve this threat model, it will be automatically created for - you (if the model is connected to a system). - */} - {/*
*/} - - Based on the threat model, set the risk value as your estimate of - the overall risk of all threats/controls found in the threat - model. - - -
- - Impact - - setExtras({ ...extras, impact: value.label }) - } - /> - -
- - Likelihood - - setExtras({ ...extras, likelihood: value.label }) - } - /> - -
Action Items - - + + Assess the severity of each threat based on what the impact of a + vulnerability could be and the current level of mitigation against + the threat. + +
+ + +
Summary - Do you have any further recommendations to the owning team? Your notes here will be forwarded via email. @@ -236,7 +127,7 @@ export function ApproveReview({ modelId }) { {(isUninitialized || isLoading) && ( - - - ); -} diff --git a/app/src/components/model/modals/ActionItemList.js b/app/src/components/model/panels/left/ActionItemList.js similarity index 50% rename from app/src/components/model/modals/ActionItemList.js rename to app/src/components/model/panels/left/ActionItemList.js index b2794f83..edc47faf 100644 --- a/app/src/components/model/modals/ActionItemList.js +++ b/app/src/components/model/panels/left/ActionItemList.js @@ -1,37 +1,42 @@ import AssignmentTurnedInIcon from "@mui/icons-material/AssignmentTurnedIn"; -import { Box, DialogContentText, Typography } from "@mui/material"; -import { useListThreatsQuery } from "../../../api/gram/threats"; -import { useComponent } from "../hooks/useComponent"; -import { Threat } from "../panels/right/Threat"; -import { useModelID } from "../hooks/useModelID"; +import { Box, DialogContentText, Stack, Typography } from "@mui/material"; +import { CollapsePaper } from "../../../elements/CollapsePaper"; +import { useActionItems } from "../../hooks/useActionItems"; +import { useComponent } from "../../hooks/useComponent"; +import { Threat } from "../right/Threat"; -function ComponentActionItem({ componentId, threats }) { +function ComponentActionItem({ + componentId, + threats, + defaultExpanded = false, +}) { const component = useComponent(componentId); return ( - {component.name} - {threats.map((th) => ( - - ))} + + + {threats.map((th, i) => ( + + ))} + + ); } -export function ActionItemList() { - const modelId = useModelID(); - const { data: threats } = useListThreatsQuery({ modelId }); - - const actionItems = threats?.threats - ? Object.keys(threats?.threats) - .map((componentId) => ({ - componentId, - threats: threats?.threats[componentId].filter( - (th) => th.isActionItem - ), - })) - .filter(({ threats }) => threats && threats.length > 0) - : []; +export function ActionItemList({ automaticallyExpanded = false }) { + const actionItems = useActionItems(); return ( <> @@ -40,9 +45,14 @@ export function ActionItemList() { The following threats are marked as action items. +
{actionItems.map(({ componentId, threats }) => ( - + ))} )} diff --git a/app/src/components/model/panels/left/ActionItemTab.js b/app/src/components/model/panels/left/ActionItemTab.js new file mode 100644 index 00000000..f291cd10 --- /dev/null +++ b/app/src/components/model/panels/left/ActionItemTab.js @@ -0,0 +1,15 @@ +import { Box } from "@mui/material"; +import { ActionItemList } from "./ActionItemList"; + +export function ActionItemTab() { + return ( + + + + ); +} diff --git a/app/src/components/model/panels/left/Footer.js b/app/src/components/model/panels/left/Footer.js index 94d036c5..f988c664 100644 --- a/app/src/components/model/panels/left/Footer.js +++ b/app/src/components/model/panels/left/Footer.js @@ -2,6 +2,7 @@ import { DeleteRounded as DeleteRoundedIcon, HelpRounded as HelpRoundedIcon, } from "@mui/icons-material"; +// import IosShareIcon from "@mui/icons-material/IosShare"; import { Box, IconButton, Paper, Tooltip, tooltipClasses } from "@mui/material"; import { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; @@ -12,7 +13,7 @@ import { MODALS } from "../../../elements/modal/ModalManager"; import { PERMISSIONS } from "../../constants"; import { useModelID } from "../../hooks/useModelID"; -export function Footer() { +export function LeftFooter() { const dispatch = useDispatch(); const emptyDiagram = useSelector( @@ -51,6 +52,11 @@ export function Footer() { + {/* + + + + */} + + + {component && ( )} -