Added detection pages and fixed more form pages

This commit is contained in:
Frikky
2024-10-11 17:10:24 +02:00
parent 38e9d66cfd
commit 79acdbb012
6 changed files with 917 additions and 75 deletions
+210
View File
@@ -0,0 +1,210 @@
import React, { useState } from "react";
import {
Container,
Box,
TextField,
Switch,
Typography,
Button,
CircularProgress,
Paper,
} from "@mui/material";
import { toast } from "react-toastify";
import theme from '../theme.jsx';
import DetectionRuleCard from "../components/DetectionRuleCard.jsx";
const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isTenzirActive) => {
if (!isTenzirActive) {
toast("connect to siem first for global enable/disable to work");
return;
}
const action = folderDisabled ? "enable_folder" : "disable_folder";
const url = `${globalUrl}/api/v1/detections/${action}`;
fetch(url, {
method: "PUT",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === true) {
if (action === "enable_folder") setFolderDisabled(false);
else setFolderDisabled(true);
} else {
//toast(`failed to disable rule`);
}
})
)
.catch((error) => {
console.log(`Error in ${action} the rule: `, error);
toast(`An error occurred while ${action} the rule`);
});
};
const Detection = (props) => {
const { globalUrl, ruleInfo, folderDisabled, setFolderDisabled, isTenzirActive } = props;
const [searchQuery, setSearchQuery] = useState("");
const [loading, setLoading] = useState(false);
const handleConnectClick = () => {
if (!isTenzirActive) {
setLoading(true);
const url = `${globalUrl}/api/v1/detections/siem/connect`;
fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === true) {
setTimeout(() => {
setLoading(false);
window.location.reload();
}, 15000);
} else {
setLoading(false);
toast("Failed to connect to SIEM");
}
})
)
.catch((error) => {
setLoading(false);
console.log(`Error in connecting to SIEM: `, error);
toast("An error occurred while connecting to SIEM");
});
} else {
console.log("Already connected to SIEM");
}
};
const filteredRules = ruleInfo?.filter((rule) =>
rule.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
rule.description.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<Container>
<Paper
style={{
marginTop: 50,
width: "100%",
padding: 50,
backgroundColor: theme.palette.backgroundColor,
borderRadius: theme.palette.borderRadius,
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 2,
}}
>
<Typography variant="h6" component="div">
Sigma Detection Rules
</Typography>
<Button
variant="contained"
onClick={handleConnectClick}
disabled={loading} // Disable the button while loading
color={isTenzirActive ? "primary" : "secondary"}
style={{ }}
>
{loading ? <CircularProgress size={24} /> : isTenzirActive ? "Connected to siem" : "Connect to siem"}
</Button>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 2,
}}
>
<Box
sx={{
display: "flex",
}}
>
<TextField
label="Search rules"
variant="outlined"
size="small"
sx={{ mr: 2 }}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{/* <Button
color="primary"
variant="contained"
onClick={() => uploadRef.current.click()}
>
<PublishIcon /> Upload sigma file
</Button>
<input
hidden
type="file"
multiple
ref={uploadRef}
onChange={(event) => {
uploadFiles(event.target.files);
}}
/> */}
</Box>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography variant="body2" sx={{ mr: 1 }}>
Global disable/enable
</Typography>
<Switch
checked={!folderDisabled}
onChange={() =>
handleDirectoryChange(folderDisabled, setFolderDisabled, globalUrl, isTenzirActive)
}
disabled={!isTenzirActive}
/>
</Box>
</Box>
<Box
sx={{
height: "500px",
width: "100%",
overflowY: "auto",
p: 1,
}}
>
{filteredRules?.length > 0 ?
filteredRules.map((card) => {
console.log("RULE CARD: ", card);
return (
<DetectionRuleCard
key={card.file_id}
ruleName={card.title}
description={card.description}
file_id={card.file_id}
globalUrl={globalUrl}
folderDisabled={folderDisabled}
isTenzirActive={isTenzirActive}
{...card}
/>
)
})
: null }
</Box>
</Paper>
</Container>
);
};
export default Detection;
@@ -0,0 +1,399 @@
import React, { useState, useEffect, } from "react";
import {
Container,
Box,
TextField,
Switch,
Typography,
Button,
CircularProgress,
Paper,
Divider,
IconButton,
} from "@mui/material";
import {
OpenInNew as OpenInNewIcon,
} from "@mui/icons-material"
import { toast } from "react-toastify";
import theme from '../theme.jsx';
import DetectionRuleCard from "../components/DetectionRuleCard.jsx";
import {
green,
red,
grey,
} from "../views/AngularWorkflow.jsx"
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx"
const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isDetectionActive) => {
if (!isDetectionActive) {
toast.warn("Connect to siem first for global enable/disable to work");
return;
}
const action = folderDisabled ? "enable_folder" : "disable_folder";
const url = `${globalUrl}/api/v1/detections/${action}`;
fetch(url, {
method: "PUT",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === true) {
if (action === "enable_folder") setFolderDisabled(false);
else setFolderDisabled(true);
} else {
//toast(`failed to disable rule`);
}
})
)
.catch((error) => {
console.log(`Error in ${action} the rule: `, error);
toast(`An error occurred while ${action} the rule`);
});
};
const DetectionExplorer = (props) => {
const { globalUrl, userdata, ruleInfo, folderDisabled, setFolderDisabled, detectionInfo, importDetectionFromUrl, rulesLoading, isDetectionActive, setIsDetectionActive, ruleMapping, setRuleMapping, } = props;
const [searchQuery, setSearchQuery] = useState("");
const [loading, setLoading] = useState(false);
const [workflow, setWorkflow] = useState({})
const [detectionWorkflowId, setDetectionWorkflowId] = useState("")
const [isDetectionValid, setIsDetectionValid] = useState(false)
const [availableDetection, setAvailableDetection] = React.useState([]);
const loadUsecases = () => {
const url = `${globalUrl}/api/v1/workflows/usecases`
fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson.success === false) {
return
}
if (responseJson.length == 0) {
return
}
for (var usecaseCategory in responseJson) {
const category = responseJson[usecaseCategory]
if (!category.name.toLowerCase().includes("respond") && !category.name.toLowerCase().includes("response")) {
continue
}
setAvailableDetection(category.list)
break
}
})
)
.catch((error) => {
console.log(`Error in loading usecases: `, error);
//toast(`An error occurred while loading usecases`);
})
}
const loadWorkflow = (workflowId) => {
const url = `${globalUrl}/api/v1/workflows/${workflowId}`
fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson.id === workflowId) {
setWorkflow(responseJson)
} else {
toast(`Failed to load workflow ${workflowId}`);
}
}))
.catch((error) => {
console.log(`Error in loading workflow ${workflowId}: `, error);
toast(`An error occurred while loading workflow ${workflowId}`);
})
}
const handleConnectClick = () => {
if (detectionWorkflowId !== "") {
// FIXME: Show the Usecase UI for how to fix the workflow(s)
// Instead loading full workflow and showing it directly? Hmm
//toast.warn("Please reload the UI to load the detection status")
return
}
if (isDetectionActive) {
return
}
if (detectionInfo.category === undefined || detectionInfo.category === null) {
toast.warn("Detection category not found. Please try again or contact support@shuffler.io if you think this is a bug.")
return
}
setLoading(true);
const url = `${globalUrl}/api/v1/detections/${detectionInfo?.category}/connect`;
fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === true) {
setLoading(false)
if (setIsDetectionActive !== undefined) {
setIsDetectionActive(true)
}
if (responseJson.workflow_id !== undefined && responseJson.workflow_id !== null) {
setDetectionWorkflowId(responseJson.workflow_id)
loadWorkflow(responseJson.workflow_id)
}
if (responseJson.workflow_valid !== undefined && responseJson.workflow_valid !== null) {
setIsDetectionValid(responseJson.workflow_valid)
}
} else {
if (responseJson.reason !== undefined && responseJson.reason !== null) {
toast(responseJson.reason)
} else {
toast(`Failed to connect to ${detectionInfo?.category}`);
}
if (responseJson.action !== undefined && responseJson.actio !== null && responseJson.action.length > 0) {
//if (responseJson.action === "environment_create") {
// navigate("/admin?tab=environments")
//}
}
setLoading(false);
}
})
)
.catch((error) => {
setLoading(false);
console.log(`Error in connecting to ${detectionInfo?.category}: `, error);
toast(`An error occurred while connecting to ${detectionInfo?.category}`);
});
}
useEffect(() => {
loadUsecases()
}, [])
useEffect(() => {
handleConnectClick()
}, [detectionInfo])
const filteredRules = ruleInfo === "default" ? [] : ruleInfo?.filter((rule) =>
rule.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
rule.description.toLowerCase().includes(searchQuery.toLowerCase())
)
return (
<Container>
<Paper
style={{
marginTop: 50,
width: "100%",
padding: 50,
backgroundColor: theme.palette.backgroundColor,
borderRadius: theme.palette.borderRadius,
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 2,
}}
>
<Typography variant="h6" component="div">
{detectionInfo?.title} {filteredRules === undefined || filteredRules === null ? null : `(${filteredRules?.length} rules)`}
</Typography>
{workflow !== undefined && workflow !== null && workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 ?
<div style={{display: "flex", }}>
<div style={{minWidth: 400, maxWidth: 400, }}>
<WorkflowValidationTimeline
originalWorkflow={workflow}
apps={[]}
getParents={undefined}
execution={undefined}
workflow={workflow}
showHoverColor={true}
globalUrl={globalUrl}
userdata={userdata}
/>
</div>
<IconButton
variant="contained"
color="secondary"
onClick={() => {
window.open(`/workflows/${workflow.id}`, "_blank")
}}
>
<OpenInNewIcon />
</IconButton>
</div>
:
<Button
variant="contained"
onClick={() => {
handleConnectClick()
}}
disabled={loading} // Disable the button while loading
style={{
// Red = workflow exists, validation is false
// Green = workflow exists, validation is true
// Grey = workflow does not exist
backgroundColor: detectionWorkflowId === "" ? grey : isDetectionValid ? green : red,
}}
>
{loading ? <CircularProgress size={24} /> :
detectionWorkflowId === "" ? `Connect to ${detectionInfo?.category}` :
isDetectionValid ? `Connected to ${detectionInfo?.category}` : `Fix ${detectionInfo?.category} connection`}
</Button>
}
</Box>
{filteredRules?.length > 0 ?
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 2,
}}
>
<Box
sx={{
display: "flex",
minHeight: 50,
maxHeight: 50,
}}
>
<TextField
label="Search rules"
variant="outlined"
size="small"
sx={{ mr: 2 }}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</Box>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography variant="body2" sx={{ mr: 1 }}>
Global disable/enable
</Typography>
<Switch
checked={!folderDisabled}
onChange={() =>
handleDirectoryChange(folderDisabled, setFolderDisabled, globalUrl, isDetectionActive)
}
/>
</Box>
</Box>
: null}
<Divider />
<Box
sx={{
height: "500px",
width: "100%",
overflowY: "auto",
p: 1,
}}
>
{filteredRules?.length > 0 ?
ruleMapping !== undefined && ruleMapping !== null && ruleMapping.value !== undefined && ruleMapping.value !== null ?
filteredRules.map((rule, index) => {
return (
<div style={{marginTop: 5, }}>
<DetectionRuleCard
globalUrl={globalUrl}
key={index}
ruleName={rule.file_name}
description={rule.description}
file_id={rule.file_id}
globalUrl={globalUrl}
folderDisabled={folderDisabled}
isDetectionActive={isDetectionActive}
ruleMapping={ruleMapping}
setRuleMapping={setRuleMapping}
availableDetection={availableDetection}
{...rule}
/>
</div>
)
})
: null
:
<div style={{textAlign: "center", }}>
{rulesLoading === true ?
<Container style={{ display: "flex", justifyContent: "center", alignItems: "center", marginTop: 25, }}>
<div>
<CircularProgress />
<Typography variant="h6" style={{ marginTop: 20 }}>Downloading rules, please wait...</Typography>
</div>
</Container>
:
<div>
<Typography variant="h6" color="textSecondary" style={{marginTop: 50, }}>
No rules loaded yet
</Typography>
<Button
style={{marginTop: 20, }}
variant="contained"
color="primary"
onClick={() => {
if (importDetectionFromUrl !== undefined) {
importDetectionFromUrl(true, detectionInfo.download_repo)
} else {
toast("Import function not found. Please contact support@shuffler.io")
}
}}
>
Load Default Rules
</Button>
</div>
}
</div>
}
</Box>
</Paper>
</Container>
);
};
export default DetectionExplorer;
+130 -31
View File
@@ -61,10 +61,11 @@ import {
OpenInNew as OpenInNewIcon,
Add as AddIcon,
Remove as RemoveIcon,
EditNote as EditNoteIcon,
} from "@mui/icons-material";
const EditWorkflow = (props) => {
const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, apps, saveWorkflow, expanded, scrollTo, } = props
const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, apps, saveWorkflow, expanded, scrollTo, setRealtimeMarkdown, } = props
const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove
@@ -82,8 +83,8 @@ const EditWorkflow = (props) => {
const [inputQuestions, setInputQuestions] = React.useState(workflow.input_questions !== undefined && workflow.input_questions !== null ? JSON.parse(JSON.stringify(workflow.input_questions)) : [])
const [inputMarkdown, setInputMarkdown] = React.useState(workflow.input_markdown !== undefined && workflow.input_markdown !== null ? workflow.input_markdown : "")
const [outputMarkdown, setOutputMarkdown] = React.useState(workflow.output_markdown !== undefined && workflow.output_markdown !== null ? workflow.output_markdown : "")
const [scrollDone, setScrollDone] = React.useState(false)
const [selectedYieldActions, setSelectedYieldActions] = React.useState(workflow.output_yields !== undefined && workflow.output_yields !== null ? JSON.parse(JSON.stringify(workflow.output_yields)) : [])
const classes = useStyles();
@@ -204,25 +205,28 @@ const EditWorkflow = (props) => {
<Typography variant="h4" style={{flex: 9, }}>
{newWorkflow ? "New" : "Editing"} workflow
</Typography>
{newWorkflow === true ? null :
<div style={{ marginLeft: 5, flex: 1 }}>
<Tooltip title="Open Workflow Form for 'normal' users">
<a
rel="noopener noreferrer"
href={`/forms/${workflow.id}`}
target="_blank"
style={{
textDecoration: "none",
color: "#f85a3e",
marginLeft: 5,
marginTop: 10,
}}
>
<OpenInNewIcon />
</a>
<Tooltip title="Go to Public Form page">
<IconButton>
<a
rel="noopener noreferrer"
href={`/forms/${workflow.id}`}
target="_blank"
style={{
textDecoration: "none",
color: "#f85a3e",
marginLeft: 5,
}}
>
<EditNoteIcon />
</a>
</IconButton>
</Tooltip>
</div>
}
</div>
<Typography variant="body2" color="textSecondary" style={{marginTop: 20, maxWidth: 440,}}>
Workflows can be built from scratch, or from templates. <a href="/usecases2" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Usecases</a> can help you discover next steps, and you can <a href="/search?tab=workflows" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>search</a> for them directly. <a href="/docs/workflows" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Learn more</a>
@@ -306,6 +310,8 @@ const EditWorkflow = (props) => {
innerWorkflow.input_questions = validfields
innerWorkflow.input_markdown = inputMarkdown
innerWorkflow.output_yields = selectedYieldActions
innerWorkflow.name = name
innerWorkflow.description = description
if (newWorkflowTags.length > 0) {
@@ -382,16 +388,16 @@ const EditWorkflow = (props) => {
<FormControl style={{flex: 1, marginRight: 5,}}>
<InputLabel htmlFor="grouped-select-usecase">Usecases</InputLabel>
<Select
defaultValue=""
id="grouped-select"
label="Matching Usecase"
multiple
value={selectedUsecases}
renderValue={(selected) => selected.join(', ')}
onChange={(event) => {
console.log("Changed: ", event)
}}
>
defaultValue=""
id="grouped-select"
label="Matching Usecase"
multiple
value={selectedUsecases}
renderValue={(selected) => selected.join(', ')}
onChange={(event) => {
console.log("Changed: ", event)
}}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
@@ -614,6 +620,11 @@ const EditWorkflow = (props) => {
<Divider style={{marginTop: 20, marginBottom: 20, }} />
<Typography variant="h4" style={{marginTop: 50, }}>
MSSP controls
</Typography>
<Typography variant="body1" style={{marginTop: 50, }}>
MSSP Suborg Distribution (beta - contact support@shuffler.io for more info)
</Typography>
@@ -719,11 +730,11 @@ const EditWorkflow = (props) => {
</Link>
}
<Divider style={{marginTop: 20, marginBottom: 20, }} />
{/*<Divider style={{marginTop: 20, marginBottom: 20, }} />*/}
<Typography variant="body1" style={{marginTop: 50, }}>
<Typography variant="body1" style={{marginTop: 100, }}>
Git Backup Repository
</Typography>
<Typography variant="body2" style={{ textAlign: "left", marginTop: 5, }} color="textSecondary">
@@ -875,9 +886,38 @@ const EditWorkflow = (props) => {
<Divider style={{marginTop: 20, marginBottom: 20, }} />
<Typography variant="h6" style={{marginTop: 50, }}>
Input fields
</Typography>
<div id="form_fill" style={{position: "relative", }}>
<Typography variant="h4" style={{marginTop: 100, }}>
Form Control
</Typography>
<Typography variant="body1" color="textSecondary" style={{marginTop: 10, }}>
Form Control is used to control how the Form for the workflow is shown to users. You can add input fields, markdown, and more. This is the first step in the workflow, and is required for all workflows.
</Typography>
<Typography variant="h6" style={{marginTop: 50, }}>
Input fields
</Typography>
<Tooltip title="Go to Public Form page">
<IconButton style={{position: "absolute", top: 0, right: 10, }}>
<a
rel="noopener noreferrer"
href={`/forms/${workflow.id}`}
target="_blank"
style={{
textDecoration: "none",
color: "#f85a3e",
marginLeft: 5,
}}
>
<EditNoteIcon />
</a>
</IconButton>
</Tooltip>
</div>
<Typography variant="body2" color="textSecondary" style={{marginBottom: 20, }}>
Input fields are fields that will be used during the startup of the workflow. These will be formatted in JSON and is most commonly used from the <a href={`/forms/${workflow.id}`} rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Form page</a> for this workflow. If chosen in the User Input node, these will be required fields. Use Semi-Colon ";" to create dropdown options. The first key will be the name shown, and subsequent keys will be the available values.
</Typography>
@@ -1008,6 +1048,10 @@ const EditWorkflow = (props) => {
}}
onChange={(e) => {
if (setRealtimeMarkdown !== undefined) {
setRealtimeMarkdown(e.target.value)
}
setInputMarkdown(e.target.value)
workflow.input_markdown = e.target.value
setWorkflow(workflow)
@@ -1015,6 +1059,61 @@ const EditWorkflow = (props) => {
}}
/>
</div>
<div id="output_control">
<Typography variant="h6" style={{marginTop: 50, }}>
Output Control ({selectedYieldActions.length === 0 ? "No Returns" : selectedYieldActions.length === 1 ? "Returning 1 node" : `Returning ${selectedYieldActions.length} nodes`})
</Typography>
<Typography variant="body2" color="textSecondary" style={{marginBottom: 20, }}>
When running this workflow, the output will be shown as a Markdown object by default, with JSON objects being rendered. By adding nodes below, they will be shown while the workflow is running as soon as they get a result. Failing/Skipped nodes are not shown. This makes it possible to track progress for more complex usecases.
</Typography>
<FormControl style={{marginTop: 15, }}>
<Select
defaultValue=""
id="output-yield-control"
label="Yielding nodes"
multiple
fullWidth
style={{width: 500, }}
value={selectedYieldActions === [] ? ["none"] : selectedYieldActions}
renderValue={(selected) => selected.join(', ')}
onChange={(event) => {
console.log("Value: ", event.target.value)
if (event.target.value.length > 0) {
if (event.target.value.includes("none")) {
setSelectedYieldActions([])
return
}
}
const newvalue = event?.target?.value
if (newvalue === undefined || newvalue === null) {
} else {
setSelectedYieldActions(newvalue)
}
}}
>
<MenuItem value="none">
<em>None</em>
</MenuItem>
{workflow.actions.map((action, actionIndex) => {
return (
<MenuItem
key={actionIndex}
value={action.id}
>
<Tooltip title={action.app_name} key={actionIndex}>
<img src={action.large_image !== undefined && action.large_image !== null && action.large_image.length > 0 ? action.large_image : theme.palette.defaultImage} style={{width: 20, height: 20, marginRight: 10, }} />
</Tooltip>
{action.label}
</MenuItem>
)
})}
</Select>
</FormControl>
</div>
</div>
: null}