From 5532296f74367af386e954d3c7b49d640a0b947d Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 10 Dec 2021 23:11:58 +0100 Subject: [PATCH] Added magic parser configuration to frontend with basic app sdk parsing --- .env | 2 +- README.md | 7 +- backend/app_sdk/README.md | 10 ++ backend/app_sdk/app_base.py | 102 ++++++++++++++++- backend/app_sdk/build.sh | 2 +- backend/app_sdk/requirements.txt | 2 +- backend/go-app/docker.go | 1 + backend/go-app/go.mod | 2 +- backend/go-app/walkoff.go | 23 +++- docker-compose.yml | 2 +- frontend/package.json | 4 + frontend/src/components/ConfigureWorkflow.jsx | 5 +- frontend/src/components/ParsedAction.jsx | 43 +++++++- frontend/src/views/AngularWorkflow.jsx | 103 ++++++++++++------ frontend/src/views/AppCreator.jsx | 3 + frontend/src/views/Workflows.jsx | 41 ++++++- 16 files changed, 300 insertions(+), 52 deletions(-) diff --git a/.env b/.env index 5a78c4d8..ac324e6a 100644 --- a/.env +++ b/.env @@ -50,8 +50,8 @@ SHUFFLE_PASS_APP_PROXY=FALSE TZ=Europe/Amsterdam # Timezone-handler in Orborus, Worker and Apps ORBORUS_CONTAINER_NAME= # Used to FIND the containername. cgroup v2: issue 501 -SHUFFLE_BASE_IMAGE_REGISTRY=ghcr.io SHUFFLE_BASE_IMAGE_NAME=frikky +SHUFFLE_BASE_IMAGE_REGISTRY=ghcr.io SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-0.8.80" # Used for auto-cleanup of containers. REALLY important at scale. diff --git a/README.md b/README.md index 261358ee..01fd94b1 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,14 @@ Shuffle Automation

-[Shuffle](https://shuffler.io) is an automation platform for and by the community, focusing on accessibility for anyone to automate. Security operations is complex, but it doesn't have to be. + +![Shuffle](https://shuffler.io) is an automation platform for and by the community, focusing on accessibility for anyone to automate. Security operations is complex, but it doesn't have to be. [_Key Features_](https://shuffler.io/docs/features) — -[_Community & Support_](https://discord.gg/B2CBzUm) +[_Community & Support_](https://discord.gg/B2CBzUm) — [_Documentation_](https://shuffler.io/docs) — [_Getting Started_](https://shuffler.io/docs/getting_started) — -[_Development_](https://github.com/frikky/Shuffle/blob/master/.github/CONTRIBUTING.md) — +[_Development_](https://github.com/frikky/Shuffle/blob/master/.github/CONTRIBUTING.md) Follow us on Twitter at [@shuffleio](https://twitter.com/shuffleio). diff --git a/backend/app_sdk/README.md b/backend/app_sdk/README.md index 170781f3..478fb394 100644 --- a/backend/app_sdk/README.md +++ b/backend/app_sdk/README.md @@ -8,5 +8,15 @@ This is the SDK used for apps to behave like they should. 4. Delete the specific app's Docker image (docker rmi frikky/shuffle:...) 5. Rebuild the Docker image (click load in GUI?) +## Cloud updates +1. Go to shuffle cloud on GCP +2. Go to Cloud Storage +3. Find shuffler.appspot.com +4. Navigate to generated_apps/baseline +5. Update SDK there. This will make all new apps run with the new SDK + +## Cloud app force-updates +1. Run the "stitcher.go" program in the public shuffle-shared repository. + # LICENSE Everything in here is MIT, not AGPLv3 as indicated by the license. diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 190d983a..6889ab75 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -62,12 +62,100 @@ class AppBase: if len(self.base_url) == 0: self.base_url = self.url + # Checks output for whether it should be automatically parsed or not + def run_magic_parser(self, input_data): + if not isinstance(input_data, str): + return input_data + + # Don't touch existing JSON/lists + if (input_data.startswith("[") and input_data.endswith("]")) or (input_data.startswith("{") and input_data.endswith("}")): + return input_data + + # Don't touch large data. + if len(input_data) > 100000: + return input_data + + + new_input = input_data + try: + #new_input.strip() + new_input = input_data.split() + new_return = [] + + index = 0 + for item in new_input: + splititem = "," + if ", " in item: + splititem = ", " + elif "," in item: + splititem = "," + else: + new_return.append(item) + index += 1 + continue + + #print("FIX ITEM %s" % item) + for subitem in item.split(splititem): + new_return.insert(index, subitem) + + index += 1 + + # Prevent large data or infinite loops + if index > 10000: + return input_data + + fixed_return = [] + for item in new_return: + if not item: + continue + + if not isinstance(item, str): + fixed_return.append(item) + continue + + if item.endswith(","): + item = item[0:-1] + + fixed_return.append(item) + + new_input = fixed_return + except Exception as e: + self.logger.info(f"[DEBUG] Failed to run magic parser (2): {e}") + return input_data + + try: + new_input = input_data.split() + except Exception as e: + self.logger.info(f"[DEBUG] Failed to run magic parser (1): {e}") + return input_data + + # Won't ever touch this one? + if isinstance(input_data, list) or isinstance(input_data, object): + try: + return json.dumps(new_input) + except Exception as e: + self.logger.info(f"[DEBUG] Failed to run magic parser: {e}") + + return new_input + # FIXME: Add more info like logs in here. # Docker logs: https://forums.docker.com/t/docker-logs-inside-the-docker-container/68190/2 def send_result(self, action_result, headers, stream_path): if action_result["status"] == "EXECUTING": action_result["status"] = "FAILURE" + try: + if self.original_action["run_magic_output"] == True: + self.logger.warning("[INFO] Action result ran with Magic parser output.") + action_result["result"] = self.run_magic_parser(action_result["result"]) + else: + self.logger.warning("[ERROR] Magic output not defined.") + except Exception as e: + self.logger.warning("[ERROR] Failed to run magic autoparser: {e}") + pass + + # Try it with some magic + self.logger.info(f"""[DEBUG] Inside Send result with status {action_result["status"]}""") # FIXME: Add cleanup of parameters to not send to frontend here @@ -981,12 +1069,20 @@ class AppBase: except json.decoder.JSONDecodeError: pass - self.action_result["result"] = "Bad result from backend: %d" % ret.status_code + self.action_result["result"] = json.dumps({ + "success": False, + "reason": f"Bad result from backend during startup of app: {ret.status_code}", + "extended_reason": f"{ret.text}" + }) 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.action_result["result"] = "Connection error during startup: %s" % e + self.action_result["result"] = json.dumps({ + "success": False, + "reason": f"Connection error during startup: {e}" + }) + self.send_result(self.action_result, headers, stream_path) return else: @@ -2805,7 +2901,7 @@ class AppBase: # Dump the result as a string of a list #self.logger.info("RESULTS: %s" % results) if isinstance(results, list) or isinstance(results, dict): - self.logger.info("JSON OBJECT? ", json_object) + self.logger.info(f"JSON OBJECT? {json_object}") # This part is weird lol if json_object: diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index 02732783..ab1c9f38 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -3,7 +3,7 @@ ### DEFAULT NAME=shuffle-app_sdk -VERSION=0.9.35 +VERSION=0.9.40 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -f Dockerfile -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION -t ghcr.io/frikky/$NAME:nightly diff --git a/backend/app_sdk/requirements.txt b/backend/app_sdk/requirements.txt index de650cea..e8a534c2 100644 --- a/backend/app_sdk/requirements.txt +++ b/backend/app_sdk/requirements.txt @@ -1,7 +1,7 @@ urllib3==1.26.5 requests==2.25.1 MarkupSafe==2.0.1 -liquidpy==0.7.2 +liquidpy==0.7.3 flask[async]==2.0.2 #waitress==2.0.0 #flask==1.1.2 diff --git a/backend/go-app/docker.go b/backend/go-app/docker.go index 60c16bc6..7ff5c0b1 100644 --- a/backend/go-app/docker.go +++ b/backend/go-app/docker.go @@ -299,6 +299,7 @@ func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder strin } if !downloaded { + return errors.New(fmt.Sprintf("Failed to build / download images %s", strings.Join(tags, ","))) } //baseDockerName diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index a479da25..48c5c4cd 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -2,7 +2,7 @@ module main go 1.15 -//replace github.com/shuffle/shuffle-shared => ../../../../git/shuffle-shared +replace github.com/shuffle/shuffle-shared => ../../../../git/shuffle-shared //replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi //replace github.com/frikky/go-elasticsearch => ../../../../git/go-elasticsearch diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index b38f5d9f..420dbd78 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -2467,6 +2467,7 @@ func handleUserInput(trigger shuffle.Trigger, organizationId string, workflowId log.Printf("[INFO] Should send email to %s during execution.", email) } + if strings.Contains(triggerType, "sms") { action := shuffle.CloudSyncJob{ Type: "user_input", @@ -2491,7 +2492,11 @@ func handleUserInput(trigger shuffle.Trigger, organizationId string, workflowId return err } - log.Printf("Should send SMS to %s during execution.", sms) + log.Printf("[DEBUG] Should send SMS to %s during execution.", sms) + } + + if strings.Contains(triggerType, "subflow") { + log.Printf("[DEBUG] Should run a subflow with the result for user input.") } return nil @@ -2927,7 +2932,21 @@ func IterateAppGithubFolders(ctx context.Context, fs billy.Filesystem, dir []os. for _, item := range buildLaterFirst { err = buildImageMemory(fs, item.Tags, item.Extra, true) if err != nil { - log.Printf("Failed image build memory: %s", err) + orgId := "" + + log.Printf("[DEBUG] Failed image build memory. Creating notification with org %#v: %s", orgId, err) + + if len(item.Tags) > 0 { + err = shuffle.CreateOrgNotification( + ctx, + fmt.Sprintf("App failed to build"), + fmt.Sprintf("The app %s with image %s failed to build. Check backend logs with docker! docker logs shuffle-backend", item.Tags[0], item.Extra), + fmt.Sprintf("/apps"), + orgId, + false, + ) + } + } else { if len(item.Tags) > 0 { log.Printf("[INFO] Successfully built image %s", item.Tags[0]) diff --git a/docker-compose.yml b/docker-compose.yml index 8a8ba025..8a1fa4bd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -64,7 +64,7 @@ services: - SHUFFLE_SWARM_NETWORK_NAME=shuffle-executions restart: unless-stopped opensearch: - image: opensearchproject/opensearch:1.1.0 + image: opensearchproject/opensearch:1.2.0 hostname: shuffle-opensearch container_name: shuffle-opensearch environment: diff --git a/frontend/package.json b/frontend/package.json index 2933a4bb..acc20550 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,6 +5,8 @@ "private": true, "dependencies": { "@babel/core": "^7.15.8", + "@emotion/react": "^11.7.0", + "@emotion/styled": "^11.6.0", "@material-ui/core": "^4.5.2", "@material-ui/data-grid": "^4.0.0-alpha.22", "@material-ui/icons": "^4.5.1", @@ -12,6 +14,8 @@ "@material-ui/styles": "^4.5.2", "@material-ui/utils": "^4.11.2", "@metamask/detect-provider": "^1.2.0", + "@mui/icons-material": "^5.2.1", + "@mui/material": "^5.2.3", "@uiw/react-codemirror": "^3.2.1", "@use-it/interval": "^1.0.0", "babel-eslint": "^10.1.0", diff --git a/frontend/src/components/ConfigureWorkflow.jsx b/frontend/src/components/ConfigureWorkflow.jsx index b66ed310..f4c36f28 100644 --- a/frontend/src/components/ConfigureWorkflow.jsx +++ b/frontend/src/components/ConfigureWorkflow.jsx @@ -164,7 +164,10 @@ const ConfigureWorkflow = (props) => { newaction.must_authenticate = true; newaction.action_ids.push(action.id); } - } + } else if (action.authentication_id !== "" && app.authentication.required === true) { + console.log("Should verify authentication ID ", action.authentication_id) + + } newaction.app = app; } diff --git a/frontend/src/components/ParsedAction.jsx b/frontend/src/components/ParsedAction.jsx index b0cc7e74..fd4dd5ab 100644 --- a/frontend/src/components/ParsedAction.jsx +++ b/frontend/src/components/ParsedAction.jsx @@ -80,7 +80,10 @@ import { LockOpen as LockOpenIcon, ExpandMore as ExpandMoreIcon, VpnKey as VpnKeyIcon, -} from "@material-ui/icons"; + AutoFixHigh as AutoFixHighIcon, +} from '@mui/icons-material'; +//} from "@material-ui/icons"; + import Autocomplete from "@material-ui/lab/Autocomplete"; import CodeMirror from "@uiw/react-codemirror"; @@ -2169,7 +2172,7 @@ const ParsedAction = (props) => { marginTop: "auto", marginBottom: "auto", height: 30, - paddingLeft: 25, + marginLeft: 15, paddingRight: 0, }} onClick={() => { @@ -2189,7 +2192,7 @@ const ParsedAction = (props) => { marginTop: "auto", marginBottom: "auto", height: 30, - paddingLeft: 25, + marginLeft: 15, paddingRight: 0, }} onClick={() => {}} @@ -2209,6 +2212,40 @@ const ParsedAction = (props) => { + { + //setAuthenticationModalOpen(true); + console.log("Should enable/disable magic!") + console.log("Action: ", selectedAction) + if (selectedAction.run_magic_output === undefined) { + selectedAction.run_magic_output = true + } else { + if (selectedAction.run_magic_output === true) { + selectedAction.run_magic_output = false + } else { + selectedAction.run_magic_output = true + } + } + + setSelectedAction(selectedAction) + setUpdate(Math.random()); + }} + > + + + +
diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 76faca3e..a229cbe6 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -1754,13 +1754,17 @@ const AngularWorkflow = (props) => { var idnumber = -1; if (curElement.id.startsWith("rightside_field_")) { console.log("FOUND FIELD WITH NUMBER: ", curElement.id); + + + // Find exact position to put the text + const idsplit = curElement.id.split("_"); console.log(idsplit); if (idsplit.length === 3 && !isNaN(idsplit[2])) { console.log("ADDING TO PARAM ", idsplit[2]); console.log("PARAM: ", selectedAction); - selectedAction.parameters[idsplit[2]].value = newValue; + selectedAction.parameters[idsplit[2]].value += newValue; paramname = selectedAction.parameters[idsplit[2]].name; idnumber = idsplit[2]; } @@ -1849,7 +1853,7 @@ const AngularWorkflow = (props) => { const onNodeDrag = (event, selectedAction) => { const nodedata = event.target.data(); if (nodedata.finished === false) { - return; + console.log("NOT FINISHED - ADD EXAMPLE BRANCHES TO CLOSEST!!") } if (nodedata.app_name !== undefined) { @@ -1859,6 +1863,15 @@ const AngularWorkflow = (props) => { if (currentNode.data.attachedTo === nodedata.id) { cy.getElementById(currentNode.data.id).remove(); } + + // Calculate location + //currentNode.position.x > + //if (nodedata.position.x > 0 && nodedata.position.y > 0) { + // console.log("Positive both") + //} + + //console.log(currentNode.position) + //console.log(nodedata.position) } } else { //console.log("No appid? ", nodedata) @@ -2389,6 +2402,9 @@ const AngularWorkflow = (props) => { return ""; } + console.log("NOT REPLACING ON PURPOSE!!") + return "" + // Basically just a stupid if-else :) const synonyms = { id: [ @@ -2491,6 +2507,7 @@ const AngularWorkflow = (props) => { // Takes an action as input, then runs through and updates the relevant fields // based on previous actions' + // Uses lots of synonyms const RunAutocompleter = (dstdata) => { // **PS: The right action should already be set here** // 1. Check execution argument @@ -2617,10 +2634,13 @@ const AngularWorkflow = (props) => { (data) => data.id === edge.source ); if (targetnode === -1) { - alert.error("Can't make arrow to starting node"); - event.target.remove(); - found = true; - break; + if (targetnode.type !== "TRIGGER") { + alert.error("Can't make arrow to starting node"); + event.target.remove(); + break; + } + + found = true; } } else if (edge.source === workflow.branches[key].source_id) { // FIXME: Verify multi-target for triggers @@ -2953,16 +2973,16 @@ const AngularWorkflow = (props) => { console.log("DELETE"); break; case 38: - console.log("UP"); + //console.log("UP"); break; case 37: - console.log("LEFT"); + //console.log("LEFT"); break; case 40: - console.log("DOWN"); + //console.log("DOWN"); break; case 39: - console.log("RIGHT"); + //console.log("RIGHT"); break; case 90: if (event.ctrlKey) { @@ -3169,7 +3189,12 @@ const AngularWorkflow = (props) => { } } - setEnvironments(responseJson); + // FIXME: Don't allow multiple in cloud yet. Cloud -> Onprem isn't stable. + if (isCloud) { + setEnvironments({ name: "Cloud", type: "cloud" }); + } else { + setEnvironments(responseJson); + } }) .catch((error) => { alert.error(error.toString()); @@ -3307,7 +3332,7 @@ const AngularWorkflow = (props) => { document.title = "Workflow - " + workflow.name; registerKeys(); } - }); + }) const animationDuration = 150; const onNodeHoverOut = (event) => { @@ -3480,11 +3505,9 @@ const AngularWorkflow = (props) => { const onNodeHover = (event) => { const nodedata = event.target.data(); - /* if (nodedata.finished === false) { return; } - */ //var parentNode = cy.$("#" + event.target.data("id")); //if (parentNode.data("isButton") || parentNode.data("buttonId")) return; @@ -4794,6 +4817,7 @@ const AngularWorkflow = (props) => { parameters: parameters, isStartNode: false, large_image: app.large_image, + run_magic_output: true, authentication: [], execution_variable: undefined, example: example, @@ -6701,6 +6725,9 @@ const AngularWorkflow = (props) => {
{outlookButton} {gmailButton} + + If you have trouble using the triggers, please contact us to get access + )} @@ -10075,7 +10102,10 @@ const AngularWorkflow = (props) => { } console.log("COPY: ", copy); - var newitem = JSON.parse(base); + + //var newitem = JSON.parse(base); + var newitem = validateJson(base).result + to_be_copied = "$" + base_node_name.toLowerCase().replaceAll(" ", "_"); for (var key in copy.namespace) { if (copy.namespace[key].includes("Results for")) { @@ -10292,7 +10322,7 @@ const AngularWorkflow = (props) => { {workflowExecutions.length > 0 ? (
{workflowExecutions.map((data, index) => { - executionDelay += 75 + executionDelay += 50 const statusColor = data.status === "FINISHED" @@ -10441,7 +10471,7 @@ const AngularWorkflow = (props) => { )}
) : ( -
+
{ onClick={() => {}} > - -

{}} - > - See other Executions -

+ +

{}} + > + See other Executions +

{ marginBottom: 10, }} /> -
+

Execution info

{
{executionData.status !== undefined && executionData.status.length > 0 ? ( -
+
Status    @@ -10544,7 +10574,7 @@ const AngularWorkflow = (props) => { executionData.execution_source !== null && executionData.execution_source.length > 0 && executionData.execution_source !== "default" ? ( -
+
Source    @@ -10581,7 +10611,7 @@ const AngularWorkflow = (props) => {
) : null} {executionData.started_at !== undefined ? ( -
+
Started    @@ -10593,7 +10623,7 @@ const AngularWorkflow = (props) => { {executionData.completed_at !== undefined && executionData.completed_at !== null && executionData.completed_at > 0 ? ( -
+
{ console.log(executionData) }}> @@ -10809,18 +10839,27 @@ const AngularWorkflow = (props) => {
{ var currentnode = cy.getElementById(data.action.id); if (currentnode.length !== 0) { currentnode.addClass("shuffle-hover-highlight"); } + + // Add a hover highlight + + //var copyText = document.getElementById( + // "copy_element_shuffle" + //) }} onMouseOut={() => { var currentnode = cy.getElementById(data.action.id); diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 2d50122c..c11970d8 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -4676,12 +4676,15 @@ const AppCreator = (props) => { for (var key in invalid) { if (e.target.value.includes(invalid[key])) { alert.error("Can't use " + invalid[key] + " in name"); + setName(e.target.value.replaceAll(".", "").replaceAll("#", "").replaceAll(":", "").replaceAll(",", "")) + return; } } if (e.target.value.length > 29) { alert.error("Choose a shorter name (max 29)."); + setName(e.target.value.slice(0,28)) return; } diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index f717f9c5..de3740bb 100644 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -398,12 +398,31 @@ export const validateJson = (showResult) => { var result = showResult; try { - const result = jsonvalid ? JSON.parse(showResult) : showResult; + result = jsonvalid ? JSON.parse(showResult) : showResult; } catch (e) { //console.log("Failed parsing JSON even though its valid: ", e) jsonvalid = false; } + if (jsonvalid === false) { + + if (typeof showResult === 'string') { + showResult = showResult.trim() + } + + try { + var newstr = showResult.replaceAll("'", '"') + + console.log("Try replacements and trimming with new value: ", newstr) + result = JSON.parse(newstr) + jsonvalid = true + } catch (e) { + + console.log("Failed parsing JSON even though its valid (2): ", e) + jsonvalid = false + } + } + //console.log("VALID: ", jsonvalid, result) return { valid: jsonvalid, @@ -458,6 +477,8 @@ const Workflows = (props) => { const [submitLoading, setSubmitLoading] = React.useState(false); const [actionImageList, setActionImageList] = React.useState([]); + const [firstLoad, setFirstLoad] = React.useState(true); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; @@ -839,6 +860,11 @@ const Workflows = (props) => { setFilteredWorkflows(responseJson); setWorkflowDone(true); + + // Ensures the zooming happens only once per load + setTimeout(() => { + setFirstLoad(false) + }, 100) } else { if (isLoggedIn) { alert.error("An error occurred while loading workflows"); @@ -1764,6 +1790,7 @@ const Workflows = (props) => { if (file.type !== "application/json") { if (file.type !== undefined) { alert.error("File has to contain valid json"); + setImportLoading(false); } continue; @@ -2581,7 +2608,11 @@ const Workflows = (props) => { data.large_image = theme.palette.defaultImage; } - appDelay += 75 + if (firstLoad) { + appDelay += 75 + } else { + appDelay = 0 + } return ( @@ -2650,7 +2681,11 @@ const Workflows = (props) => { {filteredWorkflows.map((data, index) => { - workflowDelay += 75 + if (firstLoad) { + workflowDelay += 75 + } else { + workflowDelay = 0 + } return (