From 7f811b6fd59b467902b31b8ccac5e7b9d23ea0d7 Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 4 Jun 2020 11:15:51 +0200 Subject: [PATCH 01/18] Added header param check to curl parsing --- README.md | 8 ++++++-- frontend/src/AppCreator.js | 4 ++++ frontend/src/Apps.js | 41 +++++++++++++++++++++----------------- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 04fefd6e..7de816df 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,14 @@ ![Example Shuffle webhook integration](shuffle_webhook.png) -## Try it -* Self-hosted: Check out the [installation guide](https://github.com/frikky/shuffle/blob/master/install-guide.md). +## Getting started +* Self-hosted: Check out the [installation guide](https://github.com/frikky/shuffle/blob/master/install-guide.md) and [getting started](https://shuffler.io/docs/getting_started) * Cloud: Register at https://shuffler.io/register and get cooking (there are some differences!) +## Blogposts +[1. Introducing Shuffle](https://medium.com/security-operation-capybara/introducing-shuffle-an-open-source-soar-platform-part-1-58a529de7d12) +[2. Getting started with Shuffle](https://medium.com/security-operation-capybara/getting-started-with-shuffle-an-open-source-soar-platform-part-2-1d7c67a64244) + ## Related repositories * Apps: https://github.com/frikky/shuffle-apps * Workflows: https://github.com/frikky/shuffle-workflows (empty) diff --git a/frontend/src/AppCreator.js b/frontend/src/AppCreator.js index 7925246a..67803be1 100644 --- a/frontend/src/AppCreator.js +++ b/frontend/src/AppCreator.js @@ -1261,6 +1261,10 @@ const AppCreator = (props) => { if (request.header !== undefined && request.header !== null) { var headers = [] for (let [key, value] of Object.entries(request.header)) { + if (parameterName !== undefined && key.toLowerCase() === parameterName.toLowerCase()) { + continue + } + headers += key+"="+value+"\n" } diff --git a/frontend/src/Apps.js b/frontend/src/Apps.js index 742e04ec..eb3b9e58 100644 --- a/frontend/src/Apps.js +++ b/frontend/src/Apps.js @@ -20,6 +20,8 @@ import YAML from 'yaml' import {Link} from 'react-router-dom'; import Breadcrumbs from '@material-ui/core/Breadcrumbs'; +import CloudUploadIcon from '@material-ui/icons/CloudUpload'; +import PublishIcon from '@material-ui/icons/Publish'; import CloudDownload from '@material-ui/icons/CloudDownload'; import EditIcon from '@material-ui/icons/Edit'; import DeleteIcon from '@material-ui/icons/Delete'; @@ -462,7 +464,7 @@ const Apps = (props) => {
- +  OR  + + + +
{ handleSearchChange(event.target.value) }} /> -
+
{apps.length > 0 ? filteredApps.length > 0 ? -
+
{filteredApps.map(app => { return ( appPaper(app) From 716443f3a53f57772a2896c838713604c2182093 Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 5 Jun 2020 06:33:16 +0200 Subject: [PATCH 02/18] #49: Shouldn't run build locally. --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 72dccb0c..0fbc7c41 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: frontend: - build: ./frontend + #build: ./frontend image: frikky/shuffle:frontend container_name: shuffle-frontend hostname: shuffle-frontend @@ -26,7 +26,7 @@ services: volumes: - ${DB_LOCATION}:/etc/shuffle backend: - build: ./backend + #build: ./backend image: frikky/shuffle:backend container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} From 31df620d716b99d7f75c911bc4f7b3a1ad3e5bf1 Mon Sep 17 00:00:00 2001 From: frikky Date: Tue, 9 Jun 2020 18:57:14 +0200 Subject: [PATCH 03/18] Minor edits for blog posts 3 --- backend/go-app/codegen.go | 13 ++----- backend/go-app/main.go | 6 ++-- backend/go-app/walkoff.go | 28 +++++++-------- frontend/src/AngularWorkflow.js | 51 ++++++++++++++------------- frontend/src/App.js | 14 ++++---- frontend/src/AppCreator.js | 61 ++++++++++++++++++--------------- frontend/src/Apps.js | 9 ++--- 7 files changed, 93 insertions(+), 89 deletions(-) diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index e995afea..aa5200b1 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -799,7 +799,6 @@ func handleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [ } if param.Value.In == "path" { - //log.Printf("PATH!: %s", param.Value.Name) parameters = append(parameters, param.Value.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { @@ -909,7 +908,6 @@ func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor } if param.Value.In == "path" { - log.Printf("PATH!: %s", param.Value.Name) parameters = append(parameters, param.Value.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { @@ -1016,7 +1014,6 @@ func handleHead(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo } if param.Value.In == "path" { - //log.Printf("PATH!: %s", param.Value.Name) parameters = append(parameters, param.Value.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { @@ -1122,7 +1119,6 @@ func handleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [] } if param.Value.In == "path" { - //log.Printf("PATH!: %s", param.Value.Name) parameters = append(parameters, param.Value.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { @@ -1178,9 +1174,9 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo Parameters: extraParameters, } - if path.Post.RequestBody != nil { - log.Printf("RequestBody: %#v", path.Post.RequestBody) - } + //if path.Post.RequestBody != nil { + // log.Printf("RequestBody: %#v", path.Post.RequestBody) + //} action.Returns.Schema.Type = "string" baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) @@ -1233,7 +1229,6 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo } if param.Value.In == "path" { - //log.Printf("PATH!: %s", param.Value.Name) parameters = append(parameters, param.Value.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { @@ -1339,7 +1334,6 @@ func handlePatch(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []W } if param.Value.In == "path" { - //log.Printf("PATH!: %s", param.Value.Name) parameters = append(parameters, param.Value.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { @@ -1446,7 +1440,6 @@ func handlePut(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor } if param.Value.In == "path" { - //log.Printf("PATH!: %s", param.Value.Name) parameters = append(parameters, param.Value.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 0bfeeda9..8ea76242 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -4648,7 +4648,7 @@ func handleDeleteOutlookSub(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() workflow, err := getWorkflow(ctx, workflowId) if err != nil { - log.Printf("Failed getting the workflow locally: %s", err) + log.Printf("Failed getting the workflow locally (delete outlook): %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -4782,7 +4782,7 @@ func createOutlookSub(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() workflow, err := getWorkflow(ctx, workflowId) if err != nil { - log.Printf("Failed getting the workflow locally: %s", err) + log.Printf("Failed getting the workflow locally (outlook sub): %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -5765,7 +5765,7 @@ func runInit(ctx context.Context) { _, _, err := handleExecution(schedule.WorkflowId, Workflow{}, request) if err != nil { - log.Printf("Failed to execute: %s", err) + log.Printf("Failed to execute %s: %s", schedule.WorkflowId, err) } } diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index b9fde0ba..beda7595 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -428,7 +428,7 @@ func createSchedule(ctx context.Context, scheduleId, workflowId, name, frequency _, _, err := handleExecution(workflowId, Workflow{}, request) if err != nil { - log.Printf("Failed to execute: %s", err) + log.Printf("Failed to execute %s: %s", workflowId, err) } } @@ -1058,7 +1058,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() workflow, err := getWorkflow(ctx, fileId) if err != nil { - log.Printf("Failed getting the workflow locally: %s", err) + log.Printf("Failed getting the workflow locally (delete workflow): %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -1128,7 +1128,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { return } - log.Println("Start") + //log.Println("Start") user, userErr := handleApiAuthentication(resp, request) if userErr != nil { log.Printf("Api authentication failed in edit workflow: %s", userErr) @@ -1137,7 +1137,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { return } - log.Println("PostUser") + //log.Println("PostUser") location := strings.Split(request.URL.String(), "/") var fileId string @@ -1164,7 +1164,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { tmpworkflow, err := getWorkflow(ctx, fileId) if err != nil { - log.Printf("Failed getting the workflow locally: %s", err) + log.Printf("Failed getting the workflow locally (save workflow): %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -1211,7 +1211,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { // FIXME - this shouldn't be necessary with proper API checks newActions := []Action{} allNodes := []string{} - log.Println("Pre") + //log.Println("Pre") for _, action := range workflow.Actions { allNodes = append(allNodes, action.ID) @@ -1237,7 +1237,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { workflow.Actions = newActions for _, trigger := range workflow.Triggers { - log.Println("TRIGGERS") + //log.Println("TRIGGERS") allNodes = append(allNodes, trigger.ID) } @@ -1260,7 +1260,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { foundNodes := []string{} for _, node := range allNodes { for _, branch := range workflow.Branches { - log.Println("branch") + //log.Println("branch") //log.Println(node) //log.Println(branch.DestinationID) if node == branch.DestinationID || node == branch.SourceID { @@ -1668,7 +1668,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf if workflow.ID == "" || workflow.ID != id { tmpworkflow, err := getWorkflow(ctx, id) if err != nil { - log.Printf("Failed getting the workflow locally: %s", err) + log.Printf("Failed getting the workflow locally (execution cleanup): %s", err) return WorkflowExecution{}, "Failed getting workflow", err } @@ -1993,7 +1993,7 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() workflow, err := getWorkflow(ctx, fileId) if err != nil { - log.Printf("Failed getting the workflow locally: %s", err) + log.Printf("Failed getting the workflow locally (execute workflow): %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -2064,7 +2064,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() workflow, err := getWorkflow(ctx, fileId) if err != nil { - log.Printf("Failed getting the workflow locally: %s", err) + log.Printf("Failed getting the workflow locally (stop schedule): %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -2153,7 +2153,7 @@ func stopScheduleGCP(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() workflow, err := getWorkflow(ctx, fileId) if err != nil { - log.Printf("Failed getting the workflow locally: %s", err) + log.Printf("Failed getting the workflow locally (stop schedule GCP): %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -2275,7 +2275,7 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() workflow, err := getWorkflow(ctx, fileId) if err != nil { - log.Printf("Failed getting the workflow locally: %s", err) + log.Printf("Failed getting the workflow locally (schedule workflow): %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -3773,7 +3773,7 @@ func getWorkflowExecutions(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() workflow, err := getWorkflow(ctx, fileId) if err != nil { - log.Printf("Failed getting the workflow locally: %s", err) + log.Printf("Failed getting the workflow locally (get executions): %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return diff --git a/frontend/src/AngularWorkflow.js b/frontend/src/AngularWorkflow.js index c1701c69..0ce94cb0 100644 --- a/frontend/src/AngularWorkflow.js +++ b/frontend/src/AngularWorkflow.js @@ -1560,6 +1560,7 @@ const AngularWorkflow = (props) => { aria-controls="long-menu" aria-haspopup="true" onClick={menuClick} + style={{color: "white"}} > @@ -1965,7 +1966,7 @@ const AngularWorkflow = (props) => { node.data.type = "ACTION" node.isStartNode = action["id"] === workflow.start - return node; + return node }) const tmpelements = [].concat(actions) @@ -2001,8 +2002,10 @@ const AngularWorkflow = (props) => { } } - const handleDragStop = (e) => { + const handleDragStop = (e, app) => { newNodeId = "" + console.log("STOP!: ", e) + console.log("APP!: ", app) } const appScrollStyle = { @@ -2063,7 +2066,7 @@ const AngularWorkflow = (props) => { return( {handleAppDrag(e, app)}} - onStop={(e) => {handleDragStop(e)}} + onStop={(e) => {handleDragStop(e, app)}} dragging={false} position={{ x: 0, @@ -2703,11 +2706,11 @@ const AngularWorkflow = (props) => { } const setTriggerFolderWrapperMulti = event => { - const { options } = event.target; - const value = []; + const { options } = event.target + const value = [] for (let i = 0, l = options.length; i < l; i += 1) { if (options[i].selected) { - value.push(options[i].value); + value.push(options[i].value) } } @@ -2783,7 +2786,7 @@ const AngularWorkflow = (props) => { if (splitItems.includes(value)) { for( var i = 0; i < splitItems.length; i++){ if (splitItems[i] === value) { - splitItems.splice(i, 1); + splitItems.splice(i, 1) } } @@ -2793,7 +2796,7 @@ const AngularWorkflow = (props) => { for( var i = 0; i < splitItems.length; i++){ if (splitItems[i] === "") { - splitItems.splice(i, 1); + splitItems.splice(i, 1) } } @@ -2824,7 +2827,7 @@ const AngularWorkflow = (props) => { } const AppConditionHandler = (props) => { - const { tmpdata, type } = props; + const { tmpdata, type } = props if (tmpdata === undefined) { return tmpdata @@ -3192,8 +3195,8 @@ const AngularWorkflow = (props) => { const EdgeSidebar = () => { const ConditionHandler = (condition, index) => { - const [open, setOpen] = React.useState(false); - const [anchorEl, setAnchorEl] = React.useState(null); + const [open, setOpen] = React.useState(false) + const [anchorEl, setAnchorEl] = React.useState(null) const deleteCondition = (conditionIndex) => { console.log(selectedEdge) @@ -3223,7 +3226,7 @@ const AngularWorkflow = (props) => { const menuClick = (event) => { console.log("MENU CLICK") setOpen(!open) - setAnchorEl(event.currentTarget); + setAnchorEl(event.currentTarget) } return ( @@ -3363,7 +3366,7 @@ const AngularWorkflow = (props) => { }) .catch(error => { console.log(error.toString()) - }); + }) } const getTriggerAuth = () => { @@ -3384,7 +3387,7 @@ const AngularWorkflow = (props) => { }) .catch(error => { console.log(error.toString()) - }); + }) } // Getting the triggers and the folders if they exist @@ -3426,8 +3429,8 @@ const AngularWorkflow = (props) => { }) .catch(error => { console.log(error.toString()) - }); - }, 2500); + }) + }, 2500) console.log(data) saveWorkflow(workflow) @@ -3801,7 +3804,7 @@ const AngularWorkflow = (props) => { }) .catch(error => { alert.error(error.toString()) - }); + }) } const startMailSub = (trigger, triggerindex) => { @@ -3857,7 +3860,7 @@ const AngularWorkflow = (props) => { }) .catch(error => { alert.error(error.toString()) - }); + }) } const newWebhook = (trigger) => { @@ -3903,7 +3906,7 @@ const AngularWorkflow = (props) => { }) .catch(error => { console.log(error.toString()) - }); + }) } const deleteWebhook = (trigger, triggerindex) => { @@ -3942,7 +3945,7 @@ const AngularWorkflow = (props) => { }) .catch(error => { alert.error(error.toString()) - }); + }) } const UserinputSidebar = () => { @@ -4356,7 +4359,7 @@ const AngularWorkflow = (props) => { } if (Object.getOwnPropertyNames(selectedAction).length > 0 && Object.getOwnPropertyNames(selectedApp).length > 0) { - //console.time('ACTIONSTART'); + //console.time('ACTIONSTART') return(
{appApiView} @@ -4545,7 +4548,7 @@ const AngularWorkflow = (props) => { executionData.results.map(data => { var showResult = data.result.trim() showResult.split(" None").join(" \"None\"") - //showResult = replaceAll(showResult, " None", " \"None\""); + //showResult = replaceAll(showResult, " None", " \"None\"") var jsonvalid = true try { JSON.parse(showResult) @@ -4569,7 +4572,7 @@ const AngularWorkflow = (props) => { {jsonvalid ? @@ -4872,4 +4875,4 @@ const AngularWorkflow = (props) => { ) } -export default AngularWorkflow; +export default AngularWorkflow diff --git a/frontend/src/App.js b/frontend/src/App.js index e40d7660..af034001 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -147,13 +147,13 @@ const App = (message, props) => { // This is a mess hahahah return ( - - - - {includedData} - - - + + + + {includedData} + + + ); }; diff --git a/frontend/src/AppCreator.js b/frontend/src/AppCreator.js index 67803be1..98aa7146 100644 --- a/frontend/src/AppCreator.js +++ b/frontend/src/AppCreator.js @@ -372,30 +372,6 @@ const AppCreator = (props) => { securitySchemes = data.components.securitySchemes } - // FIXME: Have multiple authentication options? - if (securitySchemes !== undefined) { - for (const [key, value] of Object.entries(securitySchemes)) { - if (value.scheme === "bearer") { - setAuthenticationOption("Bearer auth") - break - } else if (value.type === "apiKey") { - setAuthenticationOption("API key") - - value.in = value.in.charAt(0).toUpperCase() + value.in.slice(1); - setParameterLocation(value.in) - if (!apikeySelection.includes(value.in)) { - console.log("APIKEY SELECT: ", apikeySelection) - alert.error("Might be error in setting up API key authentication") - } - - setParameterName(value.name) - break - } else if (value.scheme === "basic") { - setAuthenticationOption("Basic auth") - break - } - } - } console.log(data) @@ -451,9 +427,36 @@ const AppCreator = (props) => { } } + + // FIXME: Have multiple authentication options? + if (securitySchemes !== undefined) { + for (const [key, value] of Object.entries(securitySchemes)) { + if (value.scheme === "bearer") { + setAuthenticationOption("Bearer auth") + break + } else if (value.type === "apiKey") { + setAuthenticationOption("API key") + + value.in = value.in.charAt(0).toUpperCase() + value.in.slice(1); + setParameterLocation(value.in) + if (!apikeySelection.includes(value.in)) { + console.log("APIKEY SELECT: ", apikeySelection) + alert.error("Might be error in setting up API key authentication") + } + + console.log("PARAM NAME: ", value.name) + setParameterName(value.name) + break + } else if (value.scheme === "basic") { + setAuthenticationOption("Basic auth") + break + } + } + } + setActions(newActions) } - + // Saving the app that's been configured. const submitApp = () => { alert.info("Uploading and building app " + name) @@ -810,7 +813,7 @@ const AppCreator = (props) => { id="standard-required" margin="normal" variant="outlined" - defaultValue={parameterName} + value={parameterName} helperText={
Can't be empty. Can't contain any of the following characters: !#$%&'^+-._~|]+$
} onChange={e => setParameterName(e.target.value)} InputProps={{ @@ -926,11 +929,13 @@ const AppCreator = (props) => { {data.method} - {url} - {data.name}
+ {/*
{testAction(index)}}> Test
+ */}
{deleteAction(index)}}> Delete @@ -1576,13 +1581,15 @@ const AppCreator = (props) => {
{actionView}
+ {/* {testView} + */} {errorCode.length > 0 ? `Error: ${errorCode}` : null} diff --git a/frontend/src/Apps.js b/frontend/src/Apps.js index eb3b9e58..12934d79 100644 --- a/frontend/src/Apps.js +++ b/frontend/src/Apps.js @@ -462,7 +462,8 @@ const Apps = (props) => {
Apps interact with eachother in workflows. They are created with the app creator, using OpenAPI specification or manually in python. Use the links above to find potential apps you're looking for using OpenAPI or make one from scratch. There's 1000+ available.
-
+ +
@@ -962,10 +983,160 @@ const Workflows = (props) => {
+ + const importWorkflowsFromUrl = (url) => { + console.log("IMPORT WORKFLOWS FROM ", downloadUrl) + + const parsedData = { + "url": url, + } + + if (field1.length > 0) { + parsedData["field_1"] = field1 + } + + if (field2.length > 0) { + parsedData["field_2"] = field2 + } + + alert.success("Getting specific workflows from your URL.") + var cors = "cors" + fetch(globalUrl+"/api/v1/workflows/download_remote", { + method: "POST", + mode: "cors", + headers: { + 'Accept': 'application/json', + }, + body: JSON.stringify(parsedData), + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + response.text().then(function (text) { + console.log("RETURN: ", text) + alert.success("Loaded existing apps!") + }) + } + + return response.json() + }) + .then((responseJson) => { + console.log("DATA: ", responseJson) + if (responseJson.reason !== undefined) { + alert.error("Failed loading: "+responseJson.reason) + } else { + alert.error("Failed loading") + } + }) + .catch(error => { + alert.error(error.toString()) + }) + } + + const handleGithubValidation = () => { + importWorkflowsFromUrl(downloadUrl) + setLoadWorkflowsModalOpen(false) + } + + const workflowDownloadModalOpen = loadWorkflowsModalOpen ? + { + }} + PaperProps={{ + style: { + backgroundColor: surfaceColor, + color: "white", + minWidth: "800px", + minHeight: "320px", + }, + }} + > + +
+ Load workflows from github repo +
+ + + +
+
+
+ + Repository (supported: github, gitlab, bitbucket) + setDownloadUrl(e.target.value)} + placeholder="https://github.com/frikky/shuffle-apps" + fullWidth + /> + + Authentication (optional - private repos etc): +
+ setField1(e.target.value)} + type="username" + placeholder="Username / APIkey (optional)" + fullWidth + /> + setField2(e.target.value)} + type="password" + placeholder="Password (optional)" + fullWidth + /> +
+
+ + + + +
+ : null + const loadedCheck = isLoaded && isLoggedIn && workflowDone ?
{workflowView} {modalView} + {workflowDownloadModalOpen}
:
From 3cd1f02ca770b308b5fa34b5d7dd395f73a4aad6 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 13 Jun 2020 10:12:25 +0200 Subject: [PATCH 07/18] Made docs easier to read --- backend/go-app/main.go | 1 + backend/go-app/walkoff.go | 98 +++++++++++++++++++-------------------- frontend/src/Apps.js | 8 +--- frontend/src/Docs.js | 26 ++++++++++- 4 files changed, 74 insertions(+), 59 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 998e04e8..29f3cf9a 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -5955,6 +5955,7 @@ func init() { // App specific r.HandleFunc("/api/v1/apps/get_existing", loadSpecificApps).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/apps/download_remote", loadSpecificApps).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/apps/validate", validateAppInput).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/apps/{appId}", deleteWorkflowApp).Methods("DELETE", "OPTIONS") r.HandleFunc("/api/v1/apps/{appId}/config", getWorkflowAppConfig).Methods("GET", "OPTIONS") diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 2c9e29cc..cf165594 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -3661,7 +3661,6 @@ func iterateWorkflowGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra // Onlyname is used to func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra string, onlyname string) error { var err error - runUpload := false for _, file := range dir { if len(onlyname) > 0 && file.Name() != onlyname { continue @@ -3688,83 +3687,71 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin if filename == "Dockerfile" { log.Printf("Handle Dockerfile in location %s", extra) - extraSplit := strings.Split(extra, "/") - tags := []string{} - if len(extraSplit) > 1 { - tags = []string{ - fmt.Sprintf("%s:%s_%s", baseDockerName, strings.ReplaceAll(extraSplit[0], " ", "-"), extraSplit[1]), - // Version = folder of last part of extra - // Name = first folder of extra - } - } else { - // Skip - runUpload = false - log.Printf("Skipping folder %s because the extra variable is empty~", extra) - break - //return nil - } - - /// Only upload if successful and no errors - err := buildImageMemory(fs, tags, extra) - if err != nil { - log.Printf("Failed image build memory: %s", err) - runUpload = false - } else { - runUpload = true - } - } - } - } - - // Done sequentailly to prevent bad uploads - if runUpload && err == nil { - for _, file := range dir { - if file.Name() == "api.yaml" || file.Name() == "api.yaml" { - log.Printf("Run update of %sapi.yaml in backend if it doesn't exist!!", extra) - fullPath := fmt.Sprintf("%s%s", extra, file.Name()) - + // Try api.yaml and api.yml + fullPath := fmt.Sprintf("%s%s", extra, "api.yaml") fileReader, err := fs.Open(fullPath) if err != nil { - return err + fullPath = fmt.Sprintf("%s%s", extra, "api.yml") + fileReader, err = fs.Open(fullPath) + if err != nil { + log.Printf("Failed finding api.yaml/yml: %s", err) + continue + } } readFile, err := ioutil.ReadAll(fileReader) if err != nil { - log.Printf("Filereader error: %s", err) - return err + log.Printf("Failed reading %s: %s", fullPath, err) + continue } var workflowapp WorkflowApp err = gyaml.Unmarshal(readFile, &workflowapp) if err != nil { - log.Printf("Failed api.yaml unmarshal: %s", err) - return err + log.Printf("Failed unmarshaling %s: %s", fullPath, err) + continue } - log.Printf("APIName: %s", workflowapp.Name) - extraSplit := strings.Split(extra, "/") - appName := fmt.Sprintf("%s_%s", strings.ReplaceAll(extraSplit[0], " ", "-"), extraSplit[1]) + newName := workflowapp.Name + newName = strings.ReplaceAll(newName, " ", "-") + + tags := []string{ + fmt.Sprintf("%s:%s_%s", baseDockerName, newName, workflowapp.AppVersion), + } ctx := context.Background() allapps, err := getAllWorkflowApps(ctx) if err != nil { log.Printf("Failed getting apps to verify: %s", err) - return err + continue + //return err } log.Printf("APPS: %d", len(allapps)) + // Make an option to override existing apps? + removeApps := []string{} for _, app := range allapps { if app.Name == workflowapp.Name && app.AppVersion == workflowapp.AppVersion { - log.Printf("App upload for %s:%s already exists.", app.Name, app.AppVersion) - return errors.New(fmt.Sprintf("App %s already exists. ", appName)) + //log.Printf("App upload for %s:%s already exists.", app.Name, app.AppVersion) + log.Printf("Overriding app %s:%s as it exists.", app.Name, app.AppVersion) + removeApps = append(removeApps, app.ID) } } err = checkWorkflowApp(workflowapp) if err != nil { log.Printf("%s for app %s:%s", err, workflowapp.Name, workflowapp.AppVersion) - return err + continue + } + + if len(removeApps) > 0 { + for _, item := range removeApps { + err = DeleteKey(ctx, "workflowapp", item) + if err != nil { + log.Printf("Failed deleting %s", item) + } + } } //if workflowapp.Environment == "" { @@ -3780,7 +3767,8 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin err = setWorkflowAppDatastore(ctx, workflowapp, workflowapp.ID) if err != nil { log.Printf("Failed setting workflowapp: %s", err) - return err + continue + //return err } err = increaseStatisticsField(ctx, "total_apps_created", workflowapp.ID, 1) @@ -3794,8 +3782,16 @@ func iterateAppGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra strin } log.Printf("Added %s:%s to the database", workflowapp.Name, workflowapp.AppVersion) - //memcache.Delete(ctx, "all_apps") - //os.Exit(3) + + /// Only upload if successful and no errors + err = buildImageMemory(fs, tags, extra) + if err != nil { + log.Printf("Failed image build memory: %s", err) + } else { + if len(tags) > 0 { + log.Printf("Successfully built image %s", tags[0]) + } + } } } } diff --git a/frontend/src/Apps.js b/frontend/src/Apps.js index d0c6ff9e..c4cd8878 100644 --- a/frontend/src/Apps.js +++ b/frontend/src/Apps.js @@ -648,10 +648,7 @@ const Apps = (props) => { }) .then((response) => { if (response.status === 200) { - response.text().then(function (text) { - console.log("RETURN: ", text) - alert.success("Loaded existing apps!") - }) + alert.success("Loaded existing apps!") } setIsLoading(false) stop() @@ -662,11 +659,10 @@ const Apps = (props) => { console.log("DATA: ", responseJson) if (responseJson.reason !== undefined) { alert.error("Failed loading: "+responseJson.reason) - } else { - alert.error("Failed loading") } }) .catch(error => { + console.log("ERROR: ", error.toString()) alert.error(error.toString()) }) } diff --git a/frontend/src/Docs.js b/frontend/src/Docs.js index 2ac9c2f3..9a0df2ea 100644 --- a/frontend/src/Docs.js +++ b/frontend/src/Docs.js @@ -32,7 +32,7 @@ const hrefStyle = { } const Docs = (props) => { - const { isLoaded, globalUrl } = props; + const { isLoaded, globalUrl, inputColor } = props; const [data, setData] = useState(""); const [firstrequest, setFirstrequest] = useState(true); @@ -179,6 +179,23 @@ const Docs = (props) => { return {props.alt} } + function CodeHandler(props) { + return {props.value} + } + + function Heading(props) { + const element = React.createElement(`h${props.level}`, {style: {marginTop: 25}}, props.children) + console.log(props) + return ( + + + {element} + + ) + } + //React.createElement("p", {style: {color: "red", backgroundColor: "blue"}}, this.props.paragraph) + + //function unicodeToChar(text) { // return text.replace(/\\u[\dA-F]{4}/gi, // function (match) { @@ -214,7 +231,12 @@ const Docs = (props) => { id="markdown_wrapper" escapeHtml={false} source={data} - renderers={{link: OuterLink, image: Img}} + renderers={{ + link: OuterLink, + image: Img, + code: CodeHandler, + heading: Heading, + }} />
From 3f42b49284971fa0422b312fdd03aa4d9941a9f0 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 13 Jun 2020 13:05:47 +0200 Subject: [PATCH 08/18] Added default start node to new workflows --- backend/go-app/codegen.go | 1 - backend/go-app/walkoff.go | 64 +++++++++++++++++++++++++++------ frontend/src/AngularWorkflow.js | 24 +++++++++---- 3 files changed, 71 insertions(+), 18 deletions(-) diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index 256dd34b..8b6c4f0c 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -411,7 +411,6 @@ func generateYaml(swagger *openapi3.Swagger, newmd5 string) (*openapi3.Swagger, //log.Printf("%s", j) api.SmallImage = string(j) api.LargeImage = string(j) - log.Printf("Set images!") } } diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index cf165594..ef946c42 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -966,18 +966,10 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { workflow.Sharing = "private" ctx := context.Background() - err = setWorkflow(ctx, workflow, workflow.ID) - if err != nil { - log.Printf("Failed setting workflow: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - log.Printf("Saved new workflow %s with name %s", workflow.ID, workflow.Name) err = increaseStatisticsField(ctx, "total_workflows", workflow.ID, 1) if err != nil { - log.Printf("Failed to increase total workflows: %s", err) + log.Printf("Failed to increase total workflows stats: %s", err) } if len(workflow.Actions) == 0 { @@ -1003,6 +995,37 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { newActions = append(newActions, action) } + // Initialized without functions = adding a hello world node. + if len(newActions) == 0 { + log.Printf("APPENDING NEW APP FOR NEW WORKFLOW") + //nodeId := "40447f30-fa44-4a4f-a133-4ee710368737" + //workflow.Start = nodeId + //newActions = append(newActions, Action{ + // Label: "Start node", + // Name: "hello_world", + // AppName: "testing", + // Environment: "Shuffle", + // Parameters: []WorkflowAppActionParameter{}, + // Position: struct { + // X float64 "json:\"x\" datastore:\"x\"" + // Y float64 "json:\"y\" datastore:\"y\"" + // }{X: 449.5, Y: 446}, + // Priority: 0, + // AppVersion: "1.0.0", + // AppID: "c567fc10-9c15-403e-b72c-6550e9e76bc8", + // Errors: []string{}, + // ID: nodeId, + // IsValid: true, + // IsStartNode: true, + // Sharing: true, + // PrivateID: "", + // SmallImage: "", + // LargeImage: "data:image/png;base64,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", + //}) + } else { + log.Printf("Has actions already?") + } + workflow.Actions = newActions workflow.IsValid = true @@ -1014,6 +1037,14 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { return } + err = setWorkflow(ctx, workflow, workflow.ID) + if err != nil { + log.Printf("Failed setting workflow: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + //memcacheName := fmt.Sprintf("%s_workflows", user.Username) //memcache.Delete(ctx, memcacheName) @@ -1444,6 +1475,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } } + log.Printf("NewActions: %#v", newActions) workflow.Actions = newActions workflow.IsValid = true @@ -1462,7 +1494,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { log.Printf("Failed to change total actions data: %s", err) } - log.Printf("Saved new version of workflow %s", fileId) + log.Printf("Saved new version of workflow %s (%s)", workflow.Name, fileId) resp.WriteHeader(200) resp.Write([]byte(`{"success": true}`)) } @@ -1736,6 +1768,18 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf if len(execution.Start) == 36 { log.Printf("SHOULD START ON NODE %s", execution.Start) workflow.Start = execution.Start + + found := false + for _, action := range workflow.Actions { + if action.ID == workflow.Start { + found = true + } + } + + if !found { + log.Printf("ACTION %s WAS NOT FOUND!", workflow.Start) + return WorkflowExecution{}, fmt.Sprintf("Startnode %s was not found in actions", workflow.Start), errors.New(fmt.Sprintf("Startnode %s was not found in actions", workflow.Start)) + } } if len(execution.ExecutionId) == 36 { diff --git a/frontend/src/AngularWorkflow.js b/frontend/src/AngularWorkflow.js index 0ce94cb0..e61bcf84 100644 --- a/frontend/src/AngularWorkflow.js +++ b/frontend/src/AngularWorkflow.js @@ -262,7 +262,7 @@ const AngularWorkflow = (props) => { const abortExecution = () => { setExecutionRunning(false) - alert.success("Aborting execution") + alert.info("Aborting execution") fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/executions/"+executionRequest.execution_id+"/abort", { method: 'GET', headers: { @@ -274,7 +274,9 @@ const AngularWorkflow = (props) => { .then((response) => { if (response.status !== 200) { console.log("Status not 200 for WORKFLOW EXECUTION :O!") - } + } else { + alert.success("Execution aborted") + } return response.json() }) @@ -657,7 +659,13 @@ const AngularWorkflow = (props) => { .then((responseJson) => { if (!responseJson.success) { alert.error("Failed to start: "+responseJson.reason) + setExecutionRunning(false) + setExecutionRequestStarted(false) stop() + + for (var i = 0; i < curelements.length; i++) { + curelements[i].removeClass("not-executing-highlight") + } return } else { setExecutionRunning(true) @@ -4482,11 +4490,13 @@ const AngularWorkflow = (props) => {
{timestamp}
- -
- {resultsLength}/{data.workflow.actions.length} -
-
+ {data.workflow.actions !== null ? + +
+ {resultsLength}/{data.workflow.actions.length} +
+
+ : null}
From a7c03faf999ee3171b42f9776565c9352c477420 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 13 Jun 2020 13:27:31 +0200 Subject: [PATCH 09/18] Cleaned up main folder --- README.md | 2 +- frontend/src/assets/img/logo.png | Bin 0 -> 59674 bytes frontend/src/assets/img/logo.svg | 26 ++++++++++++++ frontend/src/assets/img/logo.webp | Bin 0 -> 5822 bytes .../src/assets/img/shuffle_adminaccount.png | Bin .../src/assets/img/shuffle_webhook.png | Bin setup.sh | 33 ------------------ 7 files changed, 27 insertions(+), 34 deletions(-) create mode 100644 frontend/src/assets/img/logo.png create mode 100644 frontend/src/assets/img/logo.svg create mode 100644 frontend/src/assets/img/logo.webp rename shuffle_adminaccount.png => frontend/src/assets/img/shuffle_adminaccount.png (100%) rename shuffle_webhook.png => frontend/src/assets/img/shuffle_webhook.png (100%) delete mode 100644 setup.sh diff --git a/README.md b/README.md index 7de816df..7262a756 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **It's in BETA** - [Get in touch](https://shuffler.io/contact), send a mail to [frikky@shuffler.io](mailto:frikky@shuffler.io) or poke me on twitter [@frikkylikeme](https://twitter.com/frikkylikeme) -![Example Shuffle webhook integration](shuffle_webhook.png) +![Example Shuffle webhook integration](https://github.com/frikky/Shuffle/blob/master/frontend/src/assets/img/shuffle_webhook.png) ## Getting started * Self-hosted: Check out the [installation guide](https://github.com/frikky/shuffle/blob/master/install-guide.md) and [getting started](https://shuffler.io/docs/getting_started) diff --git a/frontend/src/assets/img/logo.png b/frontend/src/assets/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c6826517427946aac818dd6cd89608302b6a2053 GIT binary patch literal 59674 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 literal 0 HcmV?d00001 diff --git a/frontend/src/assets/img/logo.svg b/frontend/src/assets/img/logo.svg new file mode 100644 index 00000000..3c516967 --- /dev/null +++ b/frontend/src/assets/img/logo.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + background + + + + Layer 1 + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/img/logo.webp b/frontend/src/assets/img/logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..d1dadeddb67f954f227601d374b026415eb23938 GIT binary patch literal 5822 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 literal 0 HcmV?d00001 diff --git a/shuffle_adminaccount.png b/frontend/src/assets/img/shuffle_adminaccount.png similarity index 100% rename from shuffle_adminaccount.png rename to frontend/src/assets/img/shuffle_adminaccount.png diff --git a/shuffle_webhook.png b/frontend/src/assets/img/shuffle_webhook.png similarity index 100% rename from shuffle_webhook.png rename to frontend/src/assets/img/shuffle_webhook.png diff --git a/setup.sh b/setup.sh deleted file mode 100644 index 6ceb1699..00000000 --- a/setup.sh +++ /dev/null @@ -1,33 +0,0 @@ -# Build script to make it work as an open source platform - -# 1. Grab builtin functions - Done in backend as a button click -# 2. Upload to database & build docker images -# 3. Run docker-compose: frontend, backend, database & orborus -# 4. Set up docker swarm for apps? - -# Basic overview of how it works: -# -# Backend: o -# | -# Orborus: o -# / \ -# Workers: o o -# / / \ -# Apps: o o o - -# 1. Grab builtin functions -# Where should I have these? Maybe OpenAPI github repo and just preload? - -echo "Building frontend" -cd frontend -npm run build -rm -rf ../backend/go-app/build -cp -r build/ ../backend/go-app/build - -echo "Setting up backend" -cd ../backend/go-app -go build -go test - -#gcloud app deploy $GOPATH/src/github.com/frikky/shuffle/app.yaml -echo "This script is not finished. Please follow the docker installation guide here: https://github.com/frikky/shuffle/blob/master/install-guide.md" From 927ca5504d47ca103c523fc87e215a2815b86b68 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 13 Jun 2020 14:02:05 +0200 Subject: [PATCH 10/18] Fixed left sidebar tabbing and sizing --- frontend/src/AngularWorkflow.js | 208 +++++++++++--------------------- 1 file changed, 68 insertions(+), 140 deletions(-) diff --git a/frontend/src/AngularWorkflow.js b/frontend/src/AngularWorkflow.js index e61bcf84..28feea14 100644 --- a/frontend/src/AngularWorkflow.js +++ b/frontend/src/AngularWorkflow.js @@ -10,6 +10,8 @@ import Drawer from '@material-ui/core/Drawer'; import Button from '@material-ui/core/Button'; import Paper from '@material-ui/core/Paper'; import Grid from '@material-ui/core/Grid'; +import Tabs from '@material-ui/core/Tabs'; +import Tab from '@material-ui/core/Tab'; import ButtonBase from '@material-ui/core/ButtonBase'; import Tooltip from '@material-ui/core/Tooltip'; import Select from '@material-ui/core/Select'; @@ -100,7 +102,7 @@ const AngularWorkflow = (props) => { const [cy, setCy] = React.useState() const [appSearch, setAppSearch] = React.useState("") - const [currentView, setCurrentView] = React.useState("apps") + const [currentView, setCurrentView] = React.useState(0) const [triggerAuthentication, setTriggerAuthentication] = React.useState({}) const [triggerFolders, setTriggerFolders] = React.useState([]) @@ -164,9 +166,6 @@ const AngularWorkflow = (props) => { const [lastSaved, setLastSaved] = React.useState(true) - const [AppsHoverColor, setAppsHoverColor] = useState(hoverOutColor); - const [VariablesHoverColor, setVariablesHoverColor] = useState(hoverOutColor); - const [HookHoverColor, setHookHoverColor] = useState(hoverOutColor); const [appAdded, setAppAdded] = useState(false) const [update, setUpdate] = useState(""); const [workflowExecutions, setWorkflowExecutions] = React.useState([]); @@ -549,68 +548,6 @@ const AngularWorkflow = (props) => { return true } - const executeWorkflowWebsocket = () => { - if (!lastSaved) { - //alert.error("You might have forgotten to save before executing.") - console.log("FIXME: Might have forgotten to save before executing.") - } - - var returncheck = monitorUpdates() - if (!returncheck) { - alert.error("No startnode set.") - return - } - - setVisited([]) - setExecutionRunning(true) - setExecutionRequest({}) - - var curelements = cy.elements() - for (var i = 0; i < curelements.length; i++) { - curelements[i].addClass("not-executing-highlight") - } - - if (executionText.length > 0) { - alert.success("Starting execution with argument "+executionText) - } else { - alert.success("Starting execution") - } - - const data = {"execution_argument": executionText} - fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/execute_fs", { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - body: JSON.stringify(data), - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for WORKFLOW EXECUTION :O!") - } - - return response.json() - }) - .then((responseJson) => { - if (!responseJson.success) { - alert.error("Failed to start: "+responseJson.reason) - stop() - return - } - - setExecutionRequest({ - "execution_id": responseJson.execution_id, - "authorization": responseJson.authorization, - }) - setExecutingNodes([workflow.start]) - }) - .catch(error => { - alert.error(error.toString()) - }); - } - const executeWorkflow = () => { if (!lastSaved) { //alert.error("You might have forgotten to save before executing.") @@ -641,14 +578,14 @@ const AngularWorkflow = (props) => { const data = {"execution_argument": executionText, "start": workflow.start} fetch(globalUrl+"/api/v1/workflows/"+props.match.params.key+"/execute", { - method: 'POST', + method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, credentials: "include", body: JSON.stringify(data), - }) + }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for WORKFLOW EXECUTION :O!") @@ -1437,16 +1374,17 @@ const AngularWorkflow = (props) => { } const appViewStyle = { - marginLeft: "5px", - marginRight: "5px", + marginLeft: 5, + marginRight: 5, display: "flex", flexDirection: "column", + height: "100%", } const scrollStyle = { - marginTop: "10px", + marginTop: 10, overflow: "scroll", - height: "66vh", + height: "100%", overflowX: "auto", overflowY: "auto", } @@ -1463,36 +1401,6 @@ const AngularWorkflow = (props) => { display: "flex", } - // All this is stupid lmao - const handleHookHover = () => { - setHookHoverColor(hoverColor) - setAppsHoverColor(hoverOutColor) - setVariablesHoverColor(hoverOutColor) - } - - const handleHookHoverOut = () => { - setHookHoverColor(hoverOutColor) - } - - const handleAppsHover = () => { - setAppsHoverColor(hoverColor) - setHookHoverColor(hoverOutColor) - setVariablesHoverColor(hoverOutColor) - } - - const handleAppsHoverOut = () => { - setAppsHoverColor(hoverOutColor) - } - - const handleVariablesHover = () => { - setVariablesHoverColor(hoverColor) - setHookHoverColor(hoverOutColor) - setAppsHoverColor(hoverOutColor) - } - - const handleVariablesHoverOut = () => { - setVariablesHoverColor(hoverOutColor) - } const paperVariableStyle = { minHeight: "50px", @@ -1614,57 +1522,77 @@ const AngularWorkflow = (props) => {
) } + + const curTab = 0 + const handleSetTab = (event, newValue) => { + setCurrentView(newValue) + } const HandleLeftView = () => { // Defaults to apps. var thisview = - if (currentView === "triggers") { + if (currentView === 1) { thisview = - } else if (currentView === "variables") { + } else if (currentView === 2) { thisview = } + const tabStyle = { + maxWidth: leftBarSize/3, + minWidth: leftBarSize/3, + flex: 1, + textTransform: "none", + } + + const iconStyle = { + marginTop: 3, + marginRight: 5, + } + return(
- -
+
{thisview}
-
- -
-
{setCurrentView("apps")}}> - - - - + + + - Apps - + -
-
{setCurrentView("triggers")}}> - - - - + + Apps + + + } style={tabStyle} /> + - Triggers - + -
-
{setCurrentView("variables")}}> - - - - + + Triggers + + + } style={tabStyle} /> + - Variables - + -
-
-
+ + Variables + + + }style={tabStyle} /> +
) } @@ -2018,8 +1946,8 @@ const AngularWorkflow = (props) => { const appScrollStyle = { overflow: "scroll", - maxHeight: bodyHeight-appBarSize-150, - minHeight: bodyHeight-appBarSize-150, + maxHeight: bodyHeight-appBarSize-55, + minHeight: bodyHeight-appBarSize-55, overflowY: "auto", overflowX: "hidden", } @@ -2075,6 +2003,7 @@ const AngularWorkflow = (props) => { {handleAppDrag(e, app)}} onStop={(e) => {handleDragStop(e, app)}} + key={app.id} dragging={false} position={{ x: 0, @@ -2438,7 +2367,7 @@ const AngularWorkflow = (props) => { } else if (data.variant === "WORKFLOW_VARIABLE") { varcolor = "#f85a3e" if (workflow.workflow_variables === null || workflow.workflow_variables === undefined || workflow.workflow_variables.length === 0) { - setCurrentView("variables") + setCurrentView(2) datafield =
@@ -2944,7 +2873,7 @@ const AngularWorkflow = (props) => { } else if (data.variant === "WORKFLOW_VARIABLE") { varcolor = "#f85a3e" if (workflow.workflow_variables === null || workflow.workflow_variables === undefined || workflow.workflow_variables.length === 0) { - setCurrentView("variables") + setCurrentView(2) datafield =
@@ -4621,7 +4550,6 @@ const AngularWorkflow = (props) => { -
:
From 18824daeccff1474bd4b6eff2f5ded66b2ad1e4b Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 13 Jun 2020 15:51:18 +0200 Subject: [PATCH 11/18] #57: Added optiona SSL verification if no URL is specified when creating app --- backend/go-app/codegen.go | 205 +++++++++++++++++++++++++++++++- backend/go-app/walkoff.go | 1 - docker-compose.yml | 24 ++-- frontend/src/AngularWorkflow.js | 1 - frontend/src/AppCreator.js | 20 ++-- frontend/src/Apps.js | 8 +- 6 files changed, 228 insertions(+), 31 deletions(-) diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index 8b6c4f0c..48711b1a 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -300,6 +300,23 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet urlInline = "{url}" } + // Specific check for SSL verification + // This is critical for onprem stuff. + verifyParam := "" + verifyWrapper := "" + verifyAddin := "" + if len(swagger.Servers) == 0 { + verifyParam = ", verify=True" + verifyWrapper = `if type(ssl_verify) == str: ssl_verify = False if ssl_verify.lower() == "false" or ssl_verify == "0" else True` + verifyAddin = ", verify=ssl_verify" + } else { + if swagger.Servers[0].URL == "" { + verifyParam = ", ssl_verify=True" + verifyWrapper = `if type(ssl_verify) == str: ssl_verify = False if ssl_verify.lower() == "false" or ssl_verify == "0" else True` + verifyAddin = ", verify=ssl_verify" + } + } + if len(parameters) > 0 { parameterData = fmt.Sprintf(", %s", strings.Join(parameters, ", ")) } @@ -334,13 +351,14 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet // Extra param for url if it's changeable // Extra param for authentication scheme(s) - data := fmt.Sprintf(` async def %s(self%s%s%s%s%s): + data := fmt.Sprintf(` async def %s(self%s%s%s%s%s%s): headers={} url=f"%s%s" %s + %s %s %s - return requests.%s(url, headers=headers%s%s).text + return requests.%s(url, headers=headers%s%s%s).text `, functionname, authenticationParameter, @@ -348,16 +366,21 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet parameterData, queryString, bodyParameter, + verifyParam, urlInline, url, + verifyWrapper, authenticationSetup, queryData, bodyFormatter, method, authenticationAddin, bodyAddin, + verifyAddin, ) + log.Printf("CODE: %s", data) + //log.Println(data) //log.Println(functionname) return functionname, data @@ -761,6 +784,32 @@ func handleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [ optionalQueries := []string{} parameters := []string{} optionalParameters := []WorkflowAppActionParameter{} + if len(swagger.Servers) == 0 { + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + } else { + if swagger.Servers[0].URL == "" { + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + } + } + if len(path.Connect.Parameters) > 0 { for _, param := range path.Connect.Parameters { if param.Value.Schema == nil || param.Value.In == "header" { @@ -867,8 +916,33 @@ func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor // FIXME - remove this when authentication is properly introduced parameters := []string{} - optionalParameters := []WorkflowAppActionParameter{} + if len(swagger.Servers) == 0 { + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify the SSL certificate request", + Multiline: false, + Required: false, + Example: "False - default=True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + } else { + if swagger.Servers[0].URL == "" { + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + } + } + if len(path.Get.Parameters) > 0 { for _, param := range path.Get.Parameters { if param.Value.Schema == nil || param.Value.In == "header" { @@ -976,6 +1050,31 @@ func handleHead(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo optionalQueries := []string{} parameters := []string{} optionalParameters := []WorkflowAppActionParameter{} + if len(swagger.Servers) == 0 { + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + } else { + if swagger.Servers[0].URL == "" { + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + } + } if len(path.Head.Parameters) > 0 { for _, param := range path.Head.Parameters { if param.Value.Schema == nil || param.Value.In == "header" { @@ -1081,6 +1180,31 @@ func handleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [] optionalQueries := []string{} parameters := []string{} optionalParameters := []WorkflowAppActionParameter{} + if len(swagger.Servers) == 0 { + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + } else { + if swagger.Servers[0].URL == "" { + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + } + } if len(path.Delete.Parameters) > 0 { for _, param := range path.Delete.Parameters { if param.Value.Schema == nil || param.Value.In == "header" { @@ -1189,6 +1313,31 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo optionalQueries := []string{} parameters := []string{} optionalParameters := []WorkflowAppActionParameter{} + if len(swagger.Servers) == 0 { + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + } else { + if swagger.Servers[0].URL == "" { + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + } + } if len(path.Post.Parameters) > 0 { for _, param := range path.Post.Parameters { @@ -1296,6 +1445,31 @@ func handlePatch(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []W optionalQueries := []string{} parameters := []string{} optionalParameters := []WorkflowAppActionParameter{} + if len(swagger.Servers) == 0 { + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + } else { + if swagger.Servers[0].URL == "" { + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + } + } if len(path.Patch.Parameters) > 0 { for _, param := range path.Patch.Parameters { if param.Value.Schema == nil || param.Value.In == "header" { @@ -1401,6 +1575,31 @@ func handlePut(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor optionalQueries := []string{} parameters := []string{} optionalParameters := []WorkflowAppActionParameter{} + if len(swagger.Servers) == 0 { + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + } else { + if swagger.Servers[0].URL == "" { + optionalParameters = append(optionalParameters, WorkflowAppActionParameter{ + Name: "ssl_verify", + Description: "Check if you want to verify request", + Multiline: false, + Required: false, + Example: "True", + Schema: SchemaDefinition{ + Type: "string", + }, + }) + } + } if len(path.Put.Parameters) > 0 { for _, param := range path.Put.Parameters { diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index ef946c42..2f3b6391 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -1475,7 +1475,6 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } } - log.Printf("NewActions: %#v", newActions) workflow.Actions = newActions workflow.IsValid = true diff --git a/docker-compose.yml b/docker-compose.yml index 0fbc7c41..c00daf34 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,18 +13,6 @@ services: restart: unless-stopped depends_on: - backend - database: - #build: ./backend/database - image: frikky/shuffle:database - container_name: shuffle-database - hostname: shuffle-database - ports: - - "8000:8000" - networks: - - shuffle - restart: unless-stopped - volumes: - - ${DB_LOCATION}:/etc/shuffle backend: #build: ./backend image: frikky/shuffle:backend @@ -59,6 +47,18 @@ services: - BASE_URL=http://${OUTER_HOSTNAME}:${BACKEND_PORT} - DOCKER_API_VERSION=1.40 restart: unless-stopped + database: + #build: ./backend/database + image: frikky/shuffle:database + container_name: shuffle-database + hostname: shuffle-database + ports: + - "8000:8000" + networks: + - shuffle + restart: unless-stopped + volumes: + - ${DB_LOCATION}:/etc/shuffle networks: shuffle: driver: bridge diff --git a/frontend/src/AngularWorkflow.js b/frontend/src/AngularWorkflow.js index 28feea14..ccb2d3ab 100644 --- a/frontend/src/AngularWorkflow.js +++ b/frontend/src/AngularWorkflow.js @@ -1401,7 +1401,6 @@ const AngularWorkflow = (props) => { display: "flex", } - const paperVariableStyle = { minHeight: "50px", maxHeight: "50px", diff --git a/frontend/src/AppCreator.js b/frontend/src/AppCreator.js index 3a269b5c..690b73ea 100644 --- a/frontend/src/AppCreator.js +++ b/frontend/src/AppCreator.js @@ -67,7 +67,7 @@ const useStyles = makeStyles({ const rewrite = (args) => { return args.reduce(function(args, a){ - if (0 == a.indexOf('-X')) { + if (0 === a.indexOf('-X')) { args.push('-X') args.push(a.slice(2)) } else { @@ -105,35 +105,35 @@ const parseCurl = (s) => { out.url = arg break; - case arg == '-A' || arg == '--user-agent': + case arg === '-A' || arg === '--user-agent': state = 'user-agent' break; - case arg == '-H' || arg == '--header': + case arg === '-H' || arg === '--header': state = 'header' break; - case arg == '-d' || arg == '--data' || arg == '--data-ascii': + case arg === '-d' || arg === '--data' || arg === '--data-ascii': state = 'data' break; - case arg == '-u' || arg == '--user': + case arg === '-u' || arg === '--user': state = 'user' break; - case arg == '-I' || arg == '--head': + case arg === '-I' || arg === '--head': out.method = 'HEAD' break; - case arg == '-X' || arg == '--request': + case arg === '-X' || arg === '--request': state = 'method' break; - case arg == '-b' || arg =='--cookie': + case arg === '-b' || arg === '--cookie': state = 'cookie' break; - case arg == '--compressed': + case arg === '--compressed': out.header['Accept-Encoding'] = out.header['Accept-Encoding'] || 'deflate, gzip' break; @@ -149,7 +149,7 @@ const parseCurl = (s) => { state = '' break; case 'data': - if (out.method == 'GET' || out.method == 'HEAD') out.method = 'POST' + if (out.method === 'GET' || out.method === 'HEAD') out.method = 'POST' out.header['Content-Type'] = out.header['Content-Type'] || 'application/x-www-form-urlencoded' out.body = out.body ? out.body + '&' + arg diff --git a/frontend/src/Apps.js b/frontend/src/Apps.js index c4cd8878..a87bbf4e 100644 --- a/frontend/src/Apps.js +++ b/frontend/src/Apps.js @@ -512,9 +512,9 @@ const Apps = (props) => { } const appView = isLoggedIn ? -
+
1366 ? 1366 : 1200, margin: "auto",}}>
-
+

@@ -532,10 +532,10 @@ const Apps = (props) => {

- +
-
+

Available integrations

{isLoading ? : null} From 54ed729056ea4b9ccc5840d162c535fdba0ea736 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 13 Jun 2020 15:55:32 +0200 Subject: [PATCH 12/18] Added info to README --- README.md | 10 +++++----- docker-compose.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7262a756..4da0022e 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,17 @@ [1. Introducing Shuffle](https://medium.com/security-operation-capybara/introducing-shuffle-an-open-source-soar-platform-part-1-58a529de7d12) [2. Getting started with Shuffle](https://medium.com/security-operation-capybara/getting-started-with-shuffle-an-open-source-soar-platform-part-2-1d7c67a64244) +## Documentation +[Documentation](https://shuffler.io/docs) can be found on https://shuffler.io/docs/about or in your own instance. Currently lacking: +* API documentation +* Updates after migrating from SaaS to open source + ## Related repositories * Apps: https://github.com/frikky/shuffle-apps * Workflows: https://github.com/frikky/shuffle-workflows (empty) * Security OpenAPI apps: https://github.com/frikky/OpenAPI-security-definitions * Documentation: https://github.com/frikky/shuffle-docs -## Documentation -Documentation can be found on https://shuffler.io/docs/about or in your own instance. Currently lacking: -* API documentation -* Updates after migrating from SaaS to open source - ## Features * Simple workflow automation editor * Premade apps for a number of security tools diff --git a/docker-compose.yml b/docker-compose.yml index c00daf34..9fbfea69 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: frontend: - #build: ./frontend + build: ./frontend image: frikky/shuffle:frontend container_name: shuffle-frontend hostname: shuffle-frontend @@ -14,7 +14,7 @@ services: depends_on: - backend backend: - #build: ./backend + build: ./backend image: frikky/shuffle:backend container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} From 4f25d9c2e7dbb20fcacffd7e038dc8a130f78776 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 13 Jun 2020 16:52:38 +0200 Subject: [PATCH 13/18] Fixed code in docs --- README.md | 5 +++-- backend/go-app/codegen.go | 2 -- docker-compose.yml | 2 +- frontend/src/Docs.js | 19 +++++++++---------- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4da0022e..4f180ec2 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ * Cloud: Register at https://shuffler.io/register and get cooking (there are some differences!) ## Blogposts -[1. Introducing Shuffle](https://medium.com/security-operation-capybara/introducing-shuffle-an-open-source-soar-platform-part-1-58a529de7d12) -[2. Getting started with Shuffle](https://medium.com/security-operation-capybara/getting-started-with-shuffle-an-open-source-soar-platform-part-2-1d7c67a64244) +* [1. Introducing Shuffle](https://medium.com/security-operation-capybara/introducing-shuffle-an-open-source-soar-platform-part-1-58a529de7d12) +* [2. Getting started with Shuffle](https://medium.com/security-operation-capybara/getting-started-with-shuffle-an-open-source-soar-platform-part-2-1d7c67a64244) +* [3. Integrating Shuffle with Virustotal and TheHive](https://medium.com/@Frikkylikeme/integrating-shuffle-with-virustotal-and-thehive-open-source-soar-part-3-8e2e0d3396a9) ## Documentation [Documentation](https://shuffler.io/docs) can be found on https://shuffler.io/docs/about or in your own instance. Currently lacking: diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index 48711b1a..30567b16 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -379,8 +379,6 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet verifyAddin, ) - log.Printf("CODE: %s", data) - //log.Println(data) //log.Println(functionname) return functionname, data diff --git a/docker-compose.yml b/docker-compose.yml index 9fbfea69..77ee3a1a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: frontend: - build: ./frontend + #build: ./frontend image: frikky/shuffle:frontend container_name: shuffle-frontend hostname: shuffle-frontend diff --git a/frontend/src/Docs.js b/frontend/src/Docs.js index 9a0df2ea..69d0eb3f 100644 --- a/frontend/src/Docs.js +++ b/frontend/src/Docs.js @@ -58,8 +58,6 @@ const Docs = (props) => { // Continue this, and find the h2 with the data in it lol if (window.location.hash.length > 0) { - console.log("HELLO") - var parent = document.getElementById("markdown_wrapper") if (parent !== null) { var elements = parent.getElementsByTagName('h2') @@ -166,6 +164,7 @@ const Docs = (props) => { flex: "1", maxWidth: 750, overflow: "hidden", + paddingBottom: 200, } function OuterLink(props) { @@ -180,15 +179,20 @@ const Docs = (props) => { } function CodeHandler(props) { - return {props.value} + return ( +
+				
+					{props.value}
+				
+			
+ ) } function Heading(props) { const element = React.createElement(`h${props.level}`, {style: {marginTop: 25}}, props.children) - console.log(props) return ( - + {props.level !== 1 ? : null} {element} ) @@ -208,11 +212,6 @@ const Docs = (props) => {
    -
  • - -

    Home

    -
    -
  • {list.map(item => { const path = "/docs/"+item const newname = item.charAt(0).toUpperCase()+item.substring(1).split("_").join(" ") From 302044b4f3446bcf1da18397b8ce81a71de41f26 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 13 Jun 2020 19:03:19 +0200 Subject: [PATCH 14/18] Added color change to edges in graph --- frontend/src/AngularWorkflow.js | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/frontend/src/AngularWorkflow.js b/frontend/src/AngularWorkflow.js index ccb2d3ab..8bf68950 100644 --- a/frontend/src/AngularWorkflow.js +++ b/frontend/src/AngularWorkflow.js @@ -62,8 +62,6 @@ import cxtmenu from 'cytoscape-cxtmenu'; import { w3cwebsocket as W3CWebSocket } from "websocket"; import { useAlert } from "react-alert"; -const hoverColor = "#f85a3e" -const hoverOutColor = "#e8eaf6" const surfaceColor = "#27292D" const inputColor = "#383B40" @@ -1142,7 +1140,6 @@ const AngularWorkflow = (props) => { } const onNodeHover = (event) => { - //event.target.style("border-width", "5px") event.target.animate({ style: { "border-width": "5px", @@ -1156,24 +1153,22 @@ const AngularWorkflow = (props) => { } const onEdgeHoverOut = (event) => { - //event.target.removeStyle() + event.target.removeStyle() } // This is here to have a proper transition for lines const onEdgeHover = (event) => { - - //console.log(event.target.data()) - //const sourcecolor = cy.getElementById(event.target.data("source")).style("border-color") - //const targetcolor = cy.getElementById(event.target.data("target")).style("border-color") - //event.target.animate({ - // style: { - // "line-fill": "linear-gradient", - // 'target-arrow-color': targetcolor, - // "line-gradient-stop-colors": [sourcecolor, targetcolor], - // "line-gradient-stop-positions": [0, 1], - // }, - // duration: 0, - //}) + const sourcecolor = cy.getElementById(event.target.data("source")).style("border-color") + const targetcolor = cy.getElementById(event.target.data("target")).style("border-color") + event.target.animate({ + style: { + "line-fill": "linear-gradient", + 'target-arrow-color': targetcolor, + "line-gradient-stop-colors": [sourcecolor, targetcolor], + "line-gradient-stop-positions": [0, 1], + }, + duration: 0, + }) } From 0c2e4966b0720a56e0b652b897bbab1bdebbb70a Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 14 Jun 2020 08:21:38 +0200 Subject: [PATCH 15/18] Fixed spaces and headers in app generation actions --- backend/go-app/codegen.go | 283 ++++++++++++++++++++++---------- frontend/src/AngularWorkflow.js | 20 ++- frontend/src/AppCreator.js | 67 +++++--- 3 files changed, 258 insertions(+), 112 deletions(-) diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index 30567b16..3094092c 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -243,7 +243,7 @@ func buildStructure(swagger *openapi3.Swagger, curHash string) (string, error) { } // This function generates the python code that's being used. -func makePythoncode(swagger *openapi3.Swagger, name, url, method string, parameters, optionalQueries []string) (string, string) { +func makePythoncode(swagger *openapi3.Swagger, name, url, method string, parameters, optionalQueries, headers []string) (string, string) { method = strings.ToLower(method) queryString := "" queryData := "" @@ -349,10 +349,33 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet } } + preparedHeaders := "headers={}" + if len(headers) > 0 { + preparedHeaders = "headers={" + for count, header := range headers { + headerSplit := strings.Split(header, "=") + added := false + if len(headerSplit) == 2 { + if strings.Contains(preparedHeaders, headerSplit[0]) { + continue + } + + preparedHeaders += fmt.Sprintf(`"%s": "%s"`, headerSplit[0], headerSplit[1]) + added = true + } + + if count != len(headers)-1 && added { + preparedHeaders += "," + } + } + + preparedHeaders += "}" + } + // Extra param for url if it's changeable // Extra param for authentication scheme(s) data := fmt.Sprintf(` async def %s(self%s%s%s%s%s%s): - headers={} + %s url=f"%s%s" %s %s @@ -367,6 +390,7 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet queryString, bodyParameter, verifyParam, + preparedHeaders, urlInline, url, verifyWrapper, @@ -531,7 +555,11 @@ func generateYaml(swagger *openapi3.Swagger, newmd5 string) (*openapi3.Swagger, // Could just as well be go at this point lol pythonFunctions := []string{} for actualPath, path := range swagger.Paths { - // FIXME: Add everything from here: + actualPath = strings.Replace(actualPath, " ", "_", -1) + actualPath = strings.Replace(actualPath, ".", "", -1) + actualPath = strings.Replace(actualPath, ".", "", -1) + actualPath = strings.Replace(actualPath, "\\", "", -1) + // https://godoc.org/github.com/getkin/kin-openapi/openapi3#PathItem if path.Get != nil { action, curCode := handleGet(swagger, api, extraParameters, path, actualPath) @@ -808,13 +836,26 @@ func handleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [ } } + headersFound := []string{} if len(path.Connect.Parameters) > 0 { - for _, param := range path.Connect.Parameters { - if param.Value.Schema == nil || param.Value.In == "header" { + for counter, param := range path.Connect.Parameters { + if param.Value.Schema == nil { + continue + } else if param.Value.In == "header" { + headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) continue } + + parsedName := param.Value.Name + parsedName = strings.ReplaceAll(parsedName, " ", "_") + parsedName = strings.ReplaceAll(parsedName, ",", "_") + parsedName = strings.ReplaceAll(parsedName, ".", "_") + parsedName = strings.ReplaceAll(parsedName, "|", "_") + param.Value.Name = parsedName + path.Connect.Parameters[counter].Value.Name = parsedName + curParam := WorkflowAppActionParameter{ - Name: param.Value.Name, + Name: parsedName, Description: param.Value.Description, Multiline: false, Required: param.Value.Required, @@ -841,14 +882,8 @@ func handleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [ } } - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - if param.Value.In == "path" { - parameters = append(parameters, param.Value.Name) + parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { //log.Printf("QUERY!: %s", param.Value.Name) @@ -871,6 +906,12 @@ func handleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [ firstQuery = false } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + } } @@ -880,7 +921,7 @@ func handleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [ action.Parameters = append(action.Parameters, optionalParam) } - functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "connect", parameters, optionalQueries) + functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "connect", parameters, optionalQueries, headersFound) if len(functionname) > 0 { action.Name = functionname @@ -905,8 +946,6 @@ func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor action.Returns.Schema.Type = "string" baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) - //log.Println(path.Parameters) - // Parameters: []WorkflowAppActionParameter{}, // FIXME - add data for POST stuff firstQuery := true @@ -941,14 +980,26 @@ func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor } } + headersFound := []string{} if len(path.Get.Parameters) > 0 { - for _, param := range path.Get.Parameters { - if param.Value.Schema == nil || param.Value.In == "header" { + for counter, param := range path.Get.Parameters { + if param.Value.Schema == nil { + continue + } else if param.Value.In == "header" { + headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) continue } + parsedName := param.Value.Name + parsedName = strings.ReplaceAll(parsedName, " ", "_") + parsedName = strings.ReplaceAll(parsedName, ",", "_") + parsedName = strings.ReplaceAll(parsedName, ".", "_") + parsedName = strings.ReplaceAll(parsedName, "|", "_") + param.Value.Name = parsedName + path.Get.Parameters[counter].Value.Name = parsedName + curParam := WorkflowAppActionParameter{ - Name: param.Value.Name, + Name: parsedName, Description: param.Value.Description, Multiline: false, Required: param.Value.Required, @@ -975,14 +1026,8 @@ func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor } } - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - if param.Value.In == "path" { - parameters = append(parameters, param.Value.Name) + parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { //log.Printf("QUERY!: %s", param.Value.Name) @@ -1005,6 +1050,11 @@ func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor } firstQuery = false } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } } } @@ -1015,7 +1065,7 @@ func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor action.Parameters = append(action.Parameters, optionalParam) } - functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "get", parameters, optionalQueries) + functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "get", parameters, optionalQueries, headersFound) if len(functionname) > 0 { action.Name = functionname @@ -1073,13 +1123,27 @@ func handleHead(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo }) } } + + headersFound := []string{} if len(path.Head.Parameters) > 0 { - for _, param := range path.Head.Parameters { - if param.Value.Schema == nil || param.Value.In == "header" { + for counter, param := range path.Head.Parameters { + if param.Value.Schema == nil { + continue + } else if param.Value.In == "header" { + headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) continue } + + parsedName := param.Value.Name + parsedName = strings.ReplaceAll(parsedName, " ", "_") + parsedName = strings.ReplaceAll(parsedName, ",", "_") + parsedName = strings.ReplaceAll(parsedName, ".", "_") + parsedName = strings.ReplaceAll(parsedName, "|", "_") + param.Value.Name = parsedName + path.Head.Parameters[counter].Value.Name = parsedName + curParam := WorkflowAppActionParameter{ - Name: param.Value.Name, + Name: parsedName, Description: param.Value.Description, Multiline: false, Required: param.Value.Required, @@ -1106,14 +1170,8 @@ func handleHead(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo } } - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - if param.Value.In == "path" { - parameters = append(parameters, param.Value.Name) + parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { //log.Printf("QUERY!: %s", param.Value.Name) @@ -1136,6 +1194,12 @@ func handleHead(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo firstQuery = false } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + } } @@ -1145,7 +1209,7 @@ func handleHead(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo action.Parameters = append(action.Parameters, optionalParam) } - functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "head", parameters, optionalQueries) + functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "head", parameters, optionalQueries, headersFound) if len(functionname) > 0 { action.Name = functionname @@ -1203,13 +1267,27 @@ func handleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [] }) } } + + headersFound := []string{} if len(path.Delete.Parameters) > 0 { - for _, param := range path.Delete.Parameters { - if param.Value.Schema == nil || param.Value.In == "header" { + for counter, param := range path.Delete.Parameters { + if param.Value.Schema == nil { + continue + } else if param.Value.In == "header" { + headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) continue } + + parsedName := param.Value.Name + parsedName = strings.ReplaceAll(parsedName, " ", "_") + parsedName = strings.ReplaceAll(parsedName, ",", "_") + parsedName = strings.ReplaceAll(parsedName, ".", "_") + parsedName = strings.ReplaceAll(parsedName, "|", "_") + param.Value.Name = parsedName + path.Delete.Parameters[counter].Value.Name = parsedName + curParam := WorkflowAppActionParameter{ - Name: param.Value.Name, + Name: parsedName, Description: param.Value.Description, Multiline: false, Required: param.Value.Required, @@ -1236,14 +1314,8 @@ func handleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [] } } - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - if param.Value.In == "path" { - parameters = append(parameters, param.Value.Name) + parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { //log.Printf("QUERY!: %s", param.Value.Name) @@ -1266,6 +1338,12 @@ func handleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [] firstQuery = false } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + } } @@ -1275,7 +1353,7 @@ func handleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [] action.Parameters = append(action.Parameters, optionalParam) } - functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "delete", parameters, optionalQueries) + functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "delete", parameters, optionalQueries, headersFound) if len(functionname) > 0 { action.Name = functionname @@ -1298,10 +1376,6 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo Parameters: extraParameters, } - //if path.Post.RequestBody != nil { - // log.Printf("RequestBody: %#v", path.Post.RequestBody) - //} - action.Returns.Schema.Type = "string" baseUrl := fmt.Sprintf("%s%s", api.Link, actualPath) @@ -1337,14 +1411,26 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo } } + headersFound := []string{} if len(path.Post.Parameters) > 0 { - for _, param := range path.Post.Parameters { - if param.Value.Schema == nil || param.Value.In == "header" { + for counter, param := range path.Post.Parameters { + if param.Value.Schema == nil { + continue + } else if param.Value.In == "header" { + headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) continue } + parsedName := param.Value.Name + parsedName = strings.ReplaceAll(parsedName, " ", "_") + parsedName = strings.ReplaceAll(parsedName, ",", "_") + parsedName = strings.ReplaceAll(parsedName, ".", "_") + parsedName = strings.ReplaceAll(parsedName, "|", "_") + param.Value.Name = parsedName + path.Post.Parameters[counter].Value.Name = parsedName + curParam := WorkflowAppActionParameter{ - Name: param.Value.Name, + Name: parsedName, Description: param.Value.Description, Multiline: false, Required: param.Value.Required, @@ -1357,7 +1443,7 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo if param.Value.Example != nil { curParam.Example = param.Value.Example.(string) - if param.Value.Name == "body" { + if parsedName == "body" { curParam.Value = param.Value.Example.(string) } } @@ -1371,14 +1457,8 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo } } - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - if param.Value.In == "path" { - parameters = append(parameters, param.Value.Name) + parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { //log.Printf("QUERY!: %s", param.Value.Name) @@ -1401,6 +1481,11 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo firstQuery = false } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } } } @@ -1410,7 +1495,7 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo action.Parameters = append(action.Parameters, optionalParam) } - functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "post", parameters, optionalQueries) + functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "post", parameters, optionalQueries, headersFound) if len(functionname) > 0 { action.Name = functionname @@ -1468,13 +1553,27 @@ func handlePatch(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []W }) } } + + headersFound := []string{} if len(path.Patch.Parameters) > 0 { - for _, param := range path.Patch.Parameters { - if param.Value.Schema == nil || param.Value.In == "header" { + for counter, param := range path.Patch.Parameters { + if param.Value.Schema == nil { + continue + } else if param.Value.In == "header" { + headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) continue } + + parsedName := param.Value.Name + parsedName = strings.ReplaceAll(parsedName, " ", "_") + parsedName = strings.ReplaceAll(parsedName, ",", "_") + parsedName = strings.ReplaceAll(parsedName, ".", "_") + parsedName = strings.ReplaceAll(parsedName, "|", "_") + param.Value.Name = parsedName + path.Patch.Parameters[counter].Value.Name = parsedName + curParam := WorkflowAppActionParameter{ - Name: param.Value.Name, + Name: parsedName, Description: param.Value.Description, Multiline: false, Required: param.Value.Required, @@ -1501,14 +1600,8 @@ func handlePatch(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []W } } - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - if param.Value.In == "path" { - parameters = append(parameters, param.Value.Name) + parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) } else if param.Value.In == "query" { //log.Printf("QUERY!: %s", param.Value.Name) @@ -1531,6 +1624,11 @@ func handlePatch(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []W firstQuery = false } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } } } @@ -1540,7 +1638,7 @@ func handlePatch(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []W action.Parameters = append(action.Parameters, optionalParam) } - functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "patch", parameters, optionalQueries) + functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "delete", parameters, optionalQueries, headersFound) if len(functionname) > 0 { action.Name = functionname @@ -1599,13 +1697,26 @@ func handlePut(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor } } + headersFound := []string{} if len(path.Put.Parameters) > 0 { - for _, param := range path.Put.Parameters { - if param.Value.Schema == nil || param.Value.In == "header" { + for counter, param := range path.Put.Parameters { + if param.Value.Schema == nil { + continue + } else if param.Value.In == "header" { + headersFound = append(headersFound, fmt.Sprintf("%s=%s", param.Value.Name, param.Value.Example)) continue } + + parsedName := param.Value.Name + parsedName = strings.ReplaceAll(parsedName, " ", "_") + parsedName = strings.ReplaceAll(parsedName, ",", "_") + parsedName = strings.ReplaceAll(parsedName, ".", "_") + parsedName = strings.ReplaceAll(parsedName, "|", "_") + param.Value.Name = parsedName + path.Put.Parameters[counter].Value.Name = parsedName + curParam := WorkflowAppActionParameter{ - Name: param.Value.Name, + Name: parsedName, Description: param.Value.Description, Multiline: false, Required: param.Value.Required, @@ -1632,12 +1743,6 @@ func handlePut(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor } } - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - if param.Value.In == "path" { parameters = append(parameters, param.Value.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) @@ -1662,6 +1767,12 @@ func handlePut(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor firstQuery = false } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + } } @@ -1671,7 +1782,7 @@ func handlePut(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor action.Parameters = append(action.Parameters, optionalParam) } - functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "put", parameters, optionalQueries) + functionname, curCode := makePythoncode(swagger, functionName, baseUrl, "delete", parameters, optionalQueries, headersFound) if len(functionname) > 0 { action.Name = functionname diff --git a/frontend/src/AngularWorkflow.js b/frontend/src/AngularWorkflow.js index 8bf68950..854249f7 100644 --- a/frontend/src/AngularWorkflow.js +++ b/frontend/src/AngularWorkflow.js @@ -2411,7 +2411,7 @@ const AngularWorkflow = (props) => {
    - {data.name}: + {data.name}
    { @@ -2497,6 +2497,13 @@ const AngularWorkflow = (props) => { //setStartNode(selectedAction.id) } + function sortByKey(array, key) { + return array.sort(function(a, b) { + var x = a[key]; var y = b[key] + return ((x < y) ? -1 : ((x > y) ? 1 : 0)) + }) + } + const appApiView = Object.getOwnPropertyNames(selectedAction).length > 0 && Object.getOwnPropertyNames(selectedApp).length > 0 ?
    @@ -2593,7 +2600,7 @@ const AngularWorkflow = (props) => { } }} > - {selectedApp.actions.map(data => { + {sortByKey(selectedApp.actions, "label").map(data => { var newActionname = data.name if (data.label !== undefined && data.label !== null && data.label.length > 0) { newActionname = data.label @@ -2605,13 +2612,17 @@ const AngularWorkflow = (props) => { newActionname = newActionname.replace("_", " ") newActionname = newActionname.charAt(0).toUpperCase()+newActionname.substring(1) return ( - + {newActionname} ) })} + {selectedAction.description !== undefined && selectedAction.description.length > 0 ? +
    + {selectedAction.description} +
    : null}
    @@ -4536,6 +4547,9 @@ const AngularWorkflow = (props) => { boxSelectionEnabled={true} autounselectify={false} cy={(incy) => { + // FIXME: There's something specific loading when + // you do the first hover of a node. Why is this different? + console.log("CY: ", incy) setCy(incy) }} /> diff --git a/frontend/src/AppCreator.js b/frontend/src/AppCreator.js index 690b73ea..91266d7f 100644 --- a/frontend/src/AppCreator.js +++ b/frontend/src/AppCreator.js @@ -614,12 +614,25 @@ const AppCreator = (props) => { const headersSplit = item.headers.split("\n") for (var key in headersSplit) { const header = headersSplit[key] + console.log("HEADER: ", header) var key = "" var value = "" - if (header.length > 0 && header.includes("=")) { + if (header.length > 0 && header.includes("= ")) { + const headersplit = header.split("= ") + key = headersplit[0] + value = headersplit[1] + } else if (header.length > 0 && header.includes("=")) { const headersplit = header.split("=") key = headersplit[0] value = headersplit[1] + } else if (header.length > 0 && header.includes(": ")) { + const headersplit = header.split(": ") + key = headersplit[0] + value = headersplit[1] + } else if (header.length > 0 && header.includes(":")) { + const headersplit = header.split(":") + key = headersplit[0] + value = headersplit[1] } else { continue } @@ -1273,6 +1286,10 @@ const AppCreator = (props) => { continue } + if (key === "Authorization" && authenticationOption === "Bearer auth") { + continue + } + headers += key+"="+value+"\n" } @@ -1284,33 +1301,37 @@ const AppCreator = (props) => { } // Parse URL - parsedurl = request.url + if (request.url !== undefined) { + parsedurl = request.url + } } - if (parsedurl.includes("<") && parsedurl.includes(">")) { - parsedurl = parsedurl.split("<").join("{") - parsedurl = parsedurl.split(">").join("}") - } - - if (parsedurl.startsWith("http") || parsedurl.startsWith("ftp")) { - if (parsedurl !== undefined && parsedurl.includes(parameterName)) { - // Remove <> etc. - // - - console.log("IT HAS THE PARAM NAME!") - const newurl = new URL(encodeURI(parsedurl)) - newurl.searchParams.delete(parameterName) - parsedurl = decodeURI(newurl.href) + if (parsedurl !== undefined) { + if (parsedurl.includes("<") && parsedurl.includes(">")) { + parsedurl = parsedurl.split("<").join("{") + parsedurl = parsedurl.split(">").join("}") } - // Remove the base URL itself - if (parsedurl !== undefined && baseUrl !== undefined && baseUrl.length > 0 && parsedurl.includes(baseUrl)) { - parsedurl = parsedurl.replace(baseUrl, "") - } + if (parsedurl.startsWith("http") || parsedurl.startsWith("ftp")) { + if (parsedurl !== undefined && parsedurl.includes(parameterName)) { + // Remove <> etc. + // + + console.log("IT HAS THE PARAM NAME!") + const newurl = new URL(encodeURI(parsedurl)) + newurl.searchParams.delete(parameterName) + parsedurl = decodeURI(newurl.href) + } - // Check URL query && headers - setActionField("url", parsedurl) - setUrlPath(parsedurl) + // Remove the base URL itself + if (parsedurl !== undefined && baseUrl !== undefined && baseUrl.length > 0 && parsedurl.includes(baseUrl)) { + parsedurl = parsedurl.replace(baseUrl, "") + } + + // Check URL query && headers + setActionField("url", parsedurl) + setUrlPath(parsedurl) + } } //console.log("URL: ", request.url) From 76ce2bddeefb71d2e60bc5da25e844cdec583e08 Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 14 Jun 2020 18:36:45 +0200 Subject: [PATCH 16/18] Updated database location because of OS X issues --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index a07a0644..33d6f11c 100644 --- a/.env +++ b/.env @@ -13,4 +13,4 @@ BACKEND_PORT=5001 FRONTEND_PORT=3001 FRONTEND_PORT_HTTPS=3443 OUTER_HOSTNAME=shuffle-backend -DB_LOCATION=/etc/shuffle +DB_LOCATION=./etc/shuffle From d2d89d0bb9bee845490771257090f92ca56fd5bb Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 14 Jun 2020 18:40:55 +0200 Subject: [PATCH 17/18] Updated code generator for Shuffle --- README.md | 4 ++-- backend/app_sdk/app_base.py | 9 ++------- backend/go-app/codegen.go | 1 + frontend/src/AngularWorkflow.js | 3 ++- frontend/src/AppCreator.js | 2 +- functions/onprem/orborus/orborus.go | 6 +++--- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4f180ec2..697aa3f1 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ ## Related repositories * Apps: https://github.com/frikky/shuffle-apps -* Workflows: https://github.com/frikky/shuffle-workflows (empty) -* Security OpenAPI apps: https://github.com/frikky/OpenAPI-security-definitions +* Workflows: https://github.com/frikky/shuffle-workflows +* Security OpenAPI apps: https://github.com/frikky/security-openapis * Documentation: https://github.com/frikky/shuffle-docs ## Features diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 33fa9314..8bf5736b 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -73,12 +73,9 @@ class AppBase: except requests.exceptions.ConnectionError as e: print("Connectionerror: %s" % e) return - #self.logger.info("AFTER initial stream result") - self.logger.info("THIS IS THE NEW UPDATE") # Verify whether there are any parameters with ACTION_RESULT required # If found, we get the full results list from backend - fullexecution = {} try: tmpdata = { @@ -195,10 +192,9 @@ class AppBase: # Regex to find all the things if parameter["variant"] == "STATIC_VALUE": data = parameter["value"] - self.logger.debug(f"\n\nHandle static data with JSON: {data}\n\n") - actualitem = re.findall(match, data, re.MULTILINE) - self.logger.info("STATIC PARSED: %s" % actualitem) + #self.logger.debug(f"\n\nHandle static data with JSON: {data}\n\n") + #self.logger.info("STATIC PARSED: %s" % actualitem) if len(actualitem) > 0: for replace in actualitem: try: @@ -487,7 +483,6 @@ class AppBase: # With this parameter ready, add it to... a greater list of parameters. Rofl multi_parameters[parameter["name"]] = resultarray else: - print("Hello, in here?: %s" % value) params[parameter["name"]] = value multi_parameters[parameter["name"]] = value diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index 3094092c..7a728aa9 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -243,6 +243,7 @@ func buildStructure(swagger *openapi3.Swagger, curHash string) (string, error) { } // This function generates the python code that's being used. +// This is really meta when you program it. Handling parameters is hard here. func makePythoncode(swagger *openapi3.Swagger, name, url, method string, parameters, optionalQueries, headers []string) (string, string) { method = strings.ToLower(method) queryString := "" diff --git a/frontend/src/AngularWorkflow.js b/frontend/src/AngularWorkflow.js index 854249f7..c74ddf19 100644 --- a/frontend/src/AngularWorkflow.js +++ b/frontend/src/AngularWorkflow.js @@ -2281,6 +2281,7 @@ const AngularWorkflow = (props) => { rows="5" color="primary" defaultValue={data.value} + type={placeholder.includes("***") ? "password" : "text"} placeholder={placeholder} onChange={(event) => { changeActionParameter(event, count) @@ -2585,7 +2586,7 @@ const AngularWorkflow = (props) => { : null*/}
    -
    +
    Actions