import React, { useState, useEffect } from "react"; import { useInterval } from "react-powerhooks"; import { toast } from 'react-toastify'; import theme from "../theme.jsx"; import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx" import { InputAdornment, Tooltip, TextField, CircularProgress, ButtonGroup, Button, Avatar, ListItemAvatar, Typography, List, ListItem, ListItemText, Collapse, IconButton, } from "@mui/material"; import { FavoriteBorder as FavoriteBorderIcon, Error as ErrorIcon, CheckCircleRounded as CheckCircleRoundedIcon, ExpandMore as ExpandMoreIcon, ExpandLess as ExpandLessIcon, Check as CheckIcon, Visibility as VisibilityIcon, } from "@mui/icons-material"; import { FixName } from "../views/Apps.jsx"; import aa from 'search-insights' import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; // Handles workflow updates on first open to highlight the issues of the workflow // Variables // Action (exists, missing fields) // Action auth // Triggers // // Specifically used for UNSAVED workflows only? const ConfigureWorkflow = (props) => { const { apps, isCloud, workflow, userdata, globalUrl, newWebhook, referenceUrl, saveWorkflow, showTriggers, submitSchedule, setSelectedApp, selectedAction, appAuthentication, setSelectedAction, workflowExecutions, getWorkflowExecution, setAuthenticationType, setAuthenticationModalOpen, setConfigureWorkflowModalOpen, setConfigurationFinished, } = props; const [requiredActions, setRequiredActions] = React.useState([]); const [requiredVariables, setRequiredVariables] = React.useState([]); const [requiredTriggers, setRequiredTriggers] = React.useState([]); const [previousAuth, setPreviousAuth] = React.useState(appAuthentication); const [itemChanged, setItemChanged] = React.useState(false); const [firstLoad, setFirstLoad] = React.useState(""); const [showFinalizeAnimation, setShowFinalizeAnimation] = React.useState(false); const [loopRunning, setLoopRunning] = useState(false) const [checkStarted, setCheckStarted] = React.useState(false); useEffect(() => { if (requiredActions.length === 0) { if (setConfigurationFinished !== undefined) { setConfigurationFinished(true) } } }, [requiredActions]) const stop = () => { setLoopRunning(false) } const start = () => { setLoopRunning(true) } useEffect(() => { if (loopRunning) { const intervalId = setInterval(() => { if (!loopRunning) { clearInterval(intervalId); } if (getWorkflowExecution !== undefined && workflowExecutions !== undefined) { const paramkey = workflow.id getWorkflowExecution(paramkey) } else { console.log("Executions or getWorkflowExecutions not defined") } }, 3000) return () => clearInterval(intervalId); } }, [loopRunning]) /* const { start, stop } = useInterval({ duration: 3000, startImmediate: false, callback: () => { if (getWorkflowExecution !== undefined && workflowExecutions !== undefined) { const paramkey = workflow.id getWorkflowExecution(paramkey) } else { console.log("Executions or getWorkflowExecutions not defined") } }, }); */ // ONLY when component is being unloaded, run stop() function // This is to prevent the interval from running when the component is not being used /* useEffect(() => { return () => { stop() } }, []) */ // Where is this from? if (workflow === undefined || workflow === null || workflow.id === undefined) { //console.log("Workflow is undefined or null: ", workflow) return null } if (apps === undefined || apps === null) { //console.log("Apps is undefined or null: ", apps) return null } if (appAuthentication === undefined || appAuthentication === null) { //console.log("App authentication is undefined or null: ", appAuthentication) return null } const getApp = (actionId, appId) => { fetch(globalUrl + "/api/v1/apps/" + appId + "/config?openapi=false", { headers: { Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status === 200) { //toast("Successfully GOT app "+appId) } else { toast("Failed getting app"); } return response.json(); }) .then((responseJson) => { if ( responseJson.actions !== undefined && responseJson.actions !== null ) { } }) .catch((error) => { toast(error.toString()); }); }; if (firstLoad.length === 0 || firstLoad !== workflow.id) { if (apps === undefined || apps === null || apps.length === 0) { console.log("No apps loaded: ", apps); if (setConfigureWorkflowModalOpen !== undefined) { setConfigureWorkflowModalOpen(false) } return null; } setFirstLoad(workflow.id) const newactions = []; for (let [key, keyval] in Object.entries(workflow.actions)) { var action = JSON.parse(JSON.stringify(workflow.actions[key])) var newaction = { large_image: action.large_image, app_name: action.app_name, app_version: action.app_version, activation_done: false, must_activate: false, must_authenticate: false, auth_done: false, action_ids: [], action: action, update_version: action.app_version, app: {}, steps: [], show_steps: false, } if (action.app_name.toLowerCase().endsWith("_api")) { action.app_name = action.app_name.slice(0, -4) } if (action.app_name === "Integration Framework") { var selected_app = "" for (var paramkey in action.parameters) { const param = action.parameters[paramkey] if (param.name === "app_name") { selected_app = param.value break } } for (var appauth in appAuthentication) { if (appAuthentication[appauth].app.name.toLowerCase() === selected_app.toLowerCase()) { newaction.auth_done = true break } } if (newaction.auth_done) { continue } for (var appkey in apps) { if (apps[appkey].name.toLowerCase() === selected_app.toLowerCase()) { newaction.app_name = apps[appkey].name newaction.app_version = apps[appkey].app_version newaction.app = apps[appkey] newaction.app_id = apps[appkey].id newaction.must_activate = false newaction.must_authenticate = true newaction.steps.push({ "title": "Authenticate app", "type": "authenticate", "required": true, }) action.app_id = apps[appkey].id action.authentication_id = "" action.authentication = { "required": true, } break } } if (newaction.app.id === undefined) { console.log("Failed to find app: ", selected_app) continue } newaction.update_version = "1.1.0" } // ID match OR name match + version match //const app = apps.find((app) => app.id === action.app_id || (app.name === action.app_name && (app.app_version === action.app_version || (app.loop_versions !== null && app.loop_versions.includes(action.app_version))))) // without version match const newappname = action.app_name.toLowerCase().replaceAll(" ", "_") var app = apps.find((app) => app.id === action.app_id || app.name.toLowerCase().replaceAll(" ", "_") === newappname) if (app === undefined || app === null) { const subapp = apps.find(app => app.name === action.app_name) if (subapp !== undefined && subapp !== null) { newaction.update_version = "1.1.0" } newaction.must_activate = true; newaction.steps.push({ "title": "Activate app", "type": "activate", "required": true, }) } else { if (action.authentication_id !== "" && app.authentication.required === true) { var authFound = false for (var authkey in appAuthentication) { if (appAuthentication[authkey].id === action.authentication_id) { authFound = true break } } if (!authFound) { action.authentication_id = "" } } if (action?.authentication_id === "" && app?.authentication?.required === true && action.parameters !== undefined && action.parameters !== null) { // Check if configuration is filled or not var filled = true; for (let [key,keyval] in Object.entries(action.parameters)) { if (action.parameters[key].configuration) { if (action.parameters[key].value === null || action.parameters[key].value.length === 0) { filled = false; break; } } } if (app?.authentication?.type === "oauth2" || app?.authentication?.type === "oauth2-app") { filled = false action.auth_type = "oauth2" } newaction.steps.push({ "title": "Authenticate app", "type": "authenticate", "required": true, "auth_type": app.authentication.type, }) if (!filled) { newaction.must_authenticate = true; newaction.action_ids.push(action.id); } } else if (action.authentication_id !== undefined && action.authentication_id !== null && action.authentication_id !== "" && app.authentication.required === true) { console.log("FIXME: Should verify authentication ID ", action.authentication_id) } newaction.app = app; } if (newaction.errors !== undefined && newaction.errors !== null && newaction.errors.length > 0) { //console.log("Node has errors!: ", action.errors) } //console.log(newaction.app_name,"AUTH: ", newaction.must_authenticate, " ACTIVATE: ", newaction.must_activate) if (newaction.must_authenticate) { var authenticationOptions = [] for (let [key,keyval] in Object.entries(appAuthentication)) { const auth = appAuthentication[key] if (auth.app.name === app.name && auth.active) { //console.log("Found auth: ", auth) authenticationOptions.push(auth); newaction.authenticationId = auth.id; break; } } if ( newaction.authenticationId === null || newaction.authenticationId === undefined || newaction.authenticationId.length === "" ) { //console.log("FAILED to authenticate node!") if ( newactions.find( (tmpaction) => tmpaction.app_id === newaction.app_id && tmpaction.app_name === newaction.app_name ) !== undefined ) { //console.log("Action already found."); } else { newactions.push(newaction); } } else { //console.log("Skipping node as it's already authenticated.") newaction.authentication = authenticationOptions; workflow.actions[key] = newaction; } } else if (newaction.must_activate) { if ( newactions.find( (tmpaction) => tmpaction.app_id === newaction.app_id && tmpaction.app_name === newaction.app_name ) !== undefined ) { console.log("Action already found."); } else { newactions.push(newaction); } } } if (workflow.workflow_variables !== undefined && workflow.workflow_variables !== null && workflow.workflow_variables.length !== 0) { for (let [key,keyval] in Object.entries(workflow.workflow_variables)) { const variable = workflow.workflow_variables[key]; if (variable.value === undefined || variable.value === undefined || variable.value.length === 0) { variable.value = ""; requiredVariables.push(variable); } variable.index = key; } } if (workflow.triggers !== undefined && workflow.triggers !== null && workflow.triggers.length !== 0) { for (let [key,keyval] in Object.entries(workflow.triggers)) { var trigger = workflow.triggers[key]; trigger.index = key; if (trigger.trigger_type === "WEBHOOK") { if (trigger.app_association !== undefined && trigger.app_association.name !== null && trigger.app_association.name !== "") { const findapp = trigger.app_association.name.toLowerCase() const foundindex = newactions.findIndex(action => action.app_name.toLowerCase() === findapp) // Adding webhook to start of it if (foundindex >= 0) { const tmpsteps = newactions[foundindex].steps newactions[foundindex].steps = [ { "title": "Configure Webhook", "type": "webhook", "required": true, } ] for (let [subkey,subkeyval] in Object.entries(tmpsteps)) { newactions[foundindex].steps.push(tmpsteps[subkey]) } newactions[foundindex].show_steps = true continue } } } if (trigger.status === "running") { continue; } if ( trigger.trigger_type === "SUBFLOW" || trigger.trigger_type === "USERINPUT" ) { continue; } requiredTriggers.push(trigger) } } if (setConfigureWorkflowModalOpen !== undefined && requiredTriggers.length === 0 && requiredVariables.length === 0 && newactions.length === 0 ) { console.log("No required triggers, variables or actions. Closing modal.") setConfigureWorkflowModalOpen(false) } //setRequiredTriggers(requiredTriggers) setRequiredVariables(requiredVariables) setRequiredActions(newactions) } if (appAuthentication !== undefined && previousAuth !== undefined && appAuthentication.length !== previousAuth.length) { var newactions = [] for (let [actionkey, actionkeyval] in Object.entries(requiredActions)) { var newaction = requiredActions[actionkey]; const app = newaction.app; for (let [key,keyval] in Object.entries(appAuthentication)) { const auth = appAuthentication[key]; // Does this account for all the different ones of the same? if (auth.app.name === app.name && auth.active === true) { newaction.auth_done = true; break; } } newactions.push(newaction); } setRequiredActions(newactions); setPreviousAuth(appAuthentication); // Set auth done to true //"auth_done": false } const TriggerSection = (props) => { const { trigger } = props return ( {trigger.label} {trigger.trigger_type === "WEBHOOK" && trigger.status !== "running" ? ( ) : trigger.trigger_type === "SCHEDULE" && trigger.status !== "running" ? ( ) : null} ); }; const VariableSection = (props) => { const { variable } = props; //Name: {variable.name} - {variable.value}. return ( , }} fullWidth color="primary" type={"text"} placeholder={`New value for ${variable.name}`} onChange={(event) => { console.log( "NEW VALUE ON INDEX", variable.index, variable.value ); }} onBlur={(event) => { workflow.workflow_variables[variable.index].value = event.target.value; }} /> } style={{}} /> ); }; const activateApp = (app_id, app_name, app_version) => { if (aa !== undefined) { aa('init', { appId: "JNSS5CFDZZ", apiKey: "db08e40265e2941b9a7d8f644b6e5240", }) const timestamp = new Date().getTime() aa('sendEvents', [ { eventType: 'conversion', eventName: 'Public App Activated', index: 'appsearch', objectIDs: [app_id], timestamp: timestamp, userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id, } ]) } fetch( `${globalUrl}/api/v1/apps/${app_id}/activate?app_name=${app_name}&app_version=${app_version}`, { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", } ) .then((response) => { if (response.status !== 200) { //window.location.pathname = "/search" //toast("Failed to find this app. Is it public?") } return response.json(); }) .then((responseJson) => { if (responseJson.success === false) { if (responseJson.reason !== undefined) { toast("Failed to activate the app: "+responseJson.reason); } else { toast("Failed to activate the app"); } } else { toast("App activated for your organization!"); } }) .catch((error) => { toast(error.toString()); }); }; const AppSectionSelfcontained = (props) => { const { action } = props; const [opened, setOpened] = useState(false); const [filled, setFilled] = useState(false); const [submitted, setSubmitted] = useState(false); const [finalized, setFinalized] = useState(false); const [authFields, setAuthFields] = useState([]) const [sensitiveFields, setSensitiveFields] = useState([]) //console.log("ACTION", action) useEffect(() => { if (finalized === true) { setOpened(false) setFilled(true) } }, [finalized]) if (authFields.length === 0 && opened === true) { // Loop through fields of the action var newfields = [] var index = 0 var sensitiveIndexes = [] const params = action.action.parameters for (let key in params) { const param = params[key] if (param.configuration === true) { if (param.name.toLowerCase().includes("key") || param.name.toLowerCase().includes("token") || param.name.toLowerCase().includes("password")) { sensitiveIndexes.push(index) } newfields.push({ "key": param.name, "example": param.example === undefined ? "" : param.example, "value": param.name === "url" ? param.example : "", }) index += 1 } } if (newfields.length > 0) { setSensitiveFields(sensitiveIndexes) setAuthFields(newfields) } } const submitLocalAuth = (app, fields) => { const appAuthData = { active: true, app: app, fields: fields, label: "Authentication for " + app.name, usage: [{"workflow_id": workflow.id}], auto_distribute: true, } fetch(globalUrl + "/api/v1/apps/authentication", { method: "PUT", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(appAuthData), credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for setting app auth :O!"); } setSubmitted(false) return response.json(); }) .then((responseJson) => { if (!responseJson.success) { toast("Failed to set app authentication: " + responseJson.reason); } else { toast("App authentication set for app " + app.name.replace("_", " ")); setFinalized(true) setOpened(false) } }) .catch((error) => { setSubmitted(false) //toast(error.toString()); console.log("New auth error: ", error.toString()); }); } var parsedName = action.app_name.replaceAll("_", " "); if (action.app_name.toLowerCase().endsWith("_api")) { parsedName = parsedName.substring(0, parsedName.length - 4); } // Remove _basic at the end if it exists if (parsedName.toLowerCase().endsWith("_basic")) { parsedName = parsedName.substring(0, parsedName.length - 6); } parsedName = (parsedName.charAt(0).toUpperCase() + parsedName.slice(1)).replaceAll("_", " "); return (
{ if (filled) { return } setOpened(!opened); // Scroll to it const element = document.getElementById("app-config"); if (element) { // Scroll down 100px setTimeout(() => { element.scrollIntoView({ behavior: "smooth", top: 100, }) //element.scrollIntoView({ // behavior: "smooth", // block: "center", // inline: "center" //}); }, 250) } }} >
{!opened ? : } {parsedName} {finalized ? "Authenticated" : `Configure ${parsedName}`}
{filled ? : null}
{opened ?
{action.app?.authentication?.type === "oauth2-app" || action.app?.authentication?.type === "oauth2" || action.auth_type === "oauth2" ?
: authFields.map((field, index) => { console.log("THESE FIELDS?: ", field) var parsedName = field.key // Remove _basic at the end if it exists if (parsedName.toLowerCase().endsWith("_basic")) { parsedName = parsedName.substring(0, parsedName.length - 6); } parsedName = (parsedName.charAt(0).toUpperCase() + parsedName.slice(1)).replaceAll("_", " "); return (
{parsedName} { event.preventDefault(); authFields[index].value = event.target.value; setAuthFields(authFields); var allFilled = true; authFields.forEach((field) => { if (field.value.length === 0) { allFilled = false; } else { //console.log("Field is not filled: "+field.key) } }) if (allFilled) { console.log("Should test the fields, and submit them") setFilled(true); } else { if (filled) { setFilled(false); } } }} endAdornment={ // Show item that can show field value if password //field.name.toLowerCase().includes("key") || field.name.toLowerCase().includes("token") || field.name.toLowerCase().includes("password") ? field.key.toLowerCase().includes("key") || field.key.toLowerCase().includes("token") || field.key.toLowerCase().includes("password") ? { setSensitiveFields(sensitiveFields.filter((item) => item !== index)) }} onMouseDown={(event) => { event.preventDefault(); }} > : null } fullWidth color="primary" type={sensitiveFields.includes(index) ? "password" : "text"} placeholder={field.example ? field.example : `Enter your ${field.key}`} data-lpignore="true" dataLPIgnore="true" autocomplete="off" />
) })} {action.app?.authentication?.type !== "oauth2-app" && action.app?.authentication?.type !== "oauth2" ? : null}
: null}
) } const AppSection = (props) => { const { action } = props; var parsedName = action.app_name.replaceAll("_", " "); if (action.app_name.toLowerCase().endsWith("_api")) { parsedName = parsedName.substring(0, parsedName.length - 4); } return ( {action.must_authenticate ? : null} {action.update_version !== action.app_version ? : action.must_activate ? : null } ); } // Based on the color here. Default: #f86a3e //backgroundColor: selectedUsecaseCategory === usecase.name ? usecase.color : theme.palette.surfaceColor, const BoxHighlight = (props) => { const {data, appname, appinfo, index, activeStep, setActiveStep, filled, } = props const [hovered, setHovered] = useState(false) const [isOpen, setIsOpen] = useState(false) const [isLoading, setIsLoading] = useState(false) // This kind of just works for new workflows.. // What if we try many times? var webhook = { "name": "Testhook", "description": `A Webhook Trigger has been started and is ready to receive events from ${appname}. Click to copy the URL to send events to.`, "url": "", } useEffect(() => { if (data.type === "webhook" && !filled) { if (!checkStarted) { setCheckStarted(true) start() } } }, []) // Load webhook docs from the app itself (Wazuh) // Add a "sample" for what the event is supposed to look like // Have a listener for when ACTUALLY is received // INJECT the URL into the documentation when loading it in // How can we load it in? Should we just use the app name & get docs -> parse? if (data.type === "webhook" && workflow.triggers !== undefined && workflow.triggers !== null) { //console.log("Find webhook in the workflow!") for (var key in workflow.triggers) { if (workflow.triggers[key].trigger_type !== "WEBHOOK") { continue } for (var subkey in workflow.triggers[key].parameters) { const param = workflow.triggers[key].parameters[subkey] if (param.name === "url") { webhook.url = param.value if (isLoading === false) { setIsLoading(true) } break } } } } else if (data.type == "authenticate") { //console.log("Handle app authentication in the workflow!") } return (
{ setIsOpen(!isOpen) setActiveStep(index) }} onMouseOver={() => { setHovered(true); }} onMouseOut={() => { setHovered(false); }} >
{data.title} {filled ? : }
{activeStep === index ?
{data.type === "webhook" ?
{ event.preventDefault() console.log("Clicked Webhook") var copyText = document.getElementById("copy_element_shuffle") if (copyText !== undefined && copyText !== null) { console.log("NAVIGATOR: ", navigator); const clipboard = navigator.clipboard; if (clipboard === undefined) { toast("Can only copy over HTTPS (port 3443)"); return; } navigator.clipboard.writeText(webhook.url); copyText.select(); copyText.setSelectionRange( 0, 99999 ); /* For mobile devices */ /* Copy the text inside the text field */ document.execCommand("copy"); toast("Copied Webhook URL"); } }}> {webhook.description} {/*{webhook.url}*/} {isLoading && filled === false ?
: null }
: setConfigureWorkflowModalOpen !== undefined ? : }
: null}
) } const AppWrapper = (props) => { const {data, parentindex} = props const [clicked, setClicked] = useState(true) const [hovered, setHovered] = useState(false) const [activeStep, setActiveStep] = useState(0) const [firstRun, setFirstRun] = useState(true) const [finishCount, setFinishCount] = useState(0) return (
{ //setClicked(!clicked) }} onMouseOver={() => { setHovered(true); }} onMouseOut={() => { setHovered(false); }} > {data.label} Configure {data.app_name.replaceAll("_", " ")}
{clicked === true ? data.steps.map((step, index) => { var filled = false if (step.type === "activate") { if (data.activation_done === true) { filled = true if (index === activeStep && firstRun === true) { setActiveStep(activeStep+1) } if (firstRun) { setFinishCount(finishCount+1) } } } if (step.type === "authenticate") { if (data.must_authenticate === true ) { filled = false } else { if (data.activation_done === true && data.auth_done === true) { filled = true if (firstRun) { setFinishCount(finishCount+1) } if (index === activeStep && firstRun === true) { setActiveStep(activeStep+1) } } } } if (step.type === "webhook") { for (var key in workflowExecutions) { const exec = workflowExecutions[key] if (exec.execution_argument !== undefined && exec.execution_argument !== null && exec.execution_argument.length > 0 && exec.execution_source === "webhook") { //console.log("Done: ", exec) filled = true if (index === activeStep && firstRun === true) { setActiveStep(activeStep+1) } if (firstRun) { setFinishCount(finishCount+1) } // Finished + source = webhook stop() //if (isLoading === true) { // setIsLoading(false) //} break } } } if (firstRun === true && index === data.steps.length-1) { setFirstRun(false) } return ( ) }) : null}
) } return (
{setConfigureWorkflowModalOpen !== undefined ? {workflow.name} : null }
{/*
*/} {requiredActions.length > 0 ? ( To complete the workflow setup, please configure the following steps. {setConfigureWorkflowModalOpen !== undefined ? Required Actions : null} {requiredActions.map((data, index) => { // AppWrapper = Default in a workflow, only shows with steps // AppSection = // AppSectionSelfcontained = default for template generator return (
{data.steps !== undefined && data.steps !== null && data.show_steps === true && setConfigureWorkflowModalOpen !== undefined ? : setConfigureWorkflowModalOpen !== undefined ? : }
) })}
{/* Once done, you may continue to the workflow. */}
) : null} {setConfigureWorkflowModalOpen !== undefined && requiredVariables.length > 0 ? ( Variables {requiredVariables.map((data, index) => { return ; })} ) : null} {setConfigureWorkflowModalOpen !== undefined && requiredTriggers.length > 0 && showTriggers !== false ? ( Triggers {requiredTriggers.map((data, index) => { return ; })} ) : null} {setConfigureWorkflowModalOpen !== undefined ?
{showFinalizeAnimation ? finalize workflow animation { console.log("Img loaded.") setTimeout(() => { console.log("Img closing.") setConfigureWorkflowModalOpen(false); }, 1250) }}/> : {/* */} }
: null}
); }; export default ConfigureWorkflow;