diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index 533ca908..a0289ae9 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -2,7 +2,7 @@ module main go 1.16 -replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared +//replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared //replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi //replace github.com/frikky/go-elasticsearch => ../../../../git/go-elasticsearch @@ -24,7 +24,7 @@ require ( github.com/h2non/filetype v1.1.3 github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20170819232839-0fbfe93532da // indirect github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.2.6 + github.com/shuffle/shuffle-shared v0.2.7 go4.org v0.0.0-20201209231011-d4a079459e60 // indirect golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce google.golang.org/api v0.65.0 diff --git a/docker-compose.yml b/docker-compose.yml index 39a64851..02e07e9a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: depends_on: - backend backend: - build: ./backend + #build: ./backend image: ghcr.io/frikky/shuffle-backend:nightly container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} diff --git a/frontend/public/images/detectionframework.png b/frontend/public/images/detectionframework.png new file mode 100644 index 00000000..30da65a5 Binary files /dev/null and b/frontend/public/images/detectionframework.png differ diff --git a/frontend/src/components/NestedMenuItem.jsx b/frontend/src/components/NestedMenuItem.jsx new file mode 100644 index 00000000..e48c2988 --- /dev/null +++ b/frontend/src/components/NestedMenuItem.jsx @@ -0,0 +1,202 @@ +import React, {useState, useRef, useImperativeHandle} from 'react' +import {makeStyles} from '@material-ui/core/styles' +import Menu, {MenuProps} from '@material-ui/core/Menu' +import MenuItem, {MenuItemProps} from '@material-ui/core/MenuItem' +import ArrowRight from '@material-ui/icons/ArrowRight' +import clsx from 'clsx' + +export interface NestedMenuItemProps extends Omit { + /** + * Open state of parent ``, used to close decendent menus when the + * root menu is closed. + */ + parentMenuOpen: boolean + /** + * Component for the container element. + * @default 'div' + */ + component?: React.ElementType + /** + * Effectively becomes the `children` prop passed to the `` + * element. + */ + label?: React.ReactNode + /** + * @default + */ + rightIcon?: React.ReactNode + /** + * Props passed to container element. + */ + ContainerProps?: React.HTMLAttributes & + React.RefAttributes + /** + * Props passed to sub `` element + */ + MenuProps?: Omit + /** + * @see https://material-ui.com/api/list-item/ + */ + button?: true | undefined +} + +const TRANSPARENT = 'rgba(0,0,0,0)' +const useMenuItemStyles = makeStyles((theme) => ({ + root: (props: any) => ({ + backgroundColor: props.open ? theme.palette.action.hover : TRANSPARENT + }) +})) + +/** + * Use as a drop-in replacement for `` when you need to add cascading + * menu elements as children to this component. + */ +const NestedMenuItem = React.forwardRef< + HTMLLIElement | null, + NestedMenuItemProps +>(function NestedMenuItem(props, ref) { + const { + parentMenuOpen, + component = 'div', + label, + rightIcon = , + children, + className, + tabIndex: tabIndexProp, + MenuProps = {}, + ContainerProps: ContainerPropsProp = {}, + ...MenuItemProps + } = props + + const {ref: containerRefProp, ...ContainerProps} = ContainerPropsProp + + const menuItemRef = useRef(null) + useImperativeHandle(ref, () => menuItemRef.current) + + const containerRef = useRef(null) + useImperativeHandle(containerRefProp, () => containerRef.current) + + const menuContainerRef = useRef(null) + + const [isSubMenuOpen, setIsSubMenuOpen] = useState(false) + + const handleMouseEnter = (event: React.MouseEvent) => { + setIsSubMenuOpen(true) + + if (ContainerProps?.onMouseEnter) { + ContainerProps.onMouseEnter(event) + } + } + const handleMouseLeave = (event: React.MouseEvent) => { + setIsSubMenuOpen(false) + + if (ContainerProps?.onMouseLeave) { + ContainerProps.onMouseLeave(event) + } + } + + // Check if any immediate children are active + const isSubmenuFocused = () => { + const active = containerRef.current?.ownerDocument?.activeElement + for (const child of menuContainerRef.current?.children ?? []) { + if (child === active) { + return true + } + } + return false + } + + const handleFocus = (event: React.FocusEvent) => { + if (event.target === containerRef.current) { + setIsSubMenuOpen(true) + } + + if (ContainerProps?.onFocus) { + ContainerProps.onFocus(event) + } + } + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + return + } + + if (isSubmenuFocused()) { + event.stopPropagation() + } + + const active = containerRef.current?.ownerDocument?.activeElement + + if (event.key === 'ArrowLeft' && isSubmenuFocused()) { + containerRef.current?.focus() + } + + if ( + event.key === 'ArrowRight' && + event.target === containerRef.current && + event.target === active + ) { + const firstChild = menuContainerRef.current?.children[0] as + | HTMLElement + | undefined + firstChild?.focus() + } + } + + const open = isSubMenuOpen && parentMenuOpen + const menuItemClasses = useMenuItemStyles({open}) + + // Root element must have a `tabIndex` attribute for keyboard navigation + let tabIndex + if (!props.disabled) { + tabIndex = tabIndexProp !== undefined ? tabIndexProp : -1 + } + + return ( +
+ + {label} + {rightIcon} + + { + setIsSubMenuOpen(false) + }} + > +
+ {children} +
+
+
+ ) +}) + +export default NestedMenuItem diff --git a/frontend/src/components/WorkflowPaper.jsx b/frontend/src/components/WorkflowPaper.jsx new file mode 100644 index 00000000..5d3b68cb --- /dev/null +++ b/frontend/src/components/WorkflowPaper.jsx @@ -0,0 +1,287 @@ +import React, { useState, useEffect, useLayoutEffect } from "react"; +import theme from '../theme'; + +import { + Chip, + Typography, + Paper, + Avatar, + Grid, + Tooltip, +} from "@material-ui/core"; + +import { + AvatarGroup, +} from "@mui/material" + +import { + Restore as RestoreIcon, + Edit as EditIcon, + BubbleChart as BubbleChartIcon, + MoreVert as MoreVertIcon, +} from '@material-ui/icons'; + +import { useNavigate, Link, useParams } from "react-router-dom"; + +const workflowActionStyle = { + display: "flex", + width: 160, + height: 44, + justifyContent: "space-between", +} + +const paperAppStyle = { + minHeight: 130, + maxHeight: 130, + overflow: "hidden", + width: "100%", + color: "white", + backgroundColor: theme.palette.surfaceColor, + padding: "12px 12px 0px 15px", + borderRadius: 5, + display: "flex", + boxSizing: "border-box", + position: "relative", +} + +const chipStyle = { + backgroundColor: "#3d3f43", + marginRight: 5, + paddingLeft: 5, + paddingRight: 5, + height: 28, + cursor: "pointer", + borderColor: "#3d3f43", + color: "white", +} + +const WorkflowPaper = (props) => { + const { data } = props; + let navigate = useNavigate(); + + const [open, setOpen] = React.useState(false); + const [anchorEl, setAnchorEl] = React.useState(null); + const appGroup = data.action_references === undefined || data.action_references === null ? [] : data.action_references + + //console.log("Workflow: ", data) + var boxColor = "#86c142"; + + var parsedName = data.name; + if ( + parsedName !== undefined && + parsedName !== null && + parsedName.length > 20 + ) { + parsedName = parsedName.slice(0, 21) + ".."; + } + + + const imageStyle = { + width: 24, + height: 24, + marginRight: 10, + border: "1px solid rgba(255,255,255,0.3)", + } + var image = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.image !== undefined && data.creator_info.image !== null && data.creator_info.image.length > 0 ? : + const creatorname = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.username !== undefined && data.creator_info.username !== null && data.creator_info.username.length > 0 ? data.creator_info.username : "" + var orgName = ""; + var orgId = ""; + if ((data.objectID === undefined || data.objectID === null) && data.id !== undefined && data.id !== null) { + data.objectID = data.id + } + + //console.log("IMG: ", data) + + return ( +
+ +
+ + + +
{ + if (data.creator_info !== undefined) { + navigate("/creators/"+data.creator_info.username) + } + }} + > + {image} +
+
+ + + + {parsedName} + + + +
+ + {appGroup.length > 0 ? +
+ + {appGroup.map((app, index) => { + return ( +
{ + navigate("/apps/"+app.id) + }} + > + + + +
+ ) + })} +
+
+ : + + + + + {data.actions === undefined || data.actions === null ? 1 : data.actions.length} + + + + } + + + + + {data.triggers === undefined || data.triggers === null ? 1 : data.triggers.length} + + + + + { + }} + > + + + + + {0} + + + +
+ + {data.tags !== undefined && data.tags !== null + ? data.tags.map((tag, index) => { + if (index >= 3) { + return null; + } + + return ( + + ); + }) + : null} + +
+ +
+ ) + } + +export default WorkflowPaper diff --git a/frontend/src/views/Dashboard.jsx b/frontend/src/views/Dashboard.jsx index 9d75f7c2..395680b5 100644 --- a/frontend/src/views/Dashboard.jsx +++ b/frontend/src/views/Dashboard.jsx @@ -9,12 +9,21 @@ import theme from '../theme'; import { useAlert } from "react-alert"; import { + Tooltip, + IconButton, Typography, Grid, Paper, Chip, } from "@material-ui/core"; +import { + Close as CloseIcon, + DoneAll as DoneAllIcon, +} from "@material-ui/icons"; + +import WorkflowPaper from "../components/WorkflowPaper.jsx" + // core components //import { // chartExample1, @@ -41,267 +50,9 @@ import { TreeMapRect, } from 'reaviz'; -const categorydata = [ - { - "name": "1. Collect & Distribute", - "list": [ - { - "name": "2-way Ticket synchronization", - "items": {} - }, - { - "name": "Email management", - "items": { - "name": "Release a quarantined message", - "items": {} - } - }, - { - "name": "EDR to ticket", - "items": { - "name": "Get host information", - "items": {} - } - }, - { - "name": "SIEM to ticket", - "items": {} - }, - { - "name": "ChatOps", - "items": {} - }, - { - "name": "Threat Intel received", - "items": {} - }, - { - "name": "Domain investigation with LetsEncrypt", - "items": {} - }, - { - "name": "Botnet tracker", - "items": {} - }, - { - "name": "Get running containers", - "items": {} - }, - { - "name": "Assign tickets", - "items": {} - }, - { - "name": "Firewall alerts", - "items": { - "name": "URL filtering", - "items": {} - } - }, - { - "name": "IDS/IPS alerts", - "items": { - "name": "Manage policies", - "items": {} - } - }, - { - "name": "Deduplicate information", - "items": {} - }, - { - "name": "Correlate information", - "items": {} - } - ] - }, - { - "name": "3. Detect", - "list": [ - { - "name": "Search SIEM (Sigma)", - "items": { - "name": "Endpoint", - "items": {} - } - }, - { - "name": "Search EDR (OSQuery)", - "items": {} - }, - { - "name": "Search emails (Phish)", - "items": { - "name": "Check headers and IOCs", - "items": {} - } - }, - { - "name": "Search IOCs (ioc-finder)", - "items": {} - }, - { - "name": "Search files (Yara)", - "items": {} - }, - { - "name": "Correlate tickets", - "items": {} - }, - { - "name": "Honeypot access", - "items": { - "name": "...", - "items": {} - } - } - ] - }, - { - "name": "Verify", - "list": [ - { - "name": "Discover vulnerabilities", - "items": {} - }, - { - "name": "Discover assets", - "items": {} - }, - { - "name": "Ensure policies are followed", - "items": {} - }, - { - "name": "Find Inactive users", - "items": {} - }, - { - "name": "Ensure access rights match HR systems", - "items": {} - }, - { - "name": "Ensure onboarding is followed", - "items": {} - }, - { - "name": "Third party apps in SaaS", - "items": {} - }, - { - "name": "Devices used for your cloud account", - "items": {} - }, - { - "name": "Too much access in GCP/Azure/AWS/ other clouds", - "items": {} - }, - { - "name": "Certificate validation", - "items": {} - }, - { - "name": "Monitor new DNS entries for domain with passive DNS", - "items": {} - }, - { - "name": "Monitor and track password dumps", - "items": {} - }, - { - "name": "Monitor for mentions of domain on darknet sites", - "items": {} - }, - { - "name": "Reporting", - "items": { - "name": "Monthly reports", - "items": { - "name": "...", - "items": {} - } - } - } - ] - }, - { - "name": "4. Respond", - "list": [ - { - "name": "Eradicate malware", - "items": {} - }, - { - "name": "Quarantine host(s)", - "items": {} - }, - { - "name": "Trigger scans", - "items": {} - }, - { - "name": "Update indicators (FW, EDR, SIEM...)", - "items": {} - }, - { - "name": "Autoblock activity when threat intel is received", - "items": {} - }, - { - "name": "Lock/Delete/Reset account", - "items": {} - }, - { - "name": "Lock vault", - "items": {} - }, - { - "name": "Increase authentication", - "items": {} - }, - { - "name": "Trigger scans", - "items": {} - }, - { - "name": "Get policies from assets", - "items": {} - } - ] - }, - { - "name": "2. Enrich", - "list": [ - { - "name": "Internal Enrichment", - "items": { - "name": "...", - "items": {} - } - }, - { - "name": "External historical Enrichment", - "items": { - "name": "...", - "items": {} - } - }, - { - "name": "Realtime", - "items": { - "name": "Analyze screenshots", - "items": {} - } - }, - { - "name": "Ticketing webhook verification", - "items": {} - } - ] - } -] - -const UsecaseListComponent = ({keys}) => { +const UsecaseListComponent = ({keys, isCloud}) => { + const [expandedIndex, setExpandedIndex] = useState(-1); + const [expandedItem, setExpandedItem] = useState(-1); if (keys === undefined || keys === null || keys.length === 0) { return null } @@ -322,14 +73,112 @@ const UsecaseListComponent = ({keys}) => { {usecase.list.map((subcase, subindex) => { + if (subcase.matches === undefined || subcase.matches === null) { + subcase.matches = [] + } + + const selectedItem = subindex === expandedItem && index === expandedIndex + const finished = subcase.matches.length > 0 + //const backgroundColor = selectedItem ? "inherit" : finished ? "inherit" : usecase.color + const backgroundColor = "inherit" + const itemBorder = `${selectedItem ? "3px" : expandedItem >= 0 ? "0px" : "1px"} solid ${usecase.color}` + return ( - - { - console.log("Clicked: ", subcase.name) + { + if (selectedItem) { + } else { + setExpandedIndex(index) + setExpandedItem(subindex) + } + }}> + { + console.log("Clicked: ", subcase) }}> - - {subcase.name} - + {!selectedItem ? +
+ + {subcase.name} + + {finished ? + + { + }} + > + + + + : null} +
+ : +
+ + {subcase.name} + + + Description: {subcase.description} + + + { + setExpandedItem(-1) + setExpandedIndex(-1) + }} + > + + + +
+ {subcase.name} +
+ + Your workflow{subcase.matches.length === 1 ? "" : "s"} ({subcase.matches.length}) + + {subcase.matches.length > 0 ? + + {subcase.matches.map((workflow, workflowindex) => { + return ( + + + + ) + })} + + : +
+ + No workflow selected yet. + +
+ } + {isCloud !== false ? + + Public workflows + + : null} +
+
+
+ }
) @@ -403,7 +252,6 @@ const RadialChart = ({keys, setSelectedCategory}) => { { - console.log("Color: ", colorInput) return '#f86a3e' }} animated={false} @@ -473,7 +321,7 @@ const RadialChart = ({keys, setSelectedCategory}) => { // This is the start of a dashboard that can be used. // What data do we fill in here? Idk const Dashboard = (props) => { - const { globalUrl } = props; + const { globalUrl, isLoggedIn } = props; const alert = useAlert(); const [bigChartData, setBgChartData] = useState("data1"); const [dayAmount, setDayAmount] = useState(7); @@ -487,6 +335,44 @@ const Dashboard = (props) => { const [selectedUsecaseCategory, setSelectedUsecaseCategory] = useState(""); const [selectedUsecases, setSelectedUsecases] = useState([]); const [usecases, setUsecases] = useState([]); + const [workflows, setWorkflows] = useState([]); + + const isCloud = + window.location.host === "localhost:3002" || + window.location.host === "shuffler.io"; + + const getAvailableWorkflows = () => { + fetch(globalUrl + "/api/v1/workflows", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + fetchUsecases() + console.log("Status not 200 for workflows :O!: ", response.status); + return; + } + + return response.json(); + }) + .then((responseJson) => { + fetchUsecases(responseJson) + + console.log("Resp: ", responseJson) + if (responseJson !== undefined) { + //setWorkflows(responseJson); + //fetchUsecases(responseJson) + } + }) + .catch((error) => { + fetchUsecases() + //alert.error(error.toString()); + }); + } useEffect(() => { console.log("Changed: ", selectedUsecaseCategory) @@ -521,7 +407,7 @@ const Dashboard = (props) => { setTreeKeys(treeCategories) } - const fetchUsecases = () => { + const fetchUsecases = (workflows) => { fetch(globalUrl + "/api/v1/workflows/usecases", { method: "GET", headers: { @@ -540,9 +426,62 @@ const Dashboard = (props) => { .then((responseJson) => { if (responseJson.success !== false) { console.log("Usecases: ", responseJson) - handleKeysetting(responseJson) - setUsecases(responseJson) - setSelectedUsecases(responseJson) + if (workflows !== undefined && workflows !== null && workflows.length > 0) { + console.log("Got workflows: ", workflows) + var categorydata = responseJson + + var newcategories = [] + for (var key in categorydata) { + var category = categorydata[key] + category.matches = [] + + for (var subcategorykey in category.list) { + var subcategory = category.list[subcategorykey] + subcategory.matches = [] + + for (var workflowkey in workflows) { + const workflow = workflows[workflowkey] + + if (workflow.usecase_ids !== undefined && workflow.usecase_ids !== null) { + for (var usecasekey in workflow.usecase_ids) { + if (workflow.usecase_ids[usecasekey].toLowerCase() === subcategory.name.toLowerCase()) { + console.log("Got match: ", workflow.usecase_ids[usecasekey]) + + category.matches.push({ + "workflow": workflow.id, + "category": subcategory.name, + }) + + subcategory.matches.push(workflow) + break + } + } + } + + if (subcategory.matches.length > 0) { + break + } + } + } + + newcategories.push(category) + } + + console.log("Categories: ", newcategories) + if (newcategories !== undefined && newcategories !== null && newcategories.length > 0) { + handleKeysetting(newcategories) + setUsecases(newcategories) + setSelectedUsecases(newcategories) + } else { + handleKeysetting(responseJson) + setUsecases(responseJson) + setSelectedUsecases(responseJson) + } + } else { + handleKeysetting(responseJson) + setUsecases(responseJson) + setSelectedUsecases(responseJson) + } } }) .catch((error) => { @@ -552,7 +491,8 @@ const Dashboard = (props) => { }; useEffect(() => { - fetchUsecases() + getAvailableWorkflows() + //fetchUsecases() }, []); const fetchdata = (stats_id) => { @@ -845,7 +785,7 @@ const Dashboard = (props) => {
: null} - + {treeKeys.length > 0 ? diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 7167fdf2..434a9287 100644 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -930,7 +930,7 @@ const Workflows = (props) => { //workflows[0].usecase_ids = ["Correlate tickets"] if (workflows !== undefined && workflows !== null) { - const newcategories = [] + var newcategories = [] for (var key in categorydata) { var category = categorydata[key] category.matches = [] @@ -2493,7 +2493,7 @@ const Workflows = (props) => { None
{usecases.map((usecase, index) => { - console.log(usecase) + //console.log(usecase) return ( { {isCloud ? null : (