Skip to content

Commit

Permalink
feat!: action item and threat assessment overhaul
Browse files Browse the repository at this point in the history
The old impact/likelihood sliders have been removed in favour for new action item reporting.

* Action items now come with an assessment input where you set the *severity* of a threat.
* Add new action item tab to the left panel of the threat model diagram where you can see all action items of the entire model.
* Approve Review modal also displays these as a summary to review before approval.
  • Loading branch information
Tethik committed Oct 31, 2023
1 parent be2ad22 commit d0427a3
Show file tree
Hide file tree
Showing 22 changed files with 362 additions and 290 deletions.
2 changes: 1 addition & 1 deletion app/public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "Gram - Threat Modelling",
"icons": [
{
"src": "icon/gram_logo.svg",
"src": "gram_logo.svg",
"sizes": "any"
}
],
Expand Down
60 changes: 60 additions & 0 deletions app/src/components/elements/CollapsePaper.js
Original file line number Diff line number Diff line change
@@ -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 (
<Paper elevation={16} sx={sx}>
<Box
display="flex"
alignItems="center"
sx={{ paddingLeft: "10px", "&:hover": { cursor: "pointer" } }}
onClick={(e) => {
if (e.target === e.currentTarget) {
setExpanded(!expanded);
}
}}
>
<Badge
badgeContent={count}
onClick={() => setExpanded(!expanded)}
sx={{
alignItems: "center",
gap: "10px",
"& span": {
position: "relative",
transform: "scale(1)",
backgroundColor: "dimgray",
},
}}
>
{title}
</Badge>
<IconButton
disableRipple
size="large"
sx={{
marginLeft: "auto",
}}
onClick={() => setExpanded(!expanded)}
>
{expanded ? <KeyboardArrowUpRounded /> : <KeyboardArrowDownRounded />}
</IconButton>
</Box>
<Collapse in={expanded} timeout="auto" unmountOnExit>
{children}
</Collapse>
</Paper>
);
}
36 changes: 25 additions & 11 deletions app/src/components/elements/ColorSlider.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useState } from "react";
const baseMarks = [
{
value: 0,
color: "green",
color: "grey",
},
{
value: 1,
Expand All @@ -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 (
<>
<Box sx={{ paddingLeft: "30px", paddingRight: "30px" }}>
<Slider
defaultValue={defaultValue}
value={selectedMark.value}
step={null} // restricts to only these steps
marks={joinedMarks}
min={0}
max={4}
valueLabelDisplay="off"
valueLabelFormat={(v, i) => 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}
/>
</Box>
{selectedMark?.description && (
{!hideDescription && selectedMark?.description && (
<Typography
sx={{ "white-space": "pre-wrap" }}
variant="caption"
Expand Down
42 changes: 42 additions & 0 deletions app/src/components/elements/UserChip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Chip, IconButton } from "@mui/material";
import EmailIcon from "@mui/icons-material/Email";
import ChatIcon from "@mui/icons-material/Chat";

export function UserChip({ user }) {
return (
<Chip
size="small"
sx={{
color: (theme) => theme.palette.review.text,
}}
variant="outlined"
label={user.name}
icon={
<>
{user?.slackUrl && (
<IconButton
href={user?.slackUrl}
target="_blank"
rel="noreferrer"
color="inherit"
size="small"
>
<ChatIcon />
</IconButton>
)}
{user?.mail && (
<IconButton
href={`mailto:${user?.mail}`}
target="_blank"
rel="noreferrer"
color="inherit"
size="small"
>
<EmailIcon />
</IconButton>
)}
</>
}
/>
);
}
2 changes: 0 additions & 2 deletions app/src/components/elements/modal/ModalManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,7 +22,6 @@ export const MODALS = {
DeleteSelected,
DeclineReview,
CancelReview,
ViewActionItems,
};

export function ModalManager() {
Expand Down
20 changes: 20 additions & 0 deletions app/src/components/model/hooks/useActionItems.js
Original file line number Diff line number Diff line change
@@ -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;
}
133 changes: 12 additions & 121 deletions app/src/components/model/modals/ApproveReview.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
DialogContentText,
DialogTitle,
Divider,
Paper,
TextField,
Typography,
} from "@mui/material";
Expand All @@ -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 (
<>
<ColorSlider
defaultValue={1}
marks={marks}
onChange={(e) => 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 (
<ColorSlider
defaultValue={1}
step={null} // restricts to only these steps
marks={marks}
onChange={(e) => 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 });
Expand Down Expand Up @@ -145,44 +62,18 @@ export function ApproveReview({ modelId }) {
<DialogContent sx={{ paddingTop: "0" }}>
{(isUninitialized || isLoading) && (
<>
<Divider textAlign="left">Risk Evaluation</Divider>
{/* <DialogContentText>
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).
</DialogContentText> */}
{/* <br /> */}
<DialogContentText>
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.
</DialogContentText>

<br />

<Typography>Impact</Typography>
<ImpactSlider
onChange={(value) =>
setExtras({ ...extras, impact: value.label })
}
/>

<br />

<Typography>Likelihood</Typography>
<LikelihoodSlider
onChange={(value) =>
setExtras({ ...extras, likelihood: value.label })
}
/>

<br />
<Divider textAlign="left">Action Items</Divider>
<ActionItemList />

<Typography>
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.
</Typography>
<br />
<Paper sx={{ padding: "15px" }}>
<ActionItemList automaticallyExpanded={true} />
</Paper>
<br />
<Divider textAlign="left">Summary</Divider>

<DialogContentText>
Do you have any further recommendations to the owning team? Your
notes here will be forwarded via email.
Expand Down Expand Up @@ -236,7 +127,7 @@ export function ApproveReview({ modelId }) {
</Button>
{(isUninitialized || isLoading) && (
<Button
onClick={() => approveReview({ modelId, note: localNote, extras })}
onClick={() => approveReview({ modelId, note: localNote })}
disabled={isLoading || permissionsIsLoading || !reviewAllowed}
variant="contained"
>
Expand Down
Loading

0 comments on commit d0427a3

Please sign in to comment.