diff --git a/.env b/.env index df60ea0a..307c9d35 100644 --- a/.env +++ b/.env @@ -53,7 +53,7 @@ TZ=Europe/Amsterdam # Timezone-handler in Orborus, Worker and Apps ORBORUS_CONTAINER_NAME= # Used to FIND the containername. cgroup v2: issue 501 SHUFFLE_ORBORUS_STARTUP_DELAY= # Used for setting up a startup delay for Orborus -SHUFFLE_BASE_IMAGE_NAME=frikky +SHUFFLE_BASE_IMAGE_NAME=shuffle SHUFFLE_BASE_IMAGE_REGISTRY=ghcr.io SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-1.0.0" diff --git a/.github/workflows/dockerbuild.yaml b/.github/workflows/dockerbuild.yaml index 6e69e896..96e5b869 100644 --- a/.github/workflows/dockerbuild.yaml +++ b/.github/workflows/dockerbuild.yaml @@ -46,38 +46,30 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to Ghcr + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Dockerhub Build and push - id: docker_build_dockerhub - uses: docker/build-push-action@v2 + - name: Ghcr Build and push + id: docker_build + uses: docker/build-push-action@v3 env: BUILDX_NO_DEFAULT_LOAD: true with: + logout: false context: ${{ matrix.path }}/ file: ${{ matrix.path }}/Dockerfile platforms: linux/amd64,linux/arm64 - #,linux/386 - no node image I guess? push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/shuffle-${{ matrix.app }}:nightly - - #- name: Login to Ghcr - # uses: docker/login-action@v2 - # with: - # registry: ghcr.io - # username: frikky - # password: ${{ secrets.GHCR_PW }} - - #- name: Ghcr Build and push - # id: docker_build - # uses: docker/build-push-action@v2 - # env: - # BUILDX_NO_DEFAULT_LOAD: true - # with: - # context: ${{ matrix.path }}/ - # file: ${{ matrix.path }}/Dockerfile - # platforms: linux/amd64,linux/arm64 - # push: true - # tags: frikky/shuffle-${{ matrix.app }}:nightly + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + tags: | + ghcr.io/shuffle/shuffle-${{ matrix.app }}:nightly + ${{ secrets.DOCKERHUB_USERNAME }}/shuffle-${{ matrix.app }}:nightly - name: Image digest run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index dd8bf2e2..5a8a76d3 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -1433,7 +1433,7 @@ class AppBase: "execution_id": self.current_execution_id } - self.logger.info("[DEBUG] Before FULLEXEC stream result") + self.logger.info("[ERROR] Before FULLEXEC stream result") ret = requests.post( "%s/api/v1/streams/results" % (self.base_url), headers=headers, @@ -1444,8 +1444,7 @@ class AppBase: fullexecution = ret.json() else: try: - self.logger.info("[DEBUG] Error: Data: ", ret.json()) - self.logger.info("[DEBUG] Error with status code for results. Crashing because ACTION_RESULTS or WORKFLOW_VARIABLE can't be handled. Status: %d" % ret.status_code) + self.logger.info("[ERROR] Error in app with status code for results. Crashing because results can't be handled. Status: %d" % ret.status_code) except json.decoder.JSONDecodeError: pass @@ -1457,7 +1456,7 @@ class AppBase: self.send_result(self.action_result, headers, stream_path) return except requests.exceptions.ConnectionError as e: - self.logger.info("[DEBUG] FullExec Connectionerror: %s" % e) + self.logger.info("[ERROR] FullExec Connectionerror: %s" % e) self.action_result["result"] = json.dumps({ "success": False, "reason": f"Connection error during startup: {e}" @@ -1470,7 +1469,7 @@ class AppBase: try: fullexecution = json.loads(self.full_execution) except json.decoder.JSONDecodeError as e: - self.logger.info("[WARNING] Json decode execution error: %s" % e) + self.logger.info("[ERROR] Json decode execution error: %s" % e) self.action_result["result"] = "Json error during startup: %s" % e self.send_result(self.action_result, headers, stream_path) return @@ -3425,7 +3424,7 @@ class AppBase: errorstring = f"{e}" if "the JSON object must be" in errorstring: - self.logger.info("[ERROR] Something is wrong with the input for this function. Are lists and JSON data handled parsed properly?") + self.logger.info("[ERROR] Something is wrong with the input for this function. Are lists and JSON data handled parsed properly (0)?") try: e = json.loads(f"{e}") except: @@ -3455,7 +3454,7 @@ class AppBase: }) break except Exception as e: - self.logger.info("[ERROR] Something is wrong with the input for this function. Are lists and JSON data handled parsed properly?") + self.logger.info("[ERROR] Something is wrong with the input for this function. Are lists and JSON data handled parsed properly (1)?") try: e = json.loads(f"{e}") diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index 9125a3e9..9b4f011a 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -2,8 +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 @@ -15,7 +14,7 @@ require ( github.com/basgys/goxml2json v1.1.0 github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 github.com/docker/docker v20.10.12+incompatible - github.com/frikky/kin-openapi v0.41.0 + github.com/frikky/kin-openapi v0.42.0 github.com/fsouza/go-dockerclient v1.7.7 github.com/ghodss/yaml v1.0.0 github.com/go-git/go-billy/v5 v5.3.1 @@ -24,7 +23,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.87 + github.com/shuffle/shuffle-shared v0.3.5 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 6cde7541..97e6c285 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: frontend: #build: ./frontend - image: ghcr.io/frikky/shuffle-frontend:nightly + image: ghcr.io/shuffle/shuffle-frontend:nightly container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -17,7 +17,7 @@ services: - backend backend: #build: ./backend - image: ghcr.io/frikky/shuffle-backend:nightly + image: ghcr.io/shuffle/shuffle-backend:nightly container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: @@ -40,7 +40,7 @@ services: #- opensearch #Not necessary because dependancy is handled within the backend itself instead #- database orborus: - image: ghcr.io/frikky/shuffle-orborus:nightly + image: ghcr.io/shuffle/shuffle-orborus:nightly container_name: shuffle-orborus hostname: shuffle-orborus networks: diff --git a/frontend/package.json b/frontend/package.json index d6d5577c..c7431952 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,6 +47,7 @@ "react": "^16.14.0", "react-alert": "^5.5.0", "react-alert-template-basic": "^1.0.0", + "react-alice-carousel": "^2.6.4", "react-avatar-editor": "^11.1.0", "react-beforeunload": "^2.2.1", "react-chartjs-2": "^2.11.1", diff --git a/frontend/src/components/DetectionFramework.jsx b/frontend/src/components/AppFramework.jsx similarity index 91% rename from frontend/src/components/DetectionFramework.jsx rename to frontend/src/components/AppFramework.jsx index db8e0850..7862f165 100644 --- a/frontend/src/components/DetectionFramework.jsx +++ b/frontend/src/components/AppFramework.jsx @@ -1,12 +1,15 @@ import React, { useState, useEffect } from 'react'; -import { securityFramework } from "./LandingpageUsecases.jsx"; +import { securityFramework} from "./LandingpageUsecases.jsx"; import CytoscapeComponent from 'react-cytoscapejs'; import frameworkStyle from '../frameworkStyle.jsx'; import AppSearch from './Appsearch.jsx'; import { v4 as uuidv4 } from "uuid"; import theme from '../theme'; import { useAlert } from "react-alert"; +import { usecaseTypes } from "../components/UsecaseSearch.jsx" +import PaperComponent from "../components/PaperComponent.jsx" +import SuggestedWorkflows from "../components/SuggestedWorkflows.jsx" import { Paper, @@ -16,6 +19,7 @@ import { Badge, CircularProgress, Tooltip, + Dialog, } from "@material-ui/core"; import { @@ -518,24 +522,105 @@ export const usecases = { } const Framework = (props) => { - const { globalUrl, isLoaded, showOptions, selectedOption, rolling, frameworkData, size, inputUsecase, isLoggedIn, color, discoveryWrapper, setDiscoveryWrapper} = props; - + const { globalUrl, isLoaded, showOptions, selectedOption, rolling, frameworkData, setFrameworkData, size, inputUsecase, isLoggedIn, color, discoveryWrapper, setDiscoveryWrapper, userdata, apps, } = props; const [cy, setCy] = React.useState() const [edgesStarted, setEdgesStarted] = React.useState(false) const [graphDone, setGraphDone] = React.useState(false) const [cyDone, setCyDone] = React.useState(false) const [discoveryData, setDiscoveryData] = React.useState({}) const [selectionOpen, setSelectionOpen] = React.useState(true) + const [frameworkSuggestions, setFrameworkSuggestions] = React.useState([]) const [newSelectedApp, setNewSelectedApp] = React.useState({}) const [defaultSearch, setDefaultSearch] = React.useState("") const [animationStarted, setAnimationStarted] = React.useState(false) const [paperTitle, setPaperTitle] = React.useState("") + const [changedApp, setChangedApp] = React.useState("") + + + const [usecaseType, setUsecaseType] = React.useState(0) + const [selectedUsecase, setSelectedUsecase] = React.useState(selectedOption !== undefined ? selectedOption : "Phishing") + const scale = size === undefined ? 1 : size > 5 ? 3 : size const alert = useAlert() + const showRecommendations = (changed, frameworkData) => { + console.log("Inside recommendation loader") + setChangedApp(changed) + + // FIX: + // 0. Get workflows loaded in from usecasesearch + // 1. Search through workflow templates for matching app types + // 2. Validate if template is already in use~ (workflows with same tools) + // 3. Generate the workflow(s) - PS: Fix new workflow templates + // 4. Moving on! + + // How can we load templates? UsecaseSearch? + var showusecases = [] + //const foundusecase = usecaseTypes.find(data => data.name.toLowerCase() === defaultSearch.toLowerCase()) + for (var key in usecaseTypes) { + for (var subkey in usecaseTypes[key].value) { + const usecase = usecaseTypes[key].value[subkey] + + if (usecase.active === false) { + continue + } + + var potential = false + var matches = [] + for (var itemtype in usecase.items) { + const apptype = usecase.items[itemtype].app_type.toLowerCase() + //console.log("OLD: ", changed, "USECASE: ", apptype) + + if (changed.toLowerCase() === apptype || changed.toLowerCase().includes(apptype)) { + potential = true + + if (frameworkData[apptype].name !== undefined && frameworkData[apptype].name !== null && frameworkData[apptype].name.length > 0) { + usecase.items[itemtype].app = frameworkData[apptype] + } + + matches.push(usecase.items[itemtype]) + } else { + // Check if the type is done in frameworkData + if (frameworkData[apptype] !== undefined) { + //console.log("NOT UNDEFINED: ", frameworkData[apptype]) + if (frameworkData[apptype].name !== undefined && frameworkData[apptype].name !== null && frameworkData[apptype].name.length > 0) { + console.log("FOUND: ", frameworkData[apptype]) + usecase.items[itemtype].app = frameworkData[apptype] + + //console.log("Real app!") + matches.push(usecase.items[itemtype]) + } + + //if (frameworkData[apptype] !== undefined) { + } else { + console.log("UNDEFINED APP (bad name?): ", apptype) + } + } + } + + if (potential) { + if (matches.length === usecase.items.length) { + usecase.color = "#c51152" + usecase.type = usecaseTypes[key].name + showusecases.push(usecase) + } + } + } + } + + // FIXME: Check if a usecase has already been handled + console.log("") + console.log("GOT USECASES: ", showusecases) + + // FIXME: Just showing one usecase at a time for now + if (showusecases.length > 0) { + setFrameworkSuggestions(showusecases.slice(0,1)) + } + } + useEffect(() => { - //console.log("DISCWRAP CHANG: ", discoveryWrapper) + console.log("DISCWRAP CHANG: ", discoveryWrapper) if (discoveryWrapper === undefined || discoveryWrapper.id === "SHUFFLE" || discoveryWrapper.id === undefined || cy === undefined) { setDiscoveryData({}) @@ -548,26 +633,30 @@ const Framework = (props) => { // Find the node and click it? - setTimeout(() => { - const nodes = cy.nodes().jsons() - for (var key in nodes) { - const node = nodes[key] - var newSearchName = discoveryWrapper.id.valueOf() - if (newSearchName === "EMAIL") { - newSearchName = "COMMS" - } - - if (node.data.id === newSearchName) { - const tmpnode = cy.getElementById(node.data.id) - if (tmpnode !== undefined) { - tmpnode.select() - } - - setDefaultSearch(discoveryWrapper.id) - setPaperTitle(discoveryWrapper.id) - } + //setTimeout(() => { + const nodes = cy.nodes().jsons() + for (var key in nodes) { + const node = nodes[key] + var newSearchName = discoveryWrapper.id.valueOf() + if (newSearchName === "EMAIL") { + newSearchName = "COMMS" } - }, 50,) + + if (newSearchName === "ERADICATION" || newSearchName === "ENDPOINT") { + newSearchName = "EDR & AV" + } + + if (node.data.id === newSearchName) { + const tmpnode = cy.getElementById(node.data.id) + if (tmpnode !== undefined) { + tmpnode.select() + } + + setDefaultSearch(discoveryWrapper.id) + setPaperTitle(discoveryWrapper.id) + } + } + //}, 50,) //setDiscoveryData(discoveryWrapper) }, [discoveryWrapper]) @@ -652,17 +741,23 @@ const Framework = (props) => { } useEffect(() => { - if (discoveryData.id === undefined) { - return - } - + console.log(newSelectedApp, discoveryData) if (newSelectedApp.objectID === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID.length === 0) { return } - if (paperTitle.length > 0) { - setDiscoveryWrapper({}) - setSelectionOpen(false) + //if (paperTitle.length > 0) { + // console.log("No papertitle (parent button)") + + // cy.elements().unselect() + // return + //} + + if (discoveryData.id === undefined) { + console.log("No discoverydata (parent button)") + + cy.elements().unselect() + return } const submitValue = { @@ -683,7 +778,29 @@ const Framework = (props) => { foundelement.data("height", `${85*scale}px`) } + if (setFrameworkData !== undefined) { + // Find discoveryData.id + var keys = [] + for (const [key, value] of Object.entries(frameworkData)) { + if (key.toLowerCase() === discoveryData.id.toLowerCase()) { + keys.push(key) + } + } + + if (keys.length === 0) { + console.log("Failed to find: ", discoveryData.id, " IN ", frameworkData) + } else { + for (var key in keys) { + frameworkData[keys[key]] = submitValue + } + + setFrameworkData(frameworkData) + showRecommendations(discoveryData.id, frameworkData) + } + } + setFrameworkItem(submitValue) + cy.elements().unselect() }, [newSelectedApp]) @@ -792,8 +909,6 @@ const Framework = (props) => { //console.log("Framework - update? ", parsedFrameworkData) // 0 = automated, 1 = manual - const [usecaseType, setUsecaseType] = React.useState(0) - const [selectedUsecase, setSelectedUsecase] = React.useState(selectedOption !== undefined ? selectedOption : "Phishing") const elements = [] const surfaceColor = "#27292D" @@ -966,14 +1081,60 @@ const Framework = (props) => { ) } + const onNodeUnselect = (event) => { + var data = event.target.data(); + console.log("UNSELECT: ", data) + + var parsedStyle = { + "border-width": "10px", + "border-opacity": ".7", + "border-color": "#7fe57f", + } + + if (event.target !== undefined && event.target !== null) { + event.target.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) + + setTimeout(() => { + event.target.animate( + { + style: { + "border-width": "3px", + }, + }, + { + duration: animationDuration, + } + ) + }, 2500) + } + + //setDiscoveryData({}) + setDiscoveryWrapper({}) + setSelectionOpen(false) + setDefaultSearch("") + setPaperTitle("") + + //setDiscoveryData({}) + } const onNodeSelect = (event) => { - const data = event.target.data(); + var data = event.target.data(); console.log("Node: ", data) if (data.id === "SHUFFLE") { event.target.unselect() return } + + if (data.label === "EDR & AV") { + data.label = "ERADICATION" + } setDiscoveryData(data) setSelectionOpen(true) @@ -1009,6 +1170,9 @@ const Framework = (props) => { cy.on("select", "node", (e) => { onNodeSelect(e) }) + cy.on("unselect", "node", (e) => { + onNodeUnselect(e) + }) cy.on("mouseover", "node", (e) => {onNodeHover(e)}) cy.on("mouseout", "node", (e) => onNodeHoverOut(e)); @@ -1547,8 +1711,21 @@ const Framework = (props) => { var usecasediff = -100 const bgColor = color === undefined || color === null || color.length === 0 ? theme.palette.surfaceColor : color - return ( + return (
+
+ + +
+ {showOptions === false ? null :
{Object.keys(usecases).map((data, index) => { @@ -1580,7 +1757,7 @@ const Framework = (props) => { { Object.getOwnPropertyNames(discoveryData).length > 0 ? - + {paperTitle.length > 0 ? @@ -1718,6 +1895,8 @@ const Framework = (props) => { defaultSearch={defaultSearch} newSelectedApp={newSelectedApp} setNewSelectedApp={setNewSelectedApp} + userdata={userdata} + cy={cy} /> : null}
diff --git a/frontend/src/components/Appsearch.jsx b/frontend/src/components/Appsearch.jsx index b063a204..0573883d 100644 --- a/frontend/src/components/Appsearch.jsx +++ b/frontend/src/components/Appsearch.jsx @@ -10,10 +10,11 @@ import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } import algoliasearch from 'algoliasearch'; import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom'; import { Grid, Paper, TextField, ButtonBase, InputAdornment, Typography, Button, Tooltip} from '@material-ui/core'; +import aa from 'search-insights' const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") -const WorkflowSearch = props => { - const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits } = props +const Appsearch = props => { + const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, } = props const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; @@ -140,19 +141,19 @@ const WorkflowSearch = props => { } counted += 1 - var parsedname = "" - for (var key = 0; key < data.name.length; key++) { - var character = data.name.charAt(key) - if (character === character.toUpperCase()) { - //console.log(data.name[key], data.name[key+1]) - if (data.name.charAt(key+1) !== undefined && data.name.charAt(key+1) === data.name.charAt(key+1).toUpperCase()) { - } else { - parsedname += " " - } - } + var parsedname = data.name.valueOf() + //for (var key = 0; key < data.name.length; key++) { + // var character = data.name.charAt(key) + // if (character === character.toUpperCase()) { + // //console.log(data.name[key], data.name[key+1]) + // if (data.name.charAt(key+1) !== undefined && data.name.charAt(key+1) === data.name.charAt(key+1).toUpperCase()) { + // } else { + // parsedname += " " + // } + // } - parsedname += character - } + // parsedname += character + //} parsedname = (parsedname.charAt(0).toUpperCase()+parsedname.substring(1)).replaceAll("_", " ") @@ -180,6 +181,32 @@ const WorkflowSearch = props => { label: "", }) } + + const queryID = "" + + if (queryID !== undefined && queryID !== null) { + try { + aa('init', { + appId: searchClient.appId, + apiKey: searchClient.transporter.headers["x-algolia-api-key"] + }) + + const timestamp = new Date().getTime() + aa('sendEvents', [ + { + eventType: 'conversion', + eventName: 'App Framework Activation', + index: 'appsearch', + objectIDs: [data.objectID], + timestamp: timestamp, + queryID: queryID, + userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id, + } + ]) + } catch (e) { + console.log("Failed algolia search update: ", e) + } + } }}>
{data.name} @@ -215,4 +242,4 @@ const WorkflowSearch = props => { ) } -export default WorkflowSearch; +export default Appsearch; diff --git a/frontend/src/components/AppsearchPopout.jsx b/frontend/src/components/AppsearchPopout.jsx new file mode 100644 index 00000000..7e15dc67 --- /dev/null +++ b/frontend/src/components/AppsearchPopout.jsx @@ -0,0 +1,189 @@ +import React, { useState, useEffect } from 'react'; + +import theme from '../theme'; +import AppSearch from './Appsearch.jsx'; + +import { + Paper, + Typography, + Divider, + IconButton, + Badge, + CircularProgress, + Tooltip, + Button, +} from "@material-ui/core"; + +import { + Close as CloseIcon, + Delete as DeleteIcon, +} from "@material-ui/icons"; + +const AppSearchPopout = (props) => { + const { + cy, + paperTitle, + setPaperTitle, + newSelectedApp, + setNewSelectedApp, + selectionOpen, + setSelectionOpen, + discoveryData, + setDiscoveryData, + userdata, + } = props; + + const [defaultSearch, setDefaultSearch] = React.useState(paperTitle !== undefined ? paperTitle : "") + + if (selectionOpen !== true) { + return null + } + + return ( + + {paperTitle !== undefined && paperTitle.length > 0 ? + + + {paperTitle} + + + + : null} + + { + //cy.elements().unselectify(); + if (cy !== undefined) { + cy.elements().unselect() + } + + e.preventDefault(); + setSelectionOpen(false) + }} + > + + + + {/* {/*Causes errors in Cytoscape. Removing for now.} + + { + e.preventDefault(); + setDiscoveryData({ + "id": discoveryData.id, + "label": discoveryData.label, + "name": "" + }) + setNewSelectedApp({ + "image_url": "", + "name": "", + "id": "", + "objectID": "remove", + }) + setSelectionOpen(true) + setDefaultSearch("") + + const foundelement = cy.getElementById(discoveryData.id) + if (foundelement !== undefined && foundelement !== null) { + console.log("element: ", foundelement) + foundelement.data("large_image", discoveryData.large_image) + foundelement.data("text_margin_y", "14px") + foundelement.data("margin_x", "32px") + foundelement.data("margin_y", "19x") + foundelement.data("width", "45px") + foundelement.data("height", "45px") + } + + setTimeout(() => { + setDiscoveryData({}) + setNewSelectedApp({}) + }, 1000) + }} + > + + + + */} +
+ {discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ? +
+ + {discoveryData.id} 0 ? newSelectedApp.image_url : discoveryData.large_image} style={{height: 40, width: 40, margin: "auto",}}/> +
+ : + {discoveryData.id} + } + + {discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ? + discoveryData.name + : + newSelectedApp.name !== undefined && newSelectedApp.name !== null && newSelectedApp.name.length > 0 ? + newSelectedApp.name + : + `No ${discoveryData.label} app chosen` + } + +
+
+ {discoveryData !== undefined && discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ? + + + {discoveryData.description} + + {/*isCloud && defaultSearch !== undefined && defaultSearch.length > 0 ? + {< + newSelectedApp={newSelectedApp} + setNewSelectedApp={setNewSelectedApp} + defaultSearch={defaultSearch} + />} + : + null + */} + + : + selectionOpen + ? + + + Click an app below to select it + + + : + + } +
+
+ {selectionOpen ? + + : null} +
+
+ ) +} + +export default AppSearchPopout; diff --git a/frontend/src/components/AuthenticationItem.jsx b/frontend/src/components/AuthenticationItem.jsx new file mode 100644 index 00000000..45879519 --- /dev/null +++ b/frontend/src/components/AuthenticationItem.jsx @@ -0,0 +1,278 @@ +import React, { useState, useEffect } from "react"; + +import theme from '../theme'; +import { useAlert } from "react-alert"; +import { + Tooltip, + IconButton, + ListItem, + ListItemText, + FormGroup, + FormControl, + InputLabel, + FormLabel, + FormControlLabel, + Select, + MenuItem, + Grid, + Paper, + Typography, + TextField, + Zoom, +} from "@material-ui/core"; + +import { + Edit as EditIcon, + Delete as DeleteIcon, + SelectAll as SelectAllIcon, +} from "@material-ui/icons"; + +const AuthenticationItem = (props) => { + const { data, index, globalUrl, getAppAuthentication } = props + + const [selectedAuthentication, setSelectedAuthentication] = React.useState({}) + const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] = React.useState(false); + const [authenticationFields, setAuthenticationFields] = React.useState([]); + + const alert = useAlert(); + var bgColor = "#27292d"; + if (index % 2 === 0) { + bgColor = "#1f2023"; + } + + //console.log("Auth data: ", data) + if (data.type === "oauth2") { + data.fields = [ + { + key: "url", + value: "Secret. Replaced during app execution!", + }, + { + key: "client_id", + value: "Secret. Replaced during app execution!", + }, + { + key: "client_secret", + value: "Secret. Replaced during app execution!", + }, + { + key: "scope", + value: "Secret. Replaced during app execution!", + }, + ]; + } + + const deleteAuthentication = (data) => { + alert.info("Deleting auth " + data.label); + + // Just use this one? + const url = globalUrl + "/api/v1/apps/authentication/" + data.id; + console.log("URL: ", url); + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + console.log("RESP: ", responseJson); + if (responseJson["success"] === false) { + alert.error("Failed deleting auth"); + } else { + // Need to wait because query in ES is too fast + setTimeout(() => { + getAppAuthentication(); + }, 1000); + //alert.success("Successfully deleted authentication!") + } + }) + ) + .catch((error) => { + console.log("Error in userdata: ", error); + }); + } + + const editAuthenticationConfig = (id) => { + const data = { + id: id, + action: "assign_everywhere", + }; + const url = globalUrl + "/api/v1/apps/authentication/" + id + "/config"; + + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + alert.error("Failed overwriting appauth in workflows"); + } else { + alert.success("Successfully updated auth everywhere!"); + //setSelectedUserModalOpen(false); + setTimeout(() => { + getAppAuthentication(); + }, 1000); + } + }) + ) + .catch((error) => { + alert.error("Err: " + error.toString()); + }); + }; + + const updateAppAuthentication = (field) => { + setSelectedAuthenticationModalOpen(true); + setSelectedAuthentication(field); + //{selectedAuthentication.fields.map((data, index) => { + var newfields = []; + for (var key in field.fields) { + newfields.push({ + key: field.fields[key].key, + value: "", + }); + } + setAuthenticationFields(newfields); + } + + return ( + + + style={{ minWidth: 75, maxWidth: 75 }} + /> + + + {/* + + */} + + {/* + + */} + { + return data.key; + }) + .join(", ") + } + style={{ + minWidth: 125, + maxWidth: 125, + overflow: "hidden", + }} + /> + + + { + updateAppAuthentication(data); + }} + > + + + {data.defined ? ( + + { + editAuthenticationConfig(data.id); + }} + > + + + + ) : ( + + {}} + > + + + + )} + { + deleteAuthentication(data); + }} + > + + + + + ) + } + +export default AuthenticationItem diff --git a/frontend/src/components/AuthenticationNormal.jsx b/frontend/src/components/AuthenticationNormal.jsx new file mode 100644 index 00000000..aae3a128 --- /dev/null +++ b/frontend/src/components/AuthenticationNormal.jsx @@ -0,0 +1,366 @@ +import React, { useState, useEffect } from "react"; +import theme from '../theme'; +import { v4 as uuidv4 } from "uuid"; + + +import { + Button, + Divider, + Select, + MenuItem, + TextField, + DialogActions, + DialogTitle, + DialogContent, + Typography, +} from "@material-ui/core"; + +import { + LockOpen as LockOpenIcon, +} from "@material-ui/icons"; + +const AuthenticationData = (props) => { + const { + globalUrl, + saveWorkflow, + selectedApp, + workflow, + selectedAction, + authenticationType, + getAppAuthentication, + appAuthentication, + setSelectedAction, + setAuthenticationModalOpen, + isCloud, + } = props; + + const setNewAppAuth = (appAuthData) => { + console.log("DAta: ", appAuthData); + 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!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + alert.error("Failed to set app auth: " + responseJson.reason); + } else { + if (getAppAuthentication !== undefined) { + getAppAuthentication() + } + + if (setAuthenticationModalOpen !== undefined) { + setAuthenticationModalOpen(false) + } + + // Needs a refresh with the new authentication.. + //alert.success("Successfully saved new app auth") + } + }) + .catch((error) => { + //alert.error(error.toString()); + console.log("New auth error: ", error.toString()); + }); + } + + const [authenticationOption, setAuthenticationOptions] = React.useState({ + app: JSON.parse(JSON.stringify(selectedApp)), + fields: {}, + label: "", + usage: [ + { + workflow_id: workflow.id, + }, + ], + id: uuidv4(), + active: true, + }); + + if ( + selectedApp.authentication === undefined || + selectedApp.authentication.parameters === null || + selectedApp.authentication.parameters === undefined || + selectedApp.authentication.parameters.length === 0 + ) { + return ( + + + {selectedApp.name} does not require authentication + + + ); + } + + authenticationOption.app.actions = []; + + for (var key in selectedApp.authentication.parameters) { + if ( + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ] === undefined + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ] = ""; + } + } + + const handleSubmitCheck = () => { + console.log("NEW AUTH: ", authenticationOption); + if (authenticationOption.label.length === 0) { + authenticationOption.label = `Auth for ${selectedApp.name}`; + } + + // Automatically mapping fields that already exist (predefined). + // Warning if fields are NOT filled + for (var key in selectedApp.authentication.parameters) { + if ( + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ].length === 0 + ) { + if ( + selectedApp.authentication.parameters[key].value !== undefined && + selectedApp.authentication.parameters[key].value !== null && + selectedApp.authentication.parameters[key].value.length > 0 + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ] = selectedApp.authentication.parameters[key].value; + } else { + if ( + selectedApp.authentication.parameters[key].schema.type === "bool" + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[key].name + ] = "false"; + } else { + alert.info( + "Field " + + selectedApp.authentication.parameters[key].name + + " can't be empty" + ); + return; + } + } + } + } + + console.log("Action: ", selectedAction); + selectedAction.authentication_id = authenticationOption.id; + selectedAction.selectedAuthentication = authenticationOption; + if ( + selectedAction.authentication === undefined || + selectedAction.authentication === null + ) { + selectedAction.authentication = [authenticationOption]; + } else { + selectedAction.authentication.push(authenticationOption); + } + + setSelectedAction(selectedAction); + + var newAuthOption = JSON.parse(JSON.stringify(authenticationOption)); + var newFields = []; + for (const key in newAuthOption.fields) { + const value = newAuthOption.fields[key]; + newFields.push({ + key: key, + value: value, + }); + } + + console.log("FIELDS: ", newFields); + newAuthOption.fields = newFields; + setNewAppAuth(newAuthOption); + + //if (configureWorkflowModalOpen) { + // setSelectedAction({}); + //} + + //setUpdate(authenticationOption.id); + }; + + if ( + authenticationOption.label === null || + authenticationOption.label === undefined + ) { + authenticationOption.label = selectedApp.name + " authentication"; + } + + return ( +
+ +
+ Authentication for {selectedApp.name} +
+
+ + + What is app authentication? + +
+ These are required fields for authenticating with {selectedApp.name} +
+ Name - what is this used for? + { + authenticationOption.label = event.target.value; + }} + /> + +
+ {selectedApp.authentication.parameters.map((data, index) => { + return ( +
+ + {data.name} + + {data.schema !== undefined && + data.schema !== null && + data.schema.type === "bool" ? ( + + ) : ( + { + authenticationOption.fields[data.name] = + event.target.value; + }} + /> + )} +
+ ); + })} + + + + + +
+ ); +}; + +export default AuthenticationData diff --git a/frontend/src/components/ConfigureWorkflow.jsx b/frontend/src/components/ConfigureWorkflow.jsx index fbd7eb79..66c48ab2 100644 --- a/frontend/src/components/ConfigureWorkflow.jsx +++ b/frontend/src/components/ConfigureWorkflow.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { InputAdornment, @@ -13,6 +13,7 @@ import { List, ListItem, ListItemText, + Fade, } from "@material-ui/core"; import { FavoriteBorder as FavoriteBorderIcon } from "@material-ui/icons"; import { FixName } from "../views/Apps.jsx"; @@ -43,14 +44,15 @@ const ConfigureWorkflow = (props) => { isCloud, setAuthenticationType, alert, + showTriggers, } = props; const [requiredActions, setRequiredActions] = React.useState([]); const [requiredVariables, setRequiredVariables] = React.useState([]); const [requiredTriggers, setRequiredTriggers] = React.useState([]); const [previousAuth, setPreviousAuth] = React.useState(appAuthentication); - const [firstLoad, setFirstLoad] = React.useState(""); const [itemChanged, setItemChanged] = React.useState(false); - var finished = false; + const [firstLoad, setFirstLoad] = React.useState(""); + const [showFinalizeAnimation, setShowFinalizeAnimation] = React.useState(false); if (workflow === undefined || workflow === null) { return null; @@ -94,18 +96,13 @@ const ConfigureWorkflow = (props) => { }; if (firstLoad.length === 0 || firstLoad !== workflow.id) { - if (finished) { - setConfigureWorkflowModalOpen(false); - return null; - } - if (apps === undefined || apps === null || apps.length === 0) { console.log("No apps loaded: ", apps); setConfigureWorkflowModalOpen(false); return null; } - setFirstLoad(workflow.id); + setFirstLoad(workflow.id) const newactions = []; for (var key in workflow.actions) { const action = workflow.actions[key]; @@ -272,17 +269,19 @@ const ConfigureWorkflow = (props) => { setRequiredTriggers(requiredTriggers); setRequiredVariables(requiredVariables); setRequiredActions(newactions); - } + } if (appAuthentication.length !== previousAuth.length) { - var newactions = []; + var newactions = [] for (var actionkey in requiredActions) { var newaction = requiredActions[actionkey]; const app = newaction.app; for (var key in appAuthentication) { const auth = appAuthentication[key]; - if (auth.app.name === app.name && auth.active) { + + // 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; } @@ -502,6 +501,7 @@ const ConfigureWorkflow = (props) => { return ( + {/* { secondary={action.app_version} style={{}} /> - {action.must_authenticate ? ( - action.auth_done ? ( - - ) : selectedAction.app_name === action.app_name ? ( - - ) : ( - - ) - ) : null} + {action.app_name} + + {action.auth_done ? "Authenticated" : `Authenticate ${action.app_name}`} + + + : null} {action.must_activate ? ( - ) : null} {action.update_version !== action.app_version ? ( ) : null} ); - }; + } + + // Based on the color here. Default: #f86a3e + //backgroundColor: selectedUsecaseCategory === usecase.name ? usecase.color : theme.palette.surfaceColor, + const topColor = "#f86a3e, #fc3922" return (
- {workflow.name} - - The following configuration makes the workflow ready immediately. - - {requiredActions.length > 0 ? ( - - - Actions - - - {requiredActions.map((data, index) => { - return ; - })} - - - ) : null} +
+
+
+ {workflow.name} + + The following configuration makes the workflow ready immediately. + + {requiredActions.length > 0 ? ( + + + Required Actions + + + {requiredActions.map((data, index) => { + return ( + + ) + })} + + + ) : null} - {requiredVariables.length > 0 ? ( - - - Variables - - - {requiredVariables.map((data, index) => { - return ; - })} - - - ) : null} + {requiredVariables.length > 0 ? ( + + + Variables + + + {requiredVariables.map((data, index) => { + return ; + })} + + + ) : null} - {requiredTriggers.length > 0 ? ( - - - Triggers - - - {requiredTriggers.map((data, index) => { - return ; - })} - - - ) : null} -
- - {/* - - */} - - -
+ {requiredTriggers.length > 0 && showTriggers !== false ? ( + + + Triggers + + + {requiredTriggers.map((data, index) => { + return ; + })} + + + ) : null} + +
+ {showFinalizeAnimation ? + finalize workflow animation { + console.log("Img loaded.") + setTimeout(() => { + console.log("Img closing.") + setConfigureWorkflowModalOpen(false); + }, 1250) + + }}/> + : + + {/* + + */} + + + } +
+
); }; diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx index b7fefd3a..57852c5d 100644 --- a/frontend/src/components/EditWorkflow.jsx +++ b/frontend/src/components/EditWorkflow.jsx @@ -2,6 +2,7 @@ import React, { useEffect, useContext } from "react"; import theme from '../theme'; import { isMobile } from "react-device-detect" import ChipInput from "material-ui-chip-input"; +import UsecaseSearch from "../components/UsecaseSearch.jsx" import { Badge, @@ -45,19 +46,91 @@ import { } from "@material-ui/icons"; const EditWorkflow = (props) => { - const { workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, } = props + const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, } = props const [submitLoading, setSubmitLoading] = React.useState(false); - const [selectedUsecases, setSelectedUsecases] = React.useState([]); const [showMoreClicked, setShowMoreClicked] = React.useState(false); const [innerWorkflow, setInnerWorkflow] = React.useState(workflow) const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove const [newWorkflowTags, setNewWorkflowTags] = React.useState(workflow.tags !== undefined && workflow.tags !== null ? JSON.parse(JSON.stringify(workflow.tags)) : []) + const [selectedUsecases, setSelectedUsecases] = React.useState(workflow.usecase_ids !== undefined && workflow.usecase_ids !== null ? JSON.parse(JSON.stringify(workflow.usecase_ids)) : []); + const [foundWorkflowId, setFoundWorkflowId] = React.useState("") + const [name, setName] = React.useState(workflow.name !== undefined ? workflow.name : "") + const [description, setDescription] = React.useState(workflow.description !== undefined ? workflow.description : "") - if (workflow === undefined || workflow.id === undefined || setWorkflow === undefined || modalOpen !== true) { + + // Gets the generated workflow + const getGeneratedWorkflow = (workflow_id) => { + fetch(globalUrl + "/api/v1/workflows/" + workflow_id, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 when getting workflow"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.id === workflow_id) { + console.log("GOT WORKFLOW: ", responseJson) + if (name === "") { + innerWorkflow.name = responseJson.name + setName(responseJson.name) + } + + if (description === "") { + innerWorkflow.description = responseJson.description + setDescription(description) + } + + if (newWorkflowTags === []) { + innerWorkflow.tags = responseJson.tags + setNewWorkflowTags(responseJson.tags) + } + + if (selectedUsecases === []) { + selectedUsecases = responseJson.usecase_ids + } + + innerWorkflow.id = responseJson.id + innerWorkflow.blogpost = responseJson.blogpost + innerWorkflow.actions = responseJson.actions + innerWorkflow.triggers = responseJson.triggers + innerWorkflow.branches = responseJson.branches + innerWorkflow.comments = responseJson.comments + innerWorkflow.workflow_variables = responseJson.workflow_variables + innerWorkflow.execution_variables = responseJson.execution_variables + + + setInnerWorkflow(innerWorkflow) + setUpdate(Math.random()) + } + }) + .catch((error) => { + //alert.error(error.toString()); + console.log("Get workflow error: ", error.toString()); + }) + } + + if (foundWorkflowId.length > 0) { + getGeneratedWorkflow(foundWorkflowId) + + setFoundWorkflowId("") + } else { + } + + if (modalOpen !== true) { return null } + const newWorkflow = isEditing === true ? false : true + var upload = ""; var total_count = 0 @@ -71,264 +144,299 @@ const EditWorkflow = (props) => { style: { backgroundColor: theme.palette.surfaceColor, color: "white", - minWidth: isMobile ? "90%" : "800px", - maxWidth: isMobile ? "90%" : "800px", + minWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550, + maxWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550, + minHeight: 400, }, }} > - -
- {workflow.id !== undefined ? "Editing" : "New"} workflow - {showUpload === true ? -
- - - -
+ +
+
+ + {newWorkflow ? "New" : "Editing"} workflow + + + 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 + + {showUpload === true ? +
+ + + +
+ : null} +
+ {newWorkflow === true ? +
+ + Use a Template + + + Start your workflow from our templating system. This uses publied workflows from our Creators to generate full Usecases or parts of your Workflow. + +
: null} -
+
- - { - innerWorkflow.name = event.target.value - }} - InputProps={{ - style: { - color: "white", - }, - }} - color="primary" - placeholder="Name" - required - margin="dense" - defaultValue={innerWorkflow.name} - label="Name" - autoFocus - fullWidth - /> - { - innerWorkflow.description = event.target.value - setInnerWorkflow(innerWorkflow) - }} - InputProps={{ - style: { - color: "white", - }, - }} - color="primary" - defaultValue={innerWorkflow.description} - placeholder="Description" - multiline - label="Description" - margin="dense" - fullWidth - /> -
- +
+ { + setName(event.target.value) }} - placeholder="Tags" - color="primary" - fullWidth - value={newWorkflowTags} - onAdd={(chip) => { - newWorkflowTags.push(chip); - setNewWorkflowTags(newWorkflowTags); + InputProps={{ + style: { + color: "white", + }, + }} + color="primary" + placeholder="Name" + required + margin="dense" + defaultValue={innerWorkflow.name} + label="Name" + autoFocus + fullWidth + /> + { + setDescription(event.target.value) }} - onDelete={(chip, index) => { - newWorkflowTags.splice(index, 1); - setNewWorkflowTags(newWorkflowTags); - }} - /> - {usecases !== null && usecases !== undefined && usecases.length > 0 ? - - Usecases - selected.join(', ')} + onChange={(event) => { + console.log("Changed: ", event) + }} + > + + None + + {usecases.map((usecase, index) => { + //console.log(usecase) + return ( + + + {usecase.name} + + {usecase.list.map((subcase, subindex) => { + //console.log(subcase) + total_count += 1 + return ( + { + if (selectedUsecases.includes(subcase.name)) { + const itemIndex = selectedUsecases.indexOf(subcase.name) + if (itemIndex > -1) { + selectedUsecases.splice(itemIndex, 1) + } + } else { + selectedUsecases.push(subcase.name) } - } else { - selectedUsecases.push(subcase.name) - } - setUpdate(Math.random()); - setSelectedUsecases(selectedUsecases) - }}> - - - - ) - })} - - ) - })} - - + setUpdate(Math.random()); + setSelectedUsecases(selectedUsecases) + }}> + + + + ) + })} + + ) + })} + + + : null} +
+ + {showMoreClicked === true ? + + + + Status + { + console.log("Data: ", e.target.value) + + innerWorkflow.workflow_type = e.target.value + setInnerWorkflow(innerWorkflow) + }} + > + } label="Test" /> + } label="Production" /> + + + +
+ + + Type + { + console.log("Data: ", e.target.value) + + innerWorkflow.workflow_type = e.target.value + setInnerWorkflow(innerWorkflow) + }} + > + } label="Trigger" /> + } label="Subflow" /> + } label="Standalone" /> + + + + + + { + innerWorkflow.blogpost = event.target.value + setInnerWorkflow(innerWorkflow) + }} + InputProps={{ + style: { + color: "white", + }, + }} + color="primary" + defaultValue={innerWorkflow.blogpost} + placeholder="A blogpost or other reference for how this work workflow was built, and what it's for." + rows="1" + label="blogpost" + margin="dense" + fullWidth + /> + { + innerWorkflow.video = event.target.value + setInnerWorkflow(innerWorkflow) + }} + InputProps={{ + style: { + color: "white", + }, + }} + color="primary" + defaultValue={innerWorkflow.video} + placeholder="A youtube or loom link to the video" + rows="1" + label="Video" + margin="dense" + fullWidth + /> + { + innerWorkflow.default_return_value = event.target.value + setInnerWorkflow(innerWorkflow) + }} + InputProps={{ + style: { + color: "white", + }, + }} + color="primary" + defaultValue={innerWorkflow.default_return_value} + placeholder="Default return value (used for Subflows if the subflow fails)" + rows="3" + multiline + label="Default return value" + margin="dense" + fullWidth + /> + : null} + + { + setShowMoreClicked(!showMoreClicked); + }} + > + {showMoreClicked ? : } + +
- - {showMoreClicked === true ? - - - - Status - { - console.log("Data: ", e.target.value) - - innerWorkflow.workflow_type = e.target.value - setInnerWorkflow(innerWorkflow) - }} - > - } label="Test" /> - } label="Production" /> - - - -
- - - Type - { - console.log("Data: ", e.target.value) - - innerWorkflow.workflow_type = e.target.value - setInnerWorkflow(innerWorkflow) - }} - > - } label="Trigger" /> - } label="Subflow" /> - } label="Standalone" /> - - - - - - { - innerWorkflow.blogpost = event.target.value - setInnerWorkflow(innerWorkflow) - }} - InputProps={{ - style: { - color: "white", - }, - }} - color="primary" - defaultValue={innerWorkflow.blogpost} - placeholder="A blogpost or other reference for how this work workflow was built, and what it's for." - rows="1" - label="blogpost" - margin="dense" - fullWidth + {newWorkflow === true ? +
+ - { - innerWorkflow.video = event.target.value - setInnerWorkflow(innerWorkflow) - }} - InputProps={{ - style: { - color: "white", - }, - }} - color="primary" - defaultValue={innerWorkflow.video} - placeholder="A youtube or loom link to the video" - rows="1" - label="Video" - margin="dense" - fullWidth - /> - { - innerWorkflow.default_return_value = event.target.value - setInnerWorkflow(innerWorkflow) - }} - InputProps={{ - style: { - color: "white", - }, - }} - color="primary" - defaultValue={innerWorkflow.default_return_value} - placeholder="Default return value (used for Subflows if the subflow fails)" - rows="3" - multiline - label="Default return value" - margin="dense" - fullWidth - /> - +
: null} - - { - setShowMoreClicked(!showMoreClicked); - }} - > - {showMoreClicked ? : } - - -
{isMobile ? null :
- +
} {isMobile ? null : diff --git a/frontend/src/components/Oauth2Auth.jsx b/frontend/src/components/Oauth2Auth.jsx index 43456cb3..68a9be36 100644 --- a/frontend/src/components/Oauth2Auth.jsx +++ b/frontend/src/components/Oauth2Auth.jsx @@ -1,5 +1,6 @@ import React, { useRef, useState, useEffect, useLayoutEffect } from "react"; import { useTheme } from "@material-ui/core/styles"; +import theme from '../theme'; import { v4 as uuidv4 } from "uuid"; import { @@ -62,6 +63,7 @@ const registeredApps = [ "outlook_office365", "microsoft_teams", "microsoft_teams_user_access", + "todoist", ] const AuthenticationOauth2 = (props) => { @@ -77,8 +79,8 @@ const AuthenticationOauth2 = (props) => { setNewAppAuth, setAuthenticationModalOpen, isCloud, + autoAuth, } = props; - const theme = useTheme(); //const [update, setUpdate] = React.useState("|") const [defaultConfigSet, setDefaultConfigSet] = React.useState( @@ -88,7 +90,7 @@ const AuthenticationOauth2 = (props) => { authenticationType.client_secret !== undefined && authenticationType.client_secret !== null && authenticationType.client_secret.length > 0 - ); + ); const [clientId, setClientId] = React.useState( defaultConfigSet ? authenticationType.client_id : "" @@ -123,6 +125,73 @@ const AuthenticationOauth2 = (props) => { return null; } + const startOauth2Request = () => { + console.log("APP: ", selectedApp) + if (selectedApp.name.toLowerCase() == "outlook_graph" || selectedApp.name.toLowerCase() == "outlook_office365") { + handleOauth2Request( + "efe4c3fe-84a1-4821-a84f-23a6cfe8e72d", + "", + "https://graph.microsoft.com", + ["Mail.ReadWrite"], + ); + } else if (selectedApp.name.toLowerCase() == "gmail") { + handleOauth2Request( + "253565968129-c0a35knic7q1pdk6i6qk9gdkvr07ci49.apps.googleusercontent.com", + "", + "https://gmail.googleapis.com", + ["https://www.googleapis.com/auth/gmail.modify", + "https://www.googleapis.com/auth/gmail.send", + "https://www.googleapis.com/auth/gmail.insert", + "https://www.googleapis.com/auth/gmail.compose"] + ) + } else if (selectedApp.name.toLowerCase() == "zoho_desk") { + handleOauth2Request( + "1000.ZR5MHUW6B0L6W1VUENFGIATFS0TOJT", + "", + "https://desk.zoho.com", + ["Desk.tickets.READ", + "Desk.tickets.UPDATE", + "Desk.tickets.DELETE", + "Desk.tickets.CREATE"] + ) + } else if (selectedApp.name.toLowerCase() == "slack") { + handleOauth2Request( + "151779186901.2448678750935", + "", + "https://slack.com", + ["admin", "chat:write", "im:read", "im:write", "search:read", "usergroups:read", "usergroups:write"] + ) + } else if (selectedApp.name.toLowerCase() == "webex") { + handleOauth2Request( + "Cab184f3d7271f540443c79b5b79845e3387abbbdb3db4233a87ea3a5432fb3d5", + "", + "https://webexapis.com", + ["spark:all"] + ) + } else if (selectedApp.name.toLowerCase().includes("microsoft_teams")) { + handleOauth2Request( + "31cb4c84-658e-43d5-ae84-22c9142e967a", + "", + "https://graph.microsoft.com", + ["ChannelMessage.Edit", "ChannelMessage.Read.All", "ChannelMessage.Send", "Chat.Create", "Chat.ReadWrite", "Chat.Read"] + ) + } else if (selectedApp.name.toLowerCase().includes("todoist")) { + handleOauth2Request( + "35fa3a384040470db0c8527e90a3c2eb", + "", + "https://api.todoist.com", + ["task:add"] + ) + } + } + + useEffect(() => { + console.log("Should automatically click the auto-auth button?") + if (autoAuth === true && selectedApp !== undefined) { + startOauth2Request() + } + }, []) + const handleOauth2Request = (client_id, client_secret, oauth_url, scopes) => { setButtonClicked(true); console.log("SCOPES: ", scopes); @@ -164,19 +233,15 @@ const AuthenticationOauth2 = (props) => { state += `%26refresh_uri%3d${authentication_url}`; } + // No prompt forcing + const url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=login&scope=${resources}&state=${state}&access_type=offline`; // Force new consent //const url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&scope=${resources}&prompt=consent&state=${state}&access_type=offline`; // Admin consent - //const url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&scope=${resources}&prompt=admin_consent&state=${state}&access_type=offline`; - - // Skip consent - const url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&scope=${resources}&state=${state}&access_type=offline`; - //const url = `https://accounts.zoho.com/oauth/v2/auth?response_type=code&client_id=${client_id}&scope=AaaServer.profile.Read&redirect_uri=${redirectUri}&prompt=consent` - //console.log("Full URI: ", url) - //console.log("Redirect Uri: ", redirectUri) - // &resource=https%3A%2F%2Fgraph.microsoft.com& + + // &resource=https%3A%2F%2Fgraph.microsoft.com& // FIXME: Awful, but works for prototyping // How can we get a callback properly realtime? @@ -188,12 +253,20 @@ const AuthenticationOauth2 = (props) => { var open = true; const timer = setInterval(() => { if (newwin.closed) { + console.log("Closed!") + + if (setAuthenticationModalOpen !== undefined) { + setAuthenticationModalOpen(false) + } + setButtonClicked(false); clearInterval(timer); //alert('"Secure Payment" window closed!'); getAppAuthentication(true, true); - } + } else { + console.log("Not closed") + } }, 1000); //do { // setTimeout(() => { @@ -361,76 +434,48 @@ const AuthenticationOauth2 = (props) => { {isCloud && registeredApps.includes(selectedApp.name.toLowerCase()) ? diff --git a/frontend/src/components/SecurityFramework.jsx b/frontend/src/components/SecurityFramework.jsx index cfe5196d..7139f7a6 100644 --- a/frontend/src/components/SecurityFramework.jsx +++ b/frontend/src/components/SecurityFramework.jsx @@ -1,6 +1,6 @@ import React, {useState } from 'react'; import {isMobile} from "react-device-detect"; -import DetectionFramework, { usecases } from "../components/DetectionFramework.jsx"; +import AppFramework, { usecases } from "../components/AppFramework.jsx"; import {Link} from 'react-router-dom'; import ReactGA from 'react-ga'; @@ -56,6 +56,8 @@ export const securityFramework = [ ] const LandingpageUsecases = (props) => { + const { userdata } = props + const [selectedUsecase, setSelectedUsecase] = useState("Phishing") const usecasekeys = usecases === undefined || usecases === null ? [] : Object.keys(usecases) const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)" @@ -169,7 +171,12 @@ const LandingpageUsecases = (props) => {
{isMobile ? null :
- +
} {isMobile ? null : diff --git a/frontend/src/components/ShuffleCodeEditor.jsx b/frontend/src/components/ShuffleCodeEditor.jsx index 2592fb3c..5ba568f2 100644 --- a/frontend/src/components/ShuffleCodeEditor.jsx +++ b/frontend/src/components/ShuffleCodeEditor.jsx @@ -305,7 +305,6 @@ const CodeEditor = (props) => { variable_occurence = new_occurences.valueOf() console.log("Match2: ", variable_occurence) - // console.log(variable_occurence) // console.log() @@ -410,7 +409,7 @@ const CodeEditor = (props) => { // return //} - //console.log("FOUND: ", found) + console.log("FOUND: ", found) // Whelp this is inefficient af. Single loop pls // When the found array is empty. @@ -426,8 +425,15 @@ const CodeEditor = (props) => { for (var j = 0; j < actionlist.length; j++) { if(fixedVariable.slice(1,).toLowerCase() === actionlist[j].autocomplete.toLowerCase()){ valuefound = true + + console.log("Valuefound: ", fixedVariable, actionlist[j].example) + try { - input = input.replace(fixedVariable, JSON.stringify(actionlist[j].example)); + if (actionlist[j].example.trim().startsWith("{") || actionlist[j].example.trim().startsWith("[")) { + input = input.replace(fixedVariable, JSON.stringify(actionlist[j].example)); + } else { + input = input.replace(fixedVariable, actionlist[j].example) + } } catch (e) { input = input.replace(fixedVariable, actionlist[j].example) } diff --git a/frontend/src/components/SuggestedWorkflows.jsx b/frontend/src/components/SuggestedWorkflows.jsx new file mode 100644 index 00000000..47fa214f --- /dev/null +++ b/frontend/src/components/SuggestedWorkflows.jsx @@ -0,0 +1,235 @@ +import React, { useState, useEffect } from 'react'; +import ReactGA from 'react-ga'; +import theme from '../theme'; +import PaperComponent from "../components/PaperComponent.jsx" +import UsecaseSearch, { usecaseTypes } from "../components/UsecaseSearch.jsx" + +import { + Paper, + Typography, + Divider, + IconButton, + Badge, + CircularProgress, + Tooltip, + Dialog, +} from "@material-ui/core"; + +import { + Close as CloseIcon, + Delete as DeleteIcon, + AutoFixHigh as AutoFixHighIcon, + Done as DoneIcon, +} from "@mui/icons-material"; + +const SuggestedWorkflows = (props) => { + const { globalUrl, userdata, usecaseSuggestions, frameworkData, setUsecaseSuggestions, inputSearch, apps, } = props + + const [usecaseSearch, setUsecaseSearch] = React.useState("") + const [usecaseSearchType, setUsecaseSearchType] = React.useState("") + const [finishedUsecases, setFinishedUsecases] = React.useState([]) + const [previousUsecase, setPreviousUsecase] = React.useState("") + const [closeWindow, setCloseWindow] = React.useState(false) + + const isCloud = + window.location.host === "localhost:3002" || + window.location.host === "shuffler.io"; + + + useEffect(() => { + if (closeWindow === true) { + console.log("WINDOW CLOSED") + finishedUsecases.push(usecaseSearch) + setFinishedUsecases(finishedUsecases) + + setCloseWindow(false) + } + }, [closeWindow]) + + if (usecaseSuggestions === undefined || usecaseSuggestions.length === 0) { + console.log("Closing finished usecases 1") + return null + } + + if (inputSearch !== previousUsecase) { + setPreviousUsecase(inputSearch) + setFinishedUsecases([]) + } + + if (finishedUsecases.length === usecaseSuggestions.length) { + console.log("Closing finished usecases 2") + return null + } + + + //useEffect(() => { + // //if (defaultSearch === + // //setFinishedUsecases(finishedUsecases) + // console.log("Finished default usecase?", usecaseSearch) + //}, [usecaseSearch]) + + const foundZindex = usecaseSearch.length > 0 && usecaseSearchType.length > 0 ? -1 : 12500 + + const IndividualUsecase = (props) => { + const { usecase, index } = props + const [hovering, setHovering] = React.useState(false) + + const usecasename = usecase.name + const bordercolor = usecase.color !== undefined ? usecase.color : "rgba(255,255,255,0.3)" + + + const srcimage = usecase.items[0].app + var dstimage = usecase.items[1].app + if (usecase.items.length > 2) { + dstimage = usecase.items[2].app + } + + console.log(finishedUsecases) + + const finished = finishedUsecases.includes(usecasename) + const selectedIcon = finished ? : + + if (finished) { + return null + } + + // Simple visual of the usecase + return ( + +
{ + setHovering(true) + }} onMouseOut={() => { + setHovering(false) + }} onClick={() => { + if (isCloud) { + ReactGA.event({ + category: "welcome", + action: "click_suggested_workflow", + label: usecasename, + }) + } + + console.log("Try usecase ", usecasename) + setUsecaseSearchType(usecase.type) + setUsecaseSearch(usecasename) + + }}> +
+ + {usecasename} + +
+ {srcimage.name} + {dstimage.name} +
+ +
+
+ {selectedIcon} +
+
+
+ ) + } + + // + return ( + + 0 && usecaseSearchType.length > 0} + onClose={() => { + finishedUsecases.push(usecaseSearch) + setFinishedUsecases(finishedUsecases) + + + + setUsecaseSearch("") + setUsecaseSearchType("") + }} + PaperProps={{ + style: { + pointerEvents: "auto", + backgroundColor: theme.palette.surfaceColor, + color: "white", + minWidth: 450, + padding: 50, + overflow: "hidden", + zIndex: 10012, + border: theme.palette.defaultBorder, + }, + }} + > + { + finishedUsecases.push(usecaseSearch) + setFinishedUsecases(finishedUsecases) + + setUsecaseSearch("") + setUsecaseSearchType("") + }} + > + + + + + + +
+ + Suggested Workflows ({finishedUsecases.length}/{usecaseSuggestions.length}) + + { + if (setUsecaseSuggestions !== undefined) { + setUsecaseSuggestions([]) + } + }} + > + + + {usecaseSuggestions.map((usecase, index) => { + + return ( + + ) + + })} +
+
+ ) +} + +export default SuggestedWorkflows; diff --git a/frontend/src/components/UsecaseSearch.jsx b/frontend/src/components/UsecaseSearch.jsx new file mode 100644 index 00000000..2466ca4a --- /dev/null +++ b/frontend/src/components/UsecaseSearch.jsx @@ -0,0 +1,1415 @@ +import React, { useState, useEffect } from "react"; +import theme from '../theme'; +import { useAlert } from "react-alert"; +import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx"; +import PaperComponent from "../components/PaperComponent.jsx" +import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; +import AuthenticationNormal from "../components/AuthenticationNormal.jsx"; +import AppsearchPopout from "../components/AppsearchPopout.jsx"; + +import { + ExpandLess as ExpandLessIcon, + ExpandMore as ExpandMoreIcon, + AddCircleOutline as AddCircleOutlineIcon, + Delete as DeleteIcon, + Description as DescriptionIcon, + Close as CloseIcon, +} from '@mui/icons-material'; + +import { + Dialog, + IconButton, + Typography, + Button, + CircularProgress, + Tooltip, + Divider, +} from "@material-ui/core"; + +const defaultValue = {"id": "", "name": "", + "source": {"text": "No trigger selected", "error": ""}, + "destination": {"text": "No subflow selected", "error": ""}, + "middle": [] +} + +export const usecaseTypes = [{ + "name": "enrichment", + "value": [{ + "name": "EDR Ticket Enrichment", + "active": true, + "items": [{ + "name": "When an EDR alert is found", + "app_type": "edr", + "type": "trigger", + }, { + "name": "Create a ticket", + "app_type": "cases", + "type": "subflow", + }], + }, + { + "name": "SIEM alert Enrichment", + "active": true, + "items": [{ + "name": "When a SIEM alert is found", + "app_type": "siem", + "type": "trigger", + }, + { + "name": "Create a ticket", + "app_type": "cases", + "type": "subflow", + }], + }, + { + "name": "Email Enrichment", + "active": true, + "items": [{ + "name": "When I get an email", + "app_type": "email", + "type": "trigger", + }, + { + "name": "Create a ticket", + "app_type": "cases", + "type": "subflow", + }], + }] +}, +{ + "name": "phishing", + "value": [ + { + "name": "Email analysis", + "active": true, + "items": [{ + "name": "When I get an email", + "app_type": "email", + "type": "trigger", + },{ + "name": "Create a ticket", + "app_type": "cases", + "type": "subflow", + }], + }] + }, + { + "name": "detection", + "value": [ + { + "name": "Sigma rule detection", + "active": false, + "items": [{ + "name": "When a sigma rule triggers", + "app_type": "siem", + "type": "trigger", + }, + { + "name": "Create a ticket", + "app_type": "cases", + "type": "subflow", + }], + }] + }, + { + "name": "response", + "value": [ + { + "name": "EDR host isolation", + "active": false, + "items": [{ + "name": "When malicious endpoint activity is detected", + "app_type": "edr", + "type": "trigger", + }, + { + "name": "Isolate the host", + "app_type": "edr", + "type": "subflow", + }], + }] + } +] + +export const triggerlist = [ + { + "app_id": "", + "app_name": "", + "app_version": "", + "text": "No trigger selected", + "image": "", + "type": "", + "action_type": "", + "error": "", + }, + { + "app_id": "", + "app_name": "", + "app_version": "", + "text": "When I get an email", + "image": "", + "type": "email", + "action_type": "receive", + }, + { + "app_id": "", + "app_name": "", + "app_version": "", + "text": "When a ticket is created", + "image": "", + "type": "cases", + "action_type": "case_opened", + }, + { + "app_id": "", + "app_name": "", + "app_version": "", + "text": "When an EDR alert is found", + "image": "", + "type": "edr", + "action_type": "case_opened", + }, + { + "app_id": "", + "app_name": "", + "app_version": "", + "text": "When a SIEM alert is found", + "image": "", + "type": "siem", + "action_type": "case_opened", + }, + { + "app_id": "", + "app_name": "", + "app_version": "", + "text": "When a sigma rule triggers", + "image": "", + "type": "siem", + "action_type": "case_opened", + "disabled": true, + }, + { + "app_id": "", + "app_name": "", + "app_version": "", + "text": "When malicious endpoint activity is detected", + "image": "", + "type": "edr", + "action_type": "case_opened", + "disabled": true, + } +] + +export const midflows = [ + { + "app_id": "", + "app_name": "", + "app_version": "", + "text": "Enrich", + "image": "", + "type": "intel", + "action_type": "enrich", + }, + { + + "app_id": "", + "app_name": "", + "app_version": "", + "text": "Analyze", + "image": "", + "type": "intel", + "action_type": "analyze", + "disabled": true, + } +] + +export const subflows = [ + { + "app_id": "", + "app_name": "", + "app_version": "", + "text": "No subflow selected", + "image": "", + "type": "", + "action_type": "", + "error": "", + }, + { + "app_id": "", + "app_name": "", + "app_version": "", + "text": "Create a ticket", + "image": "", + "type": "cases", + "action_type": "case_create", + }, + { + "app_id": "", + "app_name": "", + "app_version": "", + "text": "Update a ticket", + "image": "", + "type": "cases", + "action_type": "case_update", + "disabled": true, + //"source_type": "case_opened", + }, + { + "app_id": "", + "app_name": "", + "app_version": "", + "text": "Answer the sender", + "image": "", + "type": "email", + "action_type": "send", + "disabled": true, + //"source_type": "case_opened", + }, + { + "app_id": "", + "app_name": "", + "app_version": "", + "text": "Isolate the host", + "image": "", + "type": "edr", + "action_type": "respond", + "disabled": true, + //"source_type": "case_opened", + }, +] + +const UsecaseSearch = (props) => { + const { defaultSearch, appFramework, globalUrl, showTitle, canExpand, apps, setFoundWorkflowId, getFramework, userdata, usecaseSearch, setUsecaseSearch, autotry, setCloseWindow, } = props + + + const [allusecases, setAllUsecases] = React.useState([ + JSON.parse(JSON.stringify(defaultValue)) + ]) + const [searchType, setSearchType] = React.useState("") + const [usecaseIndex, setUsecaseIndex] = React.useState(0) + const [_, setUpdate] = useState(""); // Used for rendring, don't remove + const [isUploading, setIsUploading] = React.useState(false) + const [configureWorkflowModalOpen, setConfigureWorkflowModalOpen] = React.useState(false); + const [configureWorkflowAuth, setConfigureWorkflowAuth] = React.useState([]); + const [workflow, setWorkflow] = React.useState({}); + const [appAuthentication, setAppAuthentication] = React.useState([]); + const [authenticationType, setAuthenticationType] = React.useState(""); + const [authenticationModalOpen, setAuthenticationModalOpen] = React.useState(false); + const [selectedApp, setSelectedApp] = React.useState({}); + const [selectedAction, setSelectedAction] = React.useState({}); + const [firstRequest, setFirstRequest] = React.useState(true); + + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const alert = useAlert() + + useEffect(() => { + // if (firstRequest !== true && workflow.id !== undefined && autotry === true && setUsecaseSearch !== undefined && authenticationModalOpen === false && configureWorkflowModalOpen === false) { + // + if (autotry === true && configureWorkflowModalOpen === false && workflow.id !== undefined && setUsecaseSearch !== undefined) { + console.log("Close it?") + alert.info("Workflow successfully added!") + + if (setCloseWindow !== undefined) { + setCloseWindow(true) + } + setUsecaseSearch("") + } + }, [configureWorkflowModalOpen]) + + const rerunWorkflowCheck = (defaultSearch, usecaseSearch) => { + const foundusecase = usecaseTypes.find(data => data.name.toLowerCase() === defaultSearch.toLowerCase()) + + //console.log("FOUND: ", usecaseTypes, defaultSearch.toLowerCase(), foundusecase) + + if (foundusecase !== undefined && foundusecase !== null) { + // Just choose the first one. + if (foundusecase.value !== undefined && foundusecase.value !== null && foundusecase.value.length > 0) { + var selectedusecase = foundusecase.value[0] + if (usecaseSearch !== undefined && usecaseSearch !== null) { + const foundSubcase = foundusecase.value.find(usecase => usecase.name.toLowerCase() === usecaseSearch.toLowerCase()) + if (foundSubcase !== undefined && foundSubcase !== null) { + selectedusecase = foundSubcase + } + } + + var newitem = JSON.parse(JSON.stringify(defaultValue)) + + for (var key in selectedusecase.items) { + // Check in different types + const itemname = selectedusecase.items[key].name.toLowerCase() + const trigger = triggerlist.find(data => data.text.toLowerCase() === itemname) + if (trigger !== undefined && trigger !== null) { + newitem.name = selectedusecase.name + newitem.source = trigger + + continue + } + + const midflow = midflows.find(data => data.text.toLowerCase() === itemname) + if (midflow !== undefined && midflow !== null) { + newitem.name = selectedusecase.name + newitem.middle.push(midflow) + + continue + } + + const subflow = subflows.find(data => data.text.toLowerCase() === itemname) + if (subflow !== undefined && subflow !== null) { + newitem.name = selectedusecase.name + newitem.destination = subflow + + continue + } + } + + setAllUsecases([newitem]) + setUpdate(Math.random()) + } + } else { + console.log("NOT FOUND FOR: ", defaultSearch) + setAllUsecases([JSON.parse(JSON.stringify(defaultValue))]) + setUpdate(Math.random()) + } + } + + if (defaultSearch !== undefined && defaultSearch !== null && defaultSearch !== searchType) { + console.log("Setting searchtype to", defaultSearch) + + + setSearchType(defaultSearch) + setUsecaseIndex(0) + rerunWorkflowCheck(defaultSearch, usecaseSearch) + } + + //if (defaultSearch !== undefined && defaultSearch.length > 0 && allusecases[usecaseIndex].name === "") { + // rerunWorkflowCheck() + //} + + /* + defaultSearch === "Enrichment" ? + [{ + "id": "", + "source": { + "app_id": "", + "app_name": "", + "app_version": "", + "text": "When I get an email", + "image": "", + "type": "email", + "action_type": "receive", + }, + "middle": [{ + "app_id": "", + "app_name": "", + "app_version": "", + "text": "Enrich the data", + "image": "", + "type": "intel", + "action_type": "case_create", + }], + "destination": { + "app_id": "", + "app_name": "", + "app_version": "", + "text": "Create a ticket", + "image": "", + "type": "cases", + "action_type": "case_create", + }, + }] + : [] + ) + */ + + /* + if (defaultSearch === undefined || defaultSearch === null || defaultSearch.length === 0) { + return ( +
+ + Choose a Usecase + +
+ ) + } + */ + + // Image = the source EMAIL system used + var usecases = JSON.parse(JSON.stringify(allusecases)) + + const imagestyle = { + height: 50, + width: 50, + borderRadius: 25, + border: "1px solid rgba(255,255,255,0.3)", + cursor: "pointer", + } + + const getType = (inputtype) => { + if (inputtype === undefined) { + return inputtype + + } + + if (inputtype.toLowerCase() === "email" || inputtype.toLowerCase() === "comms") { + inputtype = "communication" + } + + return inputtype + } + + // FIXME: Add a way for it to automatically discover + // relevant workflows from the app at this point + // or maybe it should be added directly to the app frameworks' + // data + + const getAppAuthentication = (updateAction) => { + fetch(globalUrl + "/api/v1/apps/authentication", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for app auth :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success) { + setAppAuthentication(responseJson.data); + } + }) + .catch((error) => { + console.log("App auth loading error: "+error.toString()); + }) + } + + //saveWorkflow={undefined} {/*saveWorkflow*/} + //setNewAppAuth={undefined} {/*setNewAppAuth*/} + const authenticationModal = authenticationModalOpen ? ( + { + //if (configureWorkflowModalOpen) { + // setSelectedAction({}); + //} + }} + PaperProps={{ + style: { + pointerEvents: "auto", + backgroundColor: theme.palette.surfaceColor, + color: "white", + minWidth: 1100, + minHeight: 700, + maxHeight: 700, + padding: 15, + overflow: "hidden", + zIndex: 10012, + border: theme.palette.defaultBorder, + }, + }} + > +
+ {selectedApp.reference_info === undefined || + selectedApp.reference_info === null || + selectedApp.reference_info.github_url === undefined || + selectedApp.reference_info.github_url === null || + selectedApp.reference_info.github_url.length === 0 ? ( + + {`Documentation + + ) : ( + + {`Documentation + + )} +
+ { + setAuthenticationModalOpen(false); + if (configureWorkflowModalOpen) { + setSelectedAction({}); + } + }} + > + + +
+
+ {authenticationType.type === "oauth2" ? ( + + ) : ( + + )} +
+
+ {selectedApp.documentation === undefined || + selectedApp.documentation === null || + selectedApp.documentation.length === 0 ? ( + + + {selectedApp.description} + + + + There is currently no extended documentation available for this + app. + + + Want help help making or using this app?{" "} + + Join the community on Discord! + + + + + Want to help change this app directly? + + {selectedApp.reference_info === undefined || + selectedApp.reference_info === null || + selectedApp.reference_info.github_url === undefined || + selectedApp.reference_info.github_url === null || + selectedApp.reference_info.github_url.length === 0 ? ( + + + + Check it out on Github! + + + + ) : ( + + + + Check it out on Github! + + + + )} + + ) : + null + } +
+
+
+ ) : null + + const deleteWorkflow = (workflow_id) => { + fetch(globalUrl + "/api/v1/workflows/" + workflow_id, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + console.log("Deleted workflow") + }) + .catch((error) => { + //alert.error(error.toString()); + console.log("Delete workflow error: ", error.toString()); + }) + } + + const getWorkflow = (workflow_id) => { + fetch(globalUrl + "/api/v1/workflows/" + workflow_id, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + setWorkflow(responseJson) + + if (window !== undefined && !window.location.href.includes("/workflows")) { + setConfigureWorkflowModalOpen(true) + } + }) + .catch((error) => { + //alert.error(error.toString()); + console.log("Get workflows error: ", error.toString()); + }) + } + + const mergeWorkflowUsecases = (usecasedata) => { + const url = `${globalUrl}/api/v1/workflows/merge`; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(usecasedata), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + setIsUploading(false) + var changed = false + if (responseJson.success === false) { + if (responseJson.reason !== null && responseJson.reason !== undefined) { + //alert.error(responseJson.reason) + } + + if (responseJson.source === "") { + const appname = + usecasedata.source.error = usecasedata.source.app_name === undefined ? "Select a Trigger workflow first" : `${usecasedata.source.app_name} has no public trigger workflow yet. Click this to try another app` + changed = true + } else { + usecasedata.source.error = "" + changed = true + } + + if (responseJson.destination === "") { + usecasedata.destination.error = usecasedata.destination.app_name === undefined ? "Select a Subflow first" : `${usecasedata.destination.app_name} has no public subflow yet. Click this to try another app` + changed = true + } else { + usecasedata.destination.error = "" + changed = true + } + + if (responseJson.middle !== undefined) { + for (var key in responseJson.middle) { + for (var subkey in usecasedata.middle) { + if (responseJson.middle[key] === usecasedata.middle[key].text) { + console.log("Found: ") + if (usecasedata.middle[subkey].app_name === undefined || usecasedata.middle[subkey].app_name === "") { + usecasedata.middle[subkey].error = `${usecasedata.middle[subkey].type} app must be selected first. Click this to change` + } else { + usecasedata.middle[subkey].error = `${usecasedata.middle[subkey].app_name} has no public subflow yet. Click this to change` + } + + changed = true + break + } + } + } + } + } else { + if (responseJson.workflow_id !== null && responseJson.workflow_id !== undefined) { + if (responseJson.added_auth !== undefined && responseJson.added_auth !== null && responseJson.added_auth.length > 0) { + console.log("SHOULD HANDLE AUTH: ", responseJson.added_auth) + setConfigureWorkflowAuth(responseJson.added_auth) + + if (setFoundWorkflowId !== undefined) { + console.log("Set found workflow id: ", responseJson.workflow_id) + setFoundWorkflowId(responseJson.workflow_id) + } + + getWorkflow(responseJson.workflow_id) + getAppAuthentication() + } else { + if (setFoundWorkflowId !== undefined) { + console.log("Set found workflow id: ", responseJson.workflow_id) + setFoundWorkflowId(responseJson.workflow_id) + } + + setWorkflow({"id": responseJson.workflow_id}) + } + } + + if (responseJson.auth_required === true) { + console.log("SET AUTH AS NEXT STEP!") + } + } + + if (changed === true) { + setAllUsecases([usecasedata]) + } + }) + ) + .catch((error) => { + setIsUploading(false) + console.log("Merge err: ", error.toString()) + //alert.error("Err: " + error.toString()); + }); + } + + //console.log("Filled in: ", appFramework, usecases) + if (appFramework !== undefined && usecases !== undefined) { + + for (var key in usecases) { + // source + const usecase = usecases[key] + if (usecase.source.type === undefined || usecase.source.image === "" || usecase.source.image === undefined) { + usecases[key].source.image = theme.palette.defaultImage + } + + if (usecase.destination.type === undefined || usecase.destination.image === "" || usecase.destination.image === undefined) { + usecases[key].destination.image = theme.palette.defaultImage + } + + const srctype = getType(usecase.source.type) + const dsttype = getType(usecase.destination.type) + + const srcinfo = appFramework[srctype] + if (srcinfo !== undefined && srcinfo.large_image !== undefined && srcinfo.large_image !== "" && (usecases[key].source.app_name === undefined || usecases[key].source.app_name === "")) { + usecases[key].source.image = srcinfo.large_image + usecases[key].source.app_id = srcinfo.id + usecases[key].source.app_name = srcinfo.name + } + + const destinfo = appFramework[dsttype] + if (destinfo !== undefined && destinfo.large_image !== undefined && destinfo.large_image !== "" && (usecases[key].destination.app_name === undefined || usecases[key].destination.app_name === "")) { + usecases[key].destination.image = destinfo.large_image + usecases[key].destination.app_id = destinfo.id + usecases[key].destination.app_name = destinfo.name + } + + if (usecase.middle !== undefined && usecase.middle !== null && usecase.middle.length > 0) { + for (var subkey in usecase.middle) { + const midcase = usecase.middle[subkey] + + const midtype = getType(midcase.type) + const midinfo = appFramework[midtype] + if (midinfo !== undefined && midinfo.large_image !== undefined && midinfo.large_image !== "" && (usecases[key].middle[subkey].app_name === undefined || usecases[key].middle[subkey].app_name === "")) { + usecases[key].middle[subkey].image = midinfo.large_image + usecases[key].middle[subkey].app_id = midinfo.id + usecases[key].middle[subkey].app_name = midinfo.name + } else { + if (usecases[key].middle[subkey].image === undefined || usecases[key].middle[subkey].image === "") { + usecases[key].middle[subkey].image = theme.palette.defaultImage + } + } + } + } + } + + //if (firstRequest) { + // setAllUsecases(usecases) + // setFirstRequest(false) + //} + } + + const configureWorkflowModal = + configureWorkflowModalOpen ? ( + + { + setConfigureWorkflowModalOpen(false); + }} + > + + + + {/* + referenceUrl={referenceUrl} + submitSchedule={submitSchedule} + appAuthentication={appAuthentication} + selectedAction={selectedAction} + saveWorkflow={saveWorkflow} + newWebhook={newWebhook} + */} + + ) : null + + + const createWorkflowFromTemplate = (data) => { + if (workflow.id !== undefined && workflow.id !== null && workflow.id.length !== 0) { + console.log("Should delete old one: ", workflow.id) + deleteWorkflow(workflow.id) + } + + // Should be searched? + setIsUploading(true) + + var changed = false + if (data.source.app_name === "") { + data.source.error = `'${data.source.type}' app must be selected first` + changed = true + } + + if (data.destination.app_name === "") { + data.destination.error = `'${data.destination.type}' app must be selected first` + changed = true + } + + if (data.middle !== undefined && data.middle !== null && data.middle.length > 0) { + for (var key in data.middle) { + const middleItem = data.middle[key] + if (middleItem.app_name === "") { + data.middle[key].error = `'${middleItem.type}' app must be selected first` + //changed = true + } + } + } + + if (changed) { + alert.error("Errors were found. Click them to sort sort them out or go to the next usecase.") + + setUpdate(Math.random()) + setIsUploading(false) + return + } + + // Auto finding these during deploy + //data.source.workflow_id = "e506060f-0c58-4f95-a0b8-f671103d78e5" + //data.destination.workflow_id = "ffe8122d-0787-425a-aa20-c2587ee75a83" + //if (data.middle !== undefined && data.middle !== null && data.middle.length > 0) { + // data.middle[0].workflow_id = "1077d9ee-b571-4410-a7e6-261f32f346c5" + //} + + mergeWorkflowUsecases(data) + } + + if (autotry === true && firstRequest === true) { + console.log("Should autotry the usecase! Make sure data is in order first. Usecase: ", usecases) + + if (usecases !== undefined && usecases !== null && usecases.length > 0 && usecases[0].name !== "") { + setFirstRequest(false) + createWorkflowFromTemplate(usecases[0]) + } + //{usecases.map((data, index) => { + } + + + const ShowBox = (props) => { + const { data, type, index, miditem, subindex } = props + + const [expanded, setexpanded] = React.useState(false) + const [newSelectedApp, setNewSelectedApp] = React.useState({}) + const [paperTitle, setPaperTitle] = React.useState(data.type) + const [selectionOpen, setSelectionOpen] = React.useState(false) + const [discoveryData, setDiscoveryData] = React.useState({ + "id": "", + "label": "", + "name": "", + "large_image": "", + }) + + //console.log("Data: ", data) + + const setFrameworkItem = (data) => { + fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for framework!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + if (responseJson.reason !== undefined) { + alert.error("Failed updating default app: " + responseJson.reason) + } else { + alert.error("Failed to update framework for your org.") + + } + } else { + if (getFramework !== undefined) { + getFramework() + } + } + + //setFrameworkLoaded(true) + //setFrameworkData(responseJson) + }) + .catch((error) => { + alert.error(error.toString()); + //setFrameworkLoaded(true) + }) + } + + useEffect(() => { + if (newSelectedApp.objectID === undefined) { + return + } + + var discoveredtype = "" + if (type === "middle") { + console.log("Updating middle index", subindex) + + allusecases[index][type][subindex]["app_id"] = newSelectedApp.objectID + allusecases[index][type][subindex]["app_name"] = newSelectedApp.name + allusecases[index][type][subindex]["app_version"] = newSelectedApp.app_version + allusecases[index][type][subindex]["image"] = newSelectedApp.image_url + allusecases[index][type][subindex]["error"] = "" + + discoveredtype = allusecases[index][type][subindex]["type"] + } else { + allusecases[index][type]["app_id"] = newSelectedApp.objectID + allusecases[index][type]["app_name"] = newSelectedApp.name + allusecases[index][type]["app_version"] = newSelectedApp.app_version + allusecases[index][type]["image"] = newSelectedApp.image_url + allusecases[index][type]["error"] = "" + + discoveredtype = allusecases[index][type]["type"] + } + + setSelectionOpen(false) + setUpdate(Math.random()) + setAllUsecases(allusecases) + + const submitValue = { + "type": discoveredtype, + "name": newSelectedApp.name, + "id": newSelectedApp.objectID, + "large_image": newSelectedApp.image_url, + "description": newSelectedApp.description, + } + + setFrameworkItem(submitValue) + }, [newSelectedApp]) + + if (data.text === undefined || data.text === null || data.text.length === 0) { + return null + } + + const changeAppType = () => { + setSelectionOpen(true) + } + + const looplist = type === "source" ? triggerlist : type === "destination" ? subflows : midflows + const hasError = data.error !== undefined && data.error !== null && data.error.length > 0 + const borderColor = hasError ? theme.palette.primary.main : "rgba(255,255,255,0.3)" + + return ( +
+ + {selectionOpen === true ? + + : null} + +
+
+ + {data.app_name} { + + if (data.type === undefined || data.type === null || data.type === "") { + setexpanded(true) + console.log("No type. Skipping window open.") + return + } + + changeAppType() + }}/> + +
+ + {data.text} + + {hasError ? + { + if (data.type === undefined || data.type === null || data.type === "") { + setexpanded(true) + console.log("No type. Skipping window open.") + return + } + + changeAppType() + }}> + {data.error} + + : + null} +
+
+
+ + + { + }} + > + + + + + + + + + {type === "middle" ? + { + allusecases[index][type] = [] + + setAllUsecases(allusecases) + setUpdate(Math.random()) + }} + > + + + : null} + + + { + setexpanded(!expanded) + }} + > + {expanded ? : } + + + +
+
+ + {expanded ? + looplist.map((subdata, curindex) => { + if (appFramework === undefined) { + subdata.image = theme.palette.defaultImage + } else { + const srctype = getType(subdata.type) + const srcinfo = appFramework[srctype] + + if (srcinfo !== undefined && srcinfo.large_image !== undefined && srcinfo.large_image !== "" && (subdata.app_name === undefined || subdata.app_name === "")) { + subdata.image = srcinfo.large_image + subdata.app_id = srcinfo.id + subdata.app_name = srcinfo.name + } else { + subdata.image = theme.palette.defaultImage + } + } + + return ( +
{ + if (subdata.disabled === true) { + //alert.info("Usecase not available yet.") + return + } + + if (type === "middle") { + allusecases[index][type][subindex] = subdata + } else { + allusecases[index][type] = subdata + } + + setAllUsecases(allusecases) + setUpdate(Math.random()) + setexpanded(false) + }}> + {subdata.app_name} { + }}/> + + {subdata.text} + +
+ ) + }) + : + null + } +
+ ) + } + + //console.log("ALLUSECASES: ", allusecases) + + return ( +
+ {configureWorkflowModal} + {authenticationModal} + {showTitle !== false && defaultSearch !== undefined ? + + {defaultSearch}: {allusecases[usecaseIndex].name} + + : null} + {usecases.map((data, index) => { + return ( +
+ + + {data.middle !== undefined && data.middle !== null && data.middle.length > 0 ? + data.middle.map((innerdata, innerindex) => { + return ( +
+
+ +
+ ) + }) + : null} + +
+ {defaultSearch !== undefined && data.middle.length === 0 ? + + { + console.log("Click add middle!") + + allusecases[index].middle.push(midflows[0]) + setAllUsecases(allusecases) + setUpdate(Math.random()) + }} + > + + +
+ + : null} + + + {showTitle !== false && workflow.id === undefined ? + + : null} + {workflow.id === undefined || defaultSearch === undefined ? + null + : + + + + } +
+ ) + })} +
+ ) +} + +export default UsecaseSearch diff --git a/frontend/src/components/WelcomeForm.jsx b/frontend/src/components/WelcomeForm2.jsx similarity index 57% rename from frontend/src/components/WelcomeForm.jsx rename to frontend/src/components/WelcomeForm2.jsx index 4b91a5f2..90f09a6a 100644 --- a/frontend/src/components/WelcomeForm.jsx +++ b/frontend/src/components/WelcomeForm2.jsx @@ -1,17 +1,23 @@ import React, { useState, useEffect } from "react"; -import Stepper from "@material-ui/core/Stepper"; -import Step from "@material-ui/core/Step"; -import StepLabel from "@material-ui/core/StepLabel"; +import ReactGA from 'react-ga'; import Button from "@material-ui/core/Button"; import Checkbox from '@mui/material/Checkbox'; +import AliceCarousel from 'react-alice-carousel'; +import 'react-alice-carousel/lib/alice-carousel.css'; + import SearchIcon from '@mui/icons-material/Search'; import EmailIcon from '@mui/icons-material/Email'; import NewReleasesIcon from '@mui/icons-material/NewReleases'; import ExtensionIcon from '@mui/icons-material/Extension'; +import LightbulbIcon from '@mui/icons-material/Lightbulb'; +import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; import theme from '../theme'; import { + Fade, + IconButton, FormGroup, FormControl, InputLabel, @@ -24,23 +30,91 @@ import { Typography, TextField, Zoom, + List, + ListItem, + ListItemText, + Divider, + Tooltip, + Chip, } from "@material-ui/core"; +import { useAlert } from "react-alert"; + import { useNavigate, Link } from "react-router-dom"; import WorkflowSearch from '../components/Workflowsearch.jsx'; +import AuthenticationItem from '../components/AuthenticationItem.jsx'; import WorkflowPaper from "../components/WorkflowPaper.jsx" +import UsecaseSearch from "../components/UsecaseSearch.jsx" + + +const responsive = { + 0: { items: 1 }, +}; const WelcomeForm = (props) => { - const { userdata, globalUrl, discoveryWrapper, setDiscoveryWrapper } = props + const { userdata, globalUrl, discoveryWrapper, setDiscoveryWrapper, appFramework, getFramework, activeStep, setActiveStep, steps, skipped, setSkipped, getApps, apps, handleSetSearch, usecaseButtons, defaultSearch, setDefaultSearch, selectionOpen, setSelectionOpen, } = props + + const usecaseItems = [ +
+ +
+ , +
+ +
+ , +
+ +
+ , +
+ +
+ ] - const [activeStep, setActiveStep] = React.useState(0); - const [skipped, setSkipped] = React.useState(new Set()); const [discoveryData, setDiscoveryData] = React.useState({}) const [name, setName] = React.useState("") const [orgName, setOrgName] = React.useState("") const [role, setRole] = React.useState("") const [orgType, setOrgType] = React.useState("") const [finishedApps, setFinishedApps] = React.useState([]) + const [authentication, setAuthentication] = React.useState([]); + const [newSelectedApp, setNewSelectedApp] = React.useState({}) + const [thumbIndex, setThumbIndex] = useState(0); + const [thumbAnimation, setThumbAnimation] = useState(false); + const [clickdiff, setclickdiff] = useState(0); + + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const alert = useAlert(); let navigate = useNavigate(); const onNodeSelect = (label) => { @@ -77,8 +151,7 @@ const WelcomeForm = (props) => { } }, [discoveryWrapper]) - useEffect(() => { - + useEffect(() => { if ( window.location.search !== undefined && window.location.search !== null @@ -87,24 +160,20 @@ const WelcomeForm = (props) => { const params = Object.fromEntries(urlSearchParams.entries()); const foundTab = params["tab"]; if (foundTab !== null && foundTab !== undefined && !isNaN(foundTab)) { - setActiveStep(foundTab-1) + if (foundTab === 3 || foundTab === "3") { + console.log("SET SEARCH!!") + //setclickdiff(240) + } } else { - navigate(`/welcome?tab=1`) + //navigate(`/welcome?tab=1`) } } - }, []) - const steps = getSteps(); - const isStepOptional = step => { return step === 1 } - const isStepSkipped = step => { - return skipped.has(step) - } - const sendUserUpdate = (name, role, userId) => { const data = { "tutorial": "welcome", @@ -177,7 +246,6 @@ const WelcomeForm = (props) => { console.log("Update of org failed") //alert.error("Failed updating org: ", responseJson.reason); } else { - console.log("Update success!") //alert.success("Successfully edited org!"); } }) @@ -221,12 +289,25 @@ const WelcomeForm = (props) => { ) } + const isStepSkipped = step => { + return skipped.has(step) + } + const handleNext = () => { setDefaultSearch("") if (activeStep === 0) { console.log("Should send basic information about org (fetch)") + setclickdiff(0) navigate(`/welcome?tab=2`) + + if (isCloud) { + ReactGA.event({ + category: "welcome", + action: "click_page_one_next", + label: "", + }) + } if (userdata.active_org !== undefined && userdata.active_org.id !== undefined && userdata.active_org.id !== null && userdata.active_org.id.length > 0) { sendOrgUpdate(orgName, orgType, userdata.active_org.id, "") @@ -240,11 +321,22 @@ const WelcomeForm = (props) => { console.log("Should send secondary info about apps and other things") setDiscoveryWrapper({}) + setclickdiff(240) + //setclickdiff(0) navigate(`/welcome?tab=3`) + //handleSetSearch("Enrichment", "2. Enrich") + handleSetSearch(usecaseButtons[0].name, usecaseButtons[0].usecase) + getApps() + + // Make sure it's up to date + if (getFramework !== undefined) { + getFramework() + } } else if (activeStep === 2) { console.log("Should send third page with workflows activated and the like") } + let newSkipped = skipped; if (isStepSkipped(activeStep)) { newSkipped = new Set(newSkipped.values()); @@ -253,11 +345,27 @@ const WelcomeForm = (props) => { setActiveStep(prevActiveStep => prevActiveStep + 1); setSkipped(newSkipped); - }; + } + const handleBack = () => { setActiveStep(prevActiveStep => prevActiveStep - 1); + + if (activeStep === 2) { + setDiscoveryWrapper({}) + + if (getFramework !== undefined) { + getFramework() + } + setclickdiff(0) + navigate("/welcome?tab=2") + } else if (activeStep === 1) { + //setclickdiff(0) + navigate("/welcome?tab=1") + } }; + const handleSkip = () => { + setclickdiff(240) if (!isStepOptional(activeStep)) { throw new Error("You can't skip a step that isn't optional."); } @@ -271,27 +379,16 @@ const WelcomeForm = (props) => { const handleReset = () => { setActiveStep(0); }; - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; - const [selectionOpen, setSelectionOpen] = React.useState(false) - const [newSelectedApp, setNewSelectedApp] = React.useState({}) - const [defaultSearch, setDefaultSearch] = React.useState("") useEffect(() => { console.log("Selected app changed (effect)") }, [newSelectedApp]) - function getSteps() { - return [ - "Help us get to know you", - "Discover integrations", - "Personalize Usecases", - ] - } - + //const buttonWidth = 145 const buttonWidth = 450 const buttonMargin = 10 - const sizing = 435 + const sizing = 475 const buttonStyle = { flex: 1, width: "100%", @@ -300,6 +397,23 @@ const WelcomeForm = (props) => { fontSize: 18, } + const slideNext = () => { + if (!thumbAnimation && thumbIndex < usecaseItems.length - 1) { + //handleSetSearch(usecaseButtons[0].name, usecaseButtons[0].usecase) + setThumbIndex(thumbIndex + 1); + } else if (!thumbAnimation && thumbIndex === usecaseItems.length - 1) { + setThumbIndex(0) + } + }; + + const slidePrev = () => { + if (!thumbAnimation && thumbIndex > 0) { + setThumbIndex(thumbIndex - 1); + } else if (!thumbAnimation && thumbIndex === 0) { + setThumbIndex(usecaseItems.length-1) + } + }; + const newButtonStyle = { padding: 22, flex: 1, @@ -307,22 +421,20 @@ const WelcomeForm = (props) => { minWidth: buttonWidth, maxWidth: buttonWidth, } - + const getStepContent = (step) => { switch (step) { case 0: return ( - + + {/*isCloud ? null : This data will be used within the product and NOT be shared unless cloud synchronization is configured. */} - - Welcome to Shuffle! - - We need some more information in order to understand how we best can help you find relevant Usecases in Shuffle. + First, we need some more information in order to understand how we best can help you find relevant Usecases. This is optional, but highly encouraged. { + ) case 1: return ( -
+ +
- Find your apps, then we'll help you find relevant workflows. Can't find what you're looking for? Contact support: support@shuffler.io + Clicks the buttons below to find your apps, then we will help you find relevant workflows. Can't find your app? { + if (window.drift !== undefined) { + window.drift.api.startInteraction({ interactionId: 340043 }) + } else { + console.log("Couldn't find drift in window.drift and not .drift-open-chat with querySelector: ", window.drift) + } + }}>Contact our App Developers! {/*The app framework helps us access and authenticate the most important APIs for you. */} @@ -420,11 +540,16 @@ const WelcomeForm = (props) => { */} {/*Find your integrations!*/} +
+ +
-
@@ -434,7 +559,7 @@ const WelcomeForm = (props) => { Intel -
@@ -478,94 +603,123 @@ const WelcomeForm = (props) => { */}
+ ) case 2: return ( -
- - - What usecases are you interested in? This will help us suggest relevant Workflows. - - - - - - - - - + +
+ + These are some of our Workflow templates, used to start new Workflows. Use the right and left buttons to find new Usecases, and click the orange button to build it. + + {/**/} {/* - - - +
+ {usecaseButtons.map((usecase, index) => { + + return ( + { + console.log("Clicked: ", usecase.name) + if (defaultSearch === usecase.name) { + //setSelectedUsecaseCategory("") + } else { + handleSetSearch(usecase.name, usecase.usecase) + } + //addFilter(usecase.name.slice(3,usecase.name.length)) + }} + variant="outlined" + color="primary" + /> + ) + })} +
*/} -
- {selectionOpen === true ? - + {/* + - : null} + */} + +
+ + { + slidePrev() + }} + > + + + +
+ +
+ + { + slideNext() + }} + > + + + +
+
) default: return "unknown step" @@ -573,26 +727,7 @@ const WelcomeForm = (props) => { } return ( -
- - {steps.map((label, index) => { - const stepProps = {} - const labelProps = {} - //if (isStepOptional(index)) { - // labelProps.optional = "optional" - //} - - if (isStepSkipped(index)) { - stepProps.completed = false; - } - - return ( - - {label} - - ) - })} - +
{/*selectionOpen ? { ) : (
{getStepContent(activeStep)} -
- - {/*isStepOptional(activeStep) && ( - - )*/} - -
-
- )} +
+ {activeStep === 2 || activeStep === 1 ? +
+ + +
+ : +
+ + {/*isStepOptional(activeStep) && ( + + )*/} + + {activeStep === 0 ? + + : null} +
+ } +
+ )}
); diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index f10240dd..33813a34 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useLayoutEffect } from "react"; import ReactDOM from "react-dom" + import { useInterval } from "react-powerhooks"; import { makeStyles, useTheme } from "@material-ui/core/styles"; @@ -1400,7 +1401,6 @@ const AngularWorkflow = (defaultprops) => { setVisited([]) setExecutionRequest({}) stop() - }) var curelements = cy.elements(); for (var i = 0; i < curelements.length; i++) { @@ -1476,6 +1476,7 @@ const AngularWorkflow = (defaultprops) => { setExecutionRequestStarted(false) console.log("Execute workflow err: ", error.toString()); }); + }) }; // This can be used to only show prioritzed ones later @@ -13858,7 +13859,7 @@ const AngularWorkflow = (defaultprops) => { Execution Variable - Execution Variables are TEMPORARY variables that you can ony be set + Execution Variables are TEMPORARY variables that you can only be set and used during execution. Learn more{" "} { const variable = execution.execution_variables.find( (data) => data.name === newVariableName - ); + ) if (variable === undefined || variable.value === undefined) { return null; } diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 954dace9..81538b8f 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -34,8 +34,17 @@ import { AttachFile as AttachFileIcon, Apps as AppsIcon, ErrorOutline as ErrorOutlineIcon, + AddAPhoto as AddAPhotoIcon, + AddAPhotoOutlined as AddAPhotoOutlinedIcon, + ZoomInOutlined as ZoomInOutlinedIcon, + ZoomOutOutlined as ZoomOutOutlinedIcon, + Loop as LoopIcon, } from "@material-ui/icons"; +import { + AddPhotoAlternate as AddPhotoAlternateIcon, +} from '@mui/icons-material'; + import { v4 as uuidv4 } from "uuid"; import { Link, useParams } from "react-router-dom"; import YAML from "yaml"; @@ -44,12 +53,6 @@ import { useAlert } from "react-alert"; import words from "shellwords"; import AvatarEditor from "react-avatar-editor"; -import AddAPhotoIcon from "@material-ui/icons/AddAPhoto"; -import AddAPhotoOutlinedIcon from "@material-ui/icons/AddAPhotoOutlined"; -import ZoomInOutlinedIcon from "@material-ui/icons/ZoomInOutlined"; -import ZoomOutOutlinedIcon from "@material-ui/icons/ZoomOutOutlined"; -import LoopIcon from "@material-ui/icons/Loop"; -import AddPhotoAlternateIcon from "@material-ui/icons/AddPhotoAlternate"; const surfaceColor = "#27292D"; const inputColor = "#383B40"; @@ -344,14 +347,14 @@ const AppCreator = (defaultprops) => { useEffect(() => { if (window.location.pathname.includes("apps/edit")) { setIsEditing(true); - handleEditApp(); + handleEditApp(props.match.params.appid); } else { checkQuery(); } }, []); - const handleEditApp = () => { - fetch(globalUrl + "/api/v1/apps/" + props.match.params.appid + "/config", { + const handleEditApp = (appid) => { + fetch(globalUrl + "/api/v1/apps/" + appid + "/config", { method: "GET", headers: { "Content-Type": "application/json", @@ -389,7 +392,10 @@ const AppCreator = (defaultprops) => { setIsAppLoaded(true); return; } + + //handleEditApp(urlParams.get("id")) + // THIS has to stay due to ID may not exist as normal app yet fetch(globalUrl + "/api/v1/get_openapi/" + urlParams.get("id"), { method: "GET", headers: { @@ -408,7 +414,7 @@ const AppCreator = (defaultprops) => { .then((responseJson) => { setIsAppLoaded(true); if (!responseJson.success) { - alert.error("Failed to verify"); + alert.error("Failed to get app config. Do you have access?"); } else { parseIncomingOpenapiData(responseJson); } diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index cf1922a4..e6ff7cd5 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -39,6 +39,10 @@ import { Delete as DeleteIcon, } from "@material-ui/icons"; +import { + ForkRight as ForkRightIcon, +} from '@mui/icons-material'; + import { useTheme } from "@material-ui/core/styles"; import YAML from "yaml"; @@ -807,9 +811,11 @@ const Apps = (props) => { ) : null; - // FIXME: Add /apps/new?id= to allow for changes of the original // Should always reference the original ID. - var editButton = + //if (selectedApp.name !== undefined && selectedApp.name !== null && selectedApp.name.includes("New")) { + //} + + var editButton = selectedApp.activated && selectedApp.private_id !== undefined && selectedApp.private_id.length > 0 && @@ -829,21 +835,19 @@ const Apps = (props) => { ) : null; //var editNewButton = editButton === null ? - var editNewButton = selectedApp.generated && selectedApp.activated && props.userdata.id !== selectedApp.owner ? - isCloud ? + var editNewButton = selectedApp.generated && selectedApp.activated && props.userdata.id !== selectedApp.owner && isCloud ? - + - : null : null const activateButton = @@ -879,8 +883,7 @@ const Apps = (props) => { ((selectedApp.private_id !== undefined && selectedApp.private_id.length > 0 && selectedApp.generated) || - (selectedApp.downloaded !== undefined && - selectedApp.downloaded == true) || + (selectedApp.downloaded !== undefined && selectedApp.downloaded == true) || !selectedApp.generated) && activateButton === null ? ( @@ -982,6 +985,10 @@ const Apps = (props) => { const userRoles = ["you", isCloud ? "public" : "everyone"]; + // Admin in org or creator of app + // FIXME: Missing check for if same creator account + const canEditApp = userdata !== undefined && (userdata.admin === "true" || userdata.id === selectedApp.owner || selectedApp.owner === "" || (userdata.admin === "true" && userdata.active_org.id === selectedApp.reference_org)) || !selectedApp.generated + //fetch(globalUrl+"/api/v1/get_openapi/"+urlParams.get("id"), var baseInfo = newAppname.length > 0 ? ( @@ -1086,14 +1093,20 @@ const Apps = (props) => { ) : null} {activateButton} - {editNewButton} - {(editNewButton === null && userdata !== undefined && (userdata.admin === "true" || userdata.id === selectedApp.owner || selectedApp.owner === "" )) || !selectedApp.generated ? ( + + { /* editNewButton === null && */ } + + {canEditApp ? (
{editButton} {downloadButton} {deleteButton}
- ) : null} + ) : +
+ {editNewButton} +
+ } {selectedApp.tags !== undefined && selectedApp.tags !== null ? (
{ })}
) : null} - {props.userdata !== undefined && - props.userdata.id === selectedApp.owner ? ( + {canEditApp + ? (
{/*

ID: {selectedApp.id}

*/} Sharing @@ -1877,7 +1890,7 @@ const Apps = (props) => { getApps(); }, 1000); } else { - alert.error("Failed deleting app"); + alert.error("Failed deleting app. Does it still exist?"); } }) .catch((error) => { diff --git a/frontend/src/views/Dashboard.jsx b/frontend/src/views/Dashboard.jsx index fad0d5e4..32a85328 100644 --- a/frontend/src/views/Dashboard.jsx +++ b/frontend/src/views/Dashboard.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; import { useInterval } from "react-powerhooks"; -import DetectionFramework from "../components/DetectionFramework.jsx"; +import AppFramework from "../components/AppFramework.jsx"; import { makeStyles, useTheme } from "@material-ui/core/styles"; // nodejs library that concatenates classes import classNames from "classnames"; @@ -834,7 +834,7 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged borderRadius: theme.palette.borderRadius, border: "1px solid rgba(255,255,255,0.3)", }}> - {
{frameworkLoaded === true && isLoaded ? - { const { globalUrl, surfaceColor, newColor, mini, inputColor, userdata, isLoggedIn, isLoaded } = props; + const [skipped, setSkipped] = React.useState(new Set()); const [inputUsecase, setInputUsecase] = useState({}); const [frameworkData, setFrameworkData] = useState(undefined); const [discoveryWrapper, setDiscoveryWrapper] = useState(undefined); + const [activeStep, setActiveStep] = React.useState(0); + const [apps, setApps] = React.useState([]); + const [defaultSearch, setDefaultSearch] = React.useState("") + const [selectionOpen, setSelectionOpen] = React.useState(false) + const [showWelcome, setShowWelcome] = React.useState(false) + + const isCloud = + window.location.host === "localhost:3002" || + window.location.host === "shuffler.io"; + + const [steps, setSteps] = useState([ + "Help us get to know you", + "Find your Apps", + "Discover Usecases", + ]) let navigate = useNavigate(); @@ -51,46 +76,284 @@ const Welcome = (props) => { }) } + const getApps = () => { + fetch(globalUrl + "/api/v1/apps", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + setApps(responseJson); + }) + .catch((error) => { + console.log("App loading error: "+error.toString()); + }); + } + + const usecaseButtons = [{ + "name": "Phishing", + "usecase": "Email management", + "color": "#C51152", + }, { + "name": "Enrichment", + "usecase": "2. Enrich", + "color": "#F4C20D", + }, { + "name": "Detection", + "usecase": "3. Detect", + "color": "#3CBA54", + }, { + "name": "Response", + "usecase": "4. Respond", + "color": "#4885ED", + }] + + const handleSetSearch = (input, orgupdate) => { + console.log("INPUT & ORGUPDATE: ", input, orgupdate, defaultSearch) + if (input !== defaultSearch) { + setDefaultSearch(input) + setSelectionOpen(false) + setTimeout(function(){ + setSelectionOpen(true) + }, 150); + + //if (userdata !== undefined && userdata.active_org !== undefined && userdata.active_org.id !== undefined) { + // sendOrgUpdate("", "", userdata.active_org.id, orgupdate) + //} + } else { + setDefaultSearch("") + setSelectionOpen(false) + } + } + useEffect(() => { getFramework() - }, []); + getApps() + + if ( + window.location.search !== undefined && + window.location.search !== null + ) { + const urlSearchParams = new URLSearchParams(window.location.search); + const params = Object.fromEntries(urlSearchParams.entries()); + const foundTab = params["tab"]; + if (foundTab !== null && foundTab !== undefined && !isNaN(foundTab)) { + console.log("FOUND TAB: ", foundTab) + setShowWelcome(true) + if (foundTab === 3 || foundTab === "3") { + console.log("SET SEARCH!!") + + handleSetSearch(usecaseButtons[0].name, usecaseButtons[0].usecase) + } + + setActiveStep(foundTab-1) + } else { + navigate(`/welcome?tab=1`) + } + } + }, []) + + const isStepSkipped = step => { + return skipped.has(step) + } + + const paperObject = { + flex: 1, + padding: 0, + textAlign: "center", + maxWidth: 300, + minWidth: 300, + backgroundColor: theme.palette.surfaceColor, + color: "white", + } + + const actionObject = { + padding: "50px 35px 50px 35px", + } + + const imageStyle = { + width: 150, + height: 150, + margin: "auto", + marginTop: 30, + } return ( - - -
- +
+ {/* +
+ +
+ */} + {showWelcome === true ? +
+
+ + {steps.map((label, index) => { + const stepProps = {} + const labelProps = {} + //if (isStepOptional(index)) { + // labelProps.optional = "optional" + //} + + if (isStepSkipped(index)) { + stepProps.completed = false; + } + + return ( + + + {label} + + + ) + })} +
- - {frameworkData === undefined || window.location.href.includes("tab=3") ? null : -
- - App Framework - - - + +
+ {/* + - + /> + */} + +
+
+ {frameworkData === undefined || window.location.href.includes("tab=1") || window.location.href.includes("tab=3") ? null : +
+ + App Framework + + + + +
+ } + +
+ : + +
+ + Welcome to Shuffle + + + Who do you identify with the most? + +
+ { + if (isCloud) { + ReactGA.event({ + category: "welcome", + action: "click_welcome_continue", + label: "", + }) + } + + setShowWelcome(true) + }}> + + + New to Shuffle + + + + Follow our short introduction and learn some tips and tricks + + + +
+ + OR + +
+ { + if (isCloud) { + ReactGA.event({ + category: "welcome", + action: "click_getting_started", + label: "", + }) + } + + navigate("/getting-started?message=Skipped intro") + }}> + + + Experienced + + + + You know Shuffle well. Head to the product right away! + + + +
- } - - ) +
+ } +
+ ) } export default Welcome; diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 7aeda1ca..224e717a 100644 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -1274,8 +1274,12 @@ const Workflows = (props) => { for (var subkey in data.actions[key].parameters) { const param = data.actions[key].parameters[subkey]; + + // Removed October 10th, 2022 as key usually isn't + // containing anything secret, but rather necessary configurations. + // param.name.includes("key") || + // if ( - param.name.includes("key") || param.name.includes("user") || param.name.includes("pass") || param.name.includes("api") || @@ -1322,8 +1326,9 @@ const Workflows = (props) => { ) { for (key in data.workflow_variables) { const param = data.workflow_variables[key]; + //param.name.includes("key") || + if ( - param.name.includes("key") || param.name.includes("user") || param.name.includes("pass") || param.name.includes("api") || diff --git a/functions/onprem/orborus/go.mod b/functions/onprem/orborus/go.mod index 7beea74e..db67d553 100644 --- a/functions/onprem/orborus/go.mod +++ b/functions/onprem/orborus/go.mod @@ -8,5 +8,5 @@ require ( github.com/docker/go-connections v0.4.0 // indirect github.com/mackerelio/go-osstat v0.2.1 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.2.82 + github.com/shuffle/shuffle-shared v0.3.5 ) diff --git a/functions/onprem/orborus/go.sum b/functions/onprem/orborus/go.sum index 2bb255e8..90da9381 100644 --- a/functions/onprem/orborus/go.sum +++ b/functions/onprem/orborus/go.sum @@ -794,6 +794,8 @@ github.com/shuffle/shuffle-shared v0.2.64 h1:WpCKiL5tNt7wTJaHkf1zXhjeUB5ltFap1dX github.com/shuffle/shuffle-shared v0.2.64/go.mod h1:YuMle0RjwXb3hxR5PdaOOD9e+hUyK34OABS0UbrT/Sk= github.com/shuffle/shuffle-shared v0.2.82 h1:V3bYw7MxHPQgydUuWLGHONpX7NwbRPPAALovKqsdaW0= github.com/shuffle/shuffle-shared v0.2.82/go.mod h1:YuMle0RjwXb3hxR5PdaOOD9e+hUyK34OABS0UbrT/Sk= +github.com/shuffle/shuffle-shared v0.3.5 h1:erfXVKjeSkmpoGczZ6hPETg8gDdZeXYgXsrEgVO/uqg= +github.com/shuffle/shuffle-shared v0.3.5/go.mod h1:YuMle0RjwXb3hxR5PdaOOD9e+hUyK34OABS0UbrT/Sk= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index 1dd959bc..4109b9ce 100644 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -704,8 +704,8 @@ func initializeImages() { log.Printf("[DEBUG] Setting baseimageregistry") } if baseimagename == "" { - baseimagename = "frikky/shuffle" - baseimagename = "frikky" + baseimagename = "shuffle/shuffle" + baseimagename = "shuffle" log.Printf("[DEBUG] Setting baseimagename") } @@ -713,7 +713,7 @@ func initializeImages() { // check whether they are the same first images := []string{ - fmt.Sprintf("frikky/shuffle:app_sdk"), + fmt.Sprintf("shuffle/shuffle:app_sdk"), fmt.Sprintf("%s/%s/shuffle-app_sdk:%s", baseimageregistry, baseimagename, appSdkVersion), fmt.Sprintf("%s/%s/shuffle-worker:%s", baseimageregistry, baseimagename, workerVersion), // fmt.Sprintf("docker.io/%s:app_sdk", baseimagename),