diff --git a/frontend/src/components/Detection.jsx b/frontend/src/components/Detection.jsx new file mode 100644 index 00000000..c09d0c7a --- /dev/null +++ b/frontend/src/components/Detection.jsx @@ -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 ( + + + + + Sigma Detection Rules + + + + + + setSearchQuery(e.target.value)} + /> + {/* + { + uploadFiles(event.target.files); + }} + /> */} + + + + Global disable/enable + + + handleDirectoryChange(folderDisabled, setFolderDisabled, globalUrl, isTenzirActive) + } + disabled={!isTenzirActive} + /> + + + + {filteredRules?.length > 0 ? + filteredRules.map((card) => { + console.log("RULE CARD: ", card); + + return ( + + ) + }) + : null } + + + + ); +}; + +export default Detection; diff --git a/frontend/src/components/DetectionExplorer.jsx b/frontend/src/components/DetectionExplorer.jsx new file mode 100644 index 00000000..c81bee26 --- /dev/null +++ b/frontend/src/components/DetectionExplorer.jsx @@ -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 ( + + + + + {detectionInfo?.title} {filteredRules === undefined || filteredRules === null ? null : `(${filteredRules?.length} rules)`} + + + {workflow !== undefined && workflow !== null && workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 ? +
+
+ +
+ + { + window.open(`/workflows/${workflow.id}`, "_blank") + }} + > + + +
+ + : + + } +
+ {filteredRules?.length > 0 ? + + + setSearchQuery(e.target.value)} + /> + + + + Global disable/enable + + + handleDirectoryChange(folderDisabled, setFolderDisabled, globalUrl, isDetectionActive) + } + /> + + + : null} + + + + {filteredRules?.length > 0 ? + + ruleMapping !== undefined && ruleMapping !== null && ruleMapping.value !== undefined && ruleMapping.value !== null ? + filteredRules.map((rule, index) => { + return ( +
+ +
+ ) + }) + : null + : +
+ {rulesLoading === true ? + +
+ + Downloading rules, please wait... +
+
+ : +
+ + No rules loaded yet + + +
+ } +
+ } +
+
+
+ ); +}; + +export default DetectionExplorer; diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx index 257398f8..59d47c0a 100644 --- a/frontend/src/components/EditWorkflow.jsx +++ b/frontend/src/components/EditWorkflow.jsx @@ -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) => { {newWorkflow ? "New" : "Editing"} workflow + {newWorkflow === true ? null :
- - - - + + + + + +
} + Workflows can be built from scratch, or from templates. Usecases can help you discover next steps, and you can search for them directly. Learn more @@ -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) => { Usecases 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) + } + }} + > + + None + + {workflow.actions.map((action, actionIndex) => { + return ( + + + 0 ? action.large_image : theme.palette.defaultImage} style={{width: 20, height: 20, marginRight: 10, }} /> + + {action.label} + + ) + })} + + + : null} diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 6a236161..5754f00e 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -8602,7 +8602,6 @@ const releaseToConnectLabel = "Release to Connect" if (alledges !== undefined && alledges !== null && alledges.length > 0) { for (let edgekey in alledges) { const tmp = alledges[edgekey] - console.log("TMP: ", tmp, tmp.data.source) if (tmp.data.source === trigger.id) { mappedStartnode = tmp.data.target break @@ -16079,7 +16078,7 @@ const releaseToConnectLabel = "Release to Connect" }} />
- Interval (UTC) + When to start: {isCloud || selectedTrigger?.environment === "cloud" ? Cron formatting : "every X second"}
- Execution argument: + Runtime Argument:
@@ -19303,11 +19302,11 @@ const releaseToConnectLabel = "Release to Connect" executionData.execution_source === "questions" || executionData.execution_source === "web" || executionData.execution_source === "form" || executionData.execution_source === "forms" ? - Questions + Form : executionData.execution_source @@ -22178,9 +22177,9 @@ const releaseToConnectLabel = "Release to Connect" const drawerData = originalWorkflow !== undefined && originalWorkflow !== null ?
- Version History (Beta) + Version History -
+
diff --git a/frontend/src/views/RunWorkflow.jsx b/frontend/src/views/RunWorkflow.jsx index 3b2c2e0c..e8f3617d 100644 --- a/frontend/src/views/RunWorkflow.jsx +++ b/frontend/src/views/RunWorkflow.jsx @@ -2,6 +2,7 @@ import React, {useState, useEffect} from 'react'; import ReactDOM from "react-dom" +import ReactJson from "react-json-view-ssr"; import { green, yellow, red, grey} from "./AngularWorkflow.jsx"; import { CodeHandler, Img, OuterLink, } from "../views/Docs.jsx"; import { useNavigate, Link, useParams } from "react-router-dom"; @@ -36,6 +37,8 @@ import { import { Preview as PreviewIcon, ContentCopy as ContentCopyIcon, + ArrowBack as ArrowBackIcon, + ArrowForward as ArrowForwardIcon, } from '@mui/icons-material'; const hrefStyle = { @@ -71,6 +74,7 @@ const RunWorkflow = (defaultprops) => { const [foundSourcenode, setFoundSourcenode] = React.useState(undefined); const [editWorkflowModalOpen, setEditWorkflowModalOpen] = React.useState(false) const [sharingOpen, setSharingOpen] = React.useState(false) + const [realtimeMarkdown, setRealtimeMarkdown] = React.useState("") const boxStyle = { color: "white", @@ -225,10 +229,13 @@ const RunWorkflow = (defaultprops) => { id="markdown_wrapper" escapeHtml={false} style={{ - maxWidth: "100%", minWidth: "100%", + maxWidth: "100%", + minWidth: "100%", + overflowX: "hidden", + overflowY: "auto", }} > - {executionData.result} + {executionData.result}
: null} @@ -376,6 +383,7 @@ const RunWorkflow = (defaultprops) => { }) } + const getWorkflow = (workflow_id, selectedNode) => { const url = `${globalUrl}/api/v1/workflows/${workflow_id}` @@ -477,6 +485,23 @@ const RunWorkflow = (defaultprops) => { } } + if (responseJson.input_markdown !== undefined && responseJson.input_markdown !== null && responseJson.input_markdown.length > 0) { + // Look for {{ uuid }} format, and try to run that workflow with their account + // This is a hack, but a fun one. + + const uuidRegex = /{{\s*[a-f0-9-]+\s*}}/g + const found = responseJson.input_markdown.match(uuidRegex) + if (found !== undefined && found !== null && found.length > 0) { + for (var foundkey in found) { + const uuid = found[foundkey].replace("{{", "").replace("}}", "").trim() + // Run the workflow, then replace it. + // Only run if logged in + console.log("Found UUID: " + uuid) + responseJson.input_markdown = responseJson.input_markdown.replace(found[foundkey], "") + } + } + } + handleGetOrg(responseJson.org_id) setWorkflow(responseJson); }) @@ -651,6 +676,10 @@ const RunWorkflow = (defaultprops) => { const sourceNode = searchParams.get("source_node") useEffect(() => { + if (!isLoaded) { + return + } + getWorkflow(props.match.params.key, sourceNode) if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null) { console.log("Get execution: ", execution_id) @@ -660,7 +689,7 @@ const RunWorkflow = (defaultprops) => { if (answer !== undefined && answer !== null) { console.log("Got answer: ", answer) } - }, []) + }, [isLoaded]) useEffect(() => { @@ -744,6 +773,7 @@ const RunWorkflow = (defaultprops) => { } } + var validResults = 0 const basedata =
@@ -772,7 +802,7 @@ const RunWorkflow = (defaultprops) => { }} rehypePlugins={[rehypeRaw]} > - {workflow.input_markdown} + {realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0 ? realtimeMarkdown : workflow.input_markdown}
: null} @@ -858,7 +888,7 @@ const RunWorkflow = (defaultprops) => { > {multiChoiceOptions.map((option, menuIndex) => { - if (index === 0) { + if (menuIndex === 0) { return null } @@ -928,15 +958,17 @@ const RunWorkflow = (defaultprops) => { } + + {executionRunning ? - {executionData.status !== undefined && executionData.status !== null && executionData.status !== "" ? + {/*executionData.status !== undefined && executionData.status !== null && executionData.status !== "" ? Status: {executionData.status} - : null} + : null*/} : ((answer !== undefined && answer !== null) || (foundSourcenode !== undefined && foundSourcenode !== null)) ? @@ -994,19 +1026,62 @@ const RunWorkflow = (defaultprops) => {
} - {/*buttonClicked !== undefined && buttonClicked !== null && buttonClicked !== "finished" && buttonClicked.length > 0 ? - finalize workflow animation { - console.log("Img loaded.") - setTimeout(() => { - console.log("Img closing.") - setButtonClicked("finished") - }, 1250) + {workflow.output_yields !== undefined && workflow.output_yields !== null && workflow.output_yields.length > 0 ? +
+ {workflow.output_yields.map((yieldItem, index) => { + if (executionData.results === undefined || executionData.results === null || executionData.results.length === 0) { + return null + } - }} - /> - : null*/} + const foundresult = executionData.results.find((result) => { + return result.action.id === yieldItem + }) + + if (foundresult === undefined || foundresult === null) { + return null + } + + if (foundresult.status === "SKIPPED") { + return null + } + + const validate = validateJson(foundresult.result) + validResults += 1 + console.log("VAlid: ", validate) + + var appendedDetails = foundresult.result + if (validate.valid) { + appendedDetails = { + //handleReactJsonClipboard(copy); + }} + displayDataTypes={false} + onSelect={(select) => { + //HandleJsonCopy(validate.result, select, "exec"); + }} + name={false} + /> + } + + return ( +
+ {foundresult?.action?.label?.replaceAll("_", " ")} - {foundresult.status}: +
+ + {appendedDetails} +
+ ) + })} +
+ : null}
{executionInfo} @@ -1015,6 +1090,8 @@ const RunWorkflow = (defaultprops) => { {answer !== undefined && answer !== null ? null : } + +
} @@ -1037,7 +1114,8 @@ const RunWorkflow = (defaultprops) => { usecases={undefined} expanded={true} - scrollTo={"input_markdown"} + setRealtimeMarkdown={setRealtimeMarkdown} + scrollTo={"form_fill"} /> : null} @@ -1112,20 +1190,46 @@ const RunWorkflow = (defaultprops) => { color={"secondary"} style={{marginRight: 10, }} onClick={() => { - setEditWorkflowModalOpen(true) + navigate(`/workflows`) }} > - Edit Details + + Workflows + + + + +
: null} diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 0c55a8d7..a17b08ed 100755 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -75,6 +75,7 @@ import { ArrowRight as ArrowRightIcon, QueryStats as QueryStatsIcon, Visibility as VisibilityIcon, + EditNote as EditNoteIcon, } from "@mui/icons-material"; import { DataGrid, GridToolbar } from "@mui/x-data-grid"; @@ -2050,6 +2051,18 @@ const Workflows = (props) => { {"Edit details"} + + { + window.open(`/forms/${data.id}`, "_blank") + }} + key={"explore forms"} + > + + {"Create Form"} + + { }) : null} - {data.actions !== undefined && data.actions !== null ? ( -
- - - - {workflowMenuButtons} -
- ) : null} + {data.actions !== undefined && data.actions !== null ? ( +
+ + + + {workflowMenuButtons} +
+ ) : null} + {(data.sharing !== undefined && data.sharing !== null && data.sharing === "form") || (data.input_markdown !== undefined && data.input_markdown !== null && data.input_markdown !== "") ? + +
+ { + navigate(`/forms/${data.id}`) + }} + style={{ padding: "0px", color: "#979797" }} + > + + + {workflowMenuButtons} +
+
+ : null}