Merge branch 'frikky:launch' into launch

This commit is contained in:
Jay Gohil
2022-03-03 16:09:25 +05:30
committed by GitHub
48 changed files with 5726 additions and 2447 deletions
+5 -4
View File
@@ -2,6 +2,7 @@
ORG_ID=Shuffle
ENVIRONMENT_NAME=Shuffle
# Remote github config for first load
SHUFFLE_DOWNLOAD_WORKFLOW_LOCATION=
SHUFFLE_DOWNLOAD_WORKFLOW_USERNAME=
@@ -60,10 +61,10 @@ SHUFFLE_ELASTIC=true
# DATABASE CONFIGURATIONS
DATASTORE_EMULATOR_HOST=shuffle-database:8000
#SHUFFLE_OPENSEARCH_URL=https://shuffle-opensearch:9200
SHUFFLE_OPENSEARCH_URL=http://shuffle-opensearch:9200
SHUFFLE_OPENSEARCH_USERNAME=
SHUFFLE_OPENSEARCH_PASSWORD=
#SHUFFLE_OPENSEARCH_URL=http://shuffle-opensearch:9200
SHUFFLE_OPENSEARCH_URL=https://shuffle-opensearch:9200
SHUFFLE_OPENSEARCH_USERNAME=admin
SHUFFLE_OPENSEARCH_PASSWORD=admin
SHUFFLE_OPENSEARCH_CERTIFICATE_FILE=
SHUFFLE_OPENSEARCH_APIKEY=
SHUFFLE_OPENSEARCH_CLOUDID=
+9 -8
View File
@@ -10,18 +10,19 @@ The Docker setup is done with docker-compose
1. Make sure you have [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) installed.
2. Download Shuffle
```
```bash
git clone https://github.com/frikky/Shuffle
cd Shuffle
```
3. Fix prerequisites for the Opensearch database (Elasticsearch):
```
sudo chown -R 1000:1000 shuffle-database # Required for Opensearch
```bash
mkdir shuffle-database
sudo chown -R 1000:1000 shuffle-database
```
4. Run docker-compose.
```
```bash
docker-compose up -d
```
@@ -38,20 +39,20 @@ This step is for setting up with Docker on windows from scratch.
4. Open the .env file and change the line with "OUTER_HOSTNAME" to contain your IP:
```
```bash
OUTER_HOSTNAME=YOUR.IP.HERE
```
6. Run docker-compose
```
docker compose up -d
```bash
docker-compose up -d
```
### Configurations (proxies, default users etc.)
https://shuffler.io/docs/configuration
### After installation
1. After installation, go to http://localhost:3001/adminsetup (or your servername - https is on port 3443)
1. After installation, go to http://localhost:3001 (or your servername - https is on port 3443)
2. Now set up your admin account (username & password). Shuffle doesn't have a default username and password.
3. Sign in with the same Username & Password! Go to /apps and see if you have any apps yet. If not - you may need to [configure proxies](https://shuffler.io/docs/configuration#production_readiness)
4. Check out https://shuffler.io/docs/configuration as it has a lot of useful information to get started
+1 -1
View File
@@ -6,7 +6,7 @@ Shuffle Automation
</h1><h4 align="center">
![Shuffle](https://shuffler.io) is an automation platform for and by the community, focusing on accessibility for anyone to automate. Security operations is complex, but it doesn't have to be.
[Shuffle](https://shuffler.io) is an automation platform for and by the community, focusing on accessibility for anyone to automate. Security operations is complex, but it doesn't have to be.
[_Key Features_](https://shuffler.io/docs/features) —
[_Community & Support_](https://discord.gg/B2CBzUm) —
+328 -61
View File
@@ -3,6 +3,7 @@ import copy
import sys
import re
import time
import base64
import json
import liquid
import logging
@@ -14,17 +15,122 @@ import requests
import http.client
import urllib.parse
import jinja2
from io import StringIO as StringBuffer
from io import BytesIO
from liquid import Liquid
from liquid import Liquid, defaults
runtime = os.getenv("SHUFFLE_SWARM_CONFIG", "")
###
###
###
#### Filters for liquidpy
###
###
###
defaults.MODE = 'wild'
defaults.FROM_FILE = False
from liquid.filters.manager import FilterManager
from liquid.filters.standard import standard_filter_manager
shuffle_filters = FilterManager()
for key, value in standard_filter_manager.filters.items():
shuffle_filters.filters[key] = value
#@shuffle_filters.register
#def plus(a, b):
# try:
# a = int(a)
# except:
# a = 0
#
# try:
# b = int(b)
# except:
# b = 0
#
# return standard_filter_manager.filters["plus"](a, b)
#
#@shuffle_filters.register
#def minus(a, b):
# a = int(a)
# b = int(b)
# return standard_filter_manager.filters["minus"](a, b)
#
#@shuffle_filters.register
#def multiply(a, b):
# a = int(a)
# b = int(b)
# return standard_filter_manager.filters["multiply"](a, b)
#
#@shuffle_filters.register
#def divide(a, b):
# a = int(a)
# b = int(b)
# return standard_filter_manager.filters["divide"](a, b)
@shuffle_filters.register
def md5(a):
a = str(a)
return hashlib.md5(a.encode('utf-8')).hexdigest()
@shuffle_filters.register
def sha256(a):
a = str(a)
return hashlib.sha256(str(a).encode("utf-8")).hexdigest()
@shuffle_filters.register
def md5_base64(a):
a = str(a)
foundhash = hashlib.md5(a.encode('utf-8')).hexdigest()
return base64.b64encode(foundhash.encode('utf-8'))
@shuffle_filters.register
def base64_encode(a):
a = str(a)
try:
return base64.b64encode(a.encode('utf-8')).decode()
except:
return base64.b64encode(a).decode()
@shuffle_filters.register
def base64_decode(a):
a = str(a)
try:
return base64.b64decode(a).decode()
except:
return base64.b64decode(a)
#print(standard_filter_manager.filters)
#print(shuffle_filters.filters)
#print(Liquid("{{ '10' | plus: 1}}", filters=shuffle_filters.filters).render())
#print(Liquid("{{ '10' | minus: 1}}", filters=shuffle_filters.filters).render())
#print(Liquid("{{ asd | size }}", filters=shuffle_filters.filters).render())
#print(Liquid("{{ 'asd' | md5 }}", filters=shuffle_filters.filters).render())
#print(Liquid("{{ 'asd' | sha256 }}", filters=shuffle_filters.filters).render())
#print(Liquid("{{ 'asd' | md5_base64 | base64_decode }}", filters=shuffle_filters.filters).render())
###
###
###
###
###
###
###
class AppBase:
__version__ = None
app_name = None
def __init__(self, redis=None, logger=None, console_logger=None):#, docker_client=None):
self.logger = logger if logger is not None else logging.getLogger("AppBaseLogger")
self.log_capture_string = StringBuffer()
ch = logging.StreamHandler(self.log_capture_string)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
self.redis=redis
self.console_logger = logger if logger is not None else logging.getLogger("AppBaseLogger")
@@ -139,15 +245,15 @@ class AppBase:
try:
new_input = input_data.split()
except Exception as e:
self.logger.info(f"[ERROR] Failed to run magic parser (1): {e}")
self.logger.info(f"[ERROR] Failed to run magic parser during split (1): {e}")
return input_data
# Won't ever touch this one?
if isinstance(input_data, list) or isinstance(input_data, object):
if isinstance(new_input, list) or isinstance(new_input, object):
try:
return json.dumps(new_input)
except Exception as e:
self.logger.info(f"[ERROR] Failed to run magic parser: {e}")
self.logger.info(f"[ERROR] Failed to run magic parser (3): {e}")
return new_input
@@ -165,12 +271,13 @@ class AppBase:
else:
self.logger.warning(f"[ERROR] Magic output not defined.")
except Exception as e:
self.logger.warning(f"[ERROR] Failed to run magic autoparser: {e}")
self.logger.warning(f"[ERROR] Failed to run magic autoparser (send result): {e}")
pass
# Try it with some magic
self.logger.info(f"""[DEBUG] Inside Send result with status {action_result["status"]}""")
#if isinstance(action_result,
# FIXME: Add cleanup of parameters to not send to frontend here
params = {}
@@ -188,11 +295,64 @@ class AppBase:
self.logger.info(f"[DEBUG] Before last stream result")
url = "%s%s" % (self.base_url, stream_path)
self.logger.info("[INFO] URL FOR RESULT (URL): %s" % url)
try:
ret = requests.post(url, headers=headers, json=action_result)
#self.logger.info(f"[DEBUG] Result: {ret.status_code}")
#if ret.status_code != 200:
# self.logger.info(f"[DEBUG] Shuffle Response: {ret.text}")
log_contents = self.log_capture_string.getvalue()
#print("RESULTS: %s" % log_contents)
self.logger.info("[WARNING] Got logs of length {len(log_contents)}")
if len(action_result["action"]["parameters"]) == 0:
action_result["action"]["parameters"] = []
param_found = False
for param in action_result["action"]["parameters"]:
if param["name"] == "shuffle_action_logs":
param_found = True
break
if not param_found:
action_result["action"]["parameters"].append({
"name": "shuffle_action_logs",
"value": log_contents,
})
except Exception as e:
print(f"Failed adding parameter: {e}")
# FIXME: Adding retries here.
try:
finished = False
for i in range (0, 5):
try:
ret = requests.post(url, headers=headers, json=action_result, timeout=10)
self.logger.info(f"[DEBUG] Result: {ret.status_code} (break on 200)")
if ret.status_code == 200 or ret.status_code == 201:
finished = True
break
else:
self.logger.info(f"[DEBUG] RESP: {ret.text}")
except (requests.exceptions.RequestException, TimeoutError) as e:
time.sleep(5)
continue
except requests.exceptions.ConnectionError as e:
time.sleep(5)
continue
except http.client.RemoteDisconnected as e:
time.sleep(5)
continue
except urllib3.exceptions.ProtocolError as e:
time.sleep(5)
continue
time.sleep(5)
if not finished:
# Not sure why this would work tho :)
action_result["status"] = "FAILURE"
action_result["result"] = f"POST error: {e}"
self.logger.info(f"[DEBUG] Before typeerror stream result: {e}")
ret = requests.post("%s%s" % (self.base_url, stream_path), headers=headers, json=action_result)
self.logger.info(f"""[DEBUG] Successful request result request: Status= {ret.status_code} & Response= {ret.text}. Action status: {action_result["status"]}""")
except requests.exceptions.ConnectionError as e:
@@ -821,7 +981,7 @@ class AppBase:
returns = []
for item in value:
self.logger.info("VALUE: %s" % item)
if len(item) != 36:
if len(item) != 36 and not item.startswith("file_"):
self.logger.info("Bad length for file value %s" % item)
continue
#return {
@@ -924,6 +1084,7 @@ class AppBase:
#return value.json()
return {"success": False}
# Wrapper for set_files
def set_file(self, infiles):
return self.set_files(infiles)
@@ -1010,19 +1171,23 @@ class AppBase:
}
# Simple validation of parameters in general
replace_params = False
try:
tmp_parameters = action["parameters"]
for param in tmp_parameters:
if param["value"] == "SHUFFLE_AUTO_REMOVED":
replace_params = True
except KeyError:
action["parameters"] = []
except TypeError:
pass
self.action = copy.deepcopy(action)
self.logger.info("[DEBUG] Sending starting action result (EXECUTING)")
self.logger.info(f"[DEBUG] Sending starting action result (EXECUTING). Param replace: {replace_params}")
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer %s" % self.authorization
"Authorization": f"Bearer {self.authorization}"
}
if len(self.action) == 0:
@@ -1118,6 +1283,32 @@ class AppBase:
self.full_execution = fullexecution
try:
if replace_params == True:
for inner_action in self.full_execution["workflow"]["actions"]:
self.logger.info("[DEBUG] ID: %s vs %s" % (inner_action["id"], self.action["id"]))
# In case of some kind of magic, we're just doing params
if inner_action["id"] == self.action["id"]:
self.logger.info("FOUND!")
if isinstance(self.action, str):
self.logger.info("Params is in string object for self.action?")
else:
self.action["parameters"] = inner_action["parameters"]
self.action_result["action"]["parameters"] = inner_action["parameters"]
if isinstance(self.original_action, str):
self.logger.info("Params for original actions is in string object?")
else:
self.original_action["parameters"] = inner_action["parameters"]
break
except Exception as e:
self.logger.info(f"[WARNING] Failed in replace params action parsing: {e}")
self.logger.info("[DEBUG] AFTER FULLEXEC stream result (init)")
# Gets the value at the parenthesis level you want
@@ -1489,6 +1680,11 @@ class AppBase:
newvalue = []
firstitem = actualitem[0][0]
seconditem = actualitem[0][1]
if isinstance(firstitem, int):
firstitem = str(firstitem)
if isinstance(seconditem, int):
seconditem = str(seconditem)
print("[DEBUG] ACTUAL PARSED: %s" % actualitem)
# Means it's a single item -> continue
@@ -1509,17 +1705,23 @@ class AppBase:
newvalue, is_loop = (tmpitem, parsersplit[outercnt+1:])
else:
print("[INFO] In ELSE - handling %s and %s" % (firstitem, seconditem))
if firstitem.lower() == "max" or firstitem.lower() == "last" or firstitem.lower() == "end":
firstitem = len(basejson)-1
elif firstitem.lower() == "min" or firstitem.lower() == "first":
firstitem = 0
if isinstance(firstitem, str):
if firstitem.lower() == "max" or firstitem.lower() == "last" or firstitem.lower() == "end":
firstitem = len(basejson)-1
elif firstitem.lower() == "min" or firstitem.lower() == "first":
firstitem = 0
else:
firstitem = int(firstitem)
else:
firstitem = int(firstitem)
if seconditem.lower() == "max" or seconditem.lower() == "last" or firstitem.lower() == "end":
seconditem = len(basejson)-1
elif seconditem.lower() == "min" or seconditem.lower() == "first":
seconditem = 0
if isinstance(seconditem, str):
if seconditem.lower() == "max" or seconditem.lower() == "last" or firstitem.lower() == "end":
seconditem = len(basejson)-1
elif seconditem.lower() == "min" or seconditem.lower() == "first":
seconditem = 0
else:
seconditem = int(seconditem)
else:
seconditem = int(seconditem)
@@ -1704,7 +1906,7 @@ class AppBase:
basejson = json.loads(baseresult)
except json.decoder.JSONDecodeError as e:
try:
baseresult = baseresult.replace("\'", "\"")
#baseresult = baseresult.replace("\'", "\"")
basejson = json.loads(baseresult)
except json.decoder.JSONDecodeError as e:
print("Parser issue with JSON: %s" % e)
@@ -1768,9 +1970,10 @@ class AppBase:
# return template
#self.logger.info(globals())
self.logger.info("[DEBUG] Running liquid with data of length %d" % len(template))
if len(template) > 100:
self.logger.info("[DEBUG] Running liquid with data of length %d" % len(template))
#self.logger.info(f"[DEBUG] Data: {template}")
run = Liquid(template, mode="wild", from_file=False)
run = Liquid(template, mode="wild", from_file=False, filters=shuffle_filters.filters)
# Can't handle self yet (?)
ret = run.render(**globals())
@@ -1822,8 +2025,8 @@ class AppBase:
self.action_result["status"] = "FAILURE"
data = {
"success": False,
"input": template,
"reason": f"Failed to parse LiquidPy: {error_msg}",
"input": template,
}
try:
self.action_result["result"] = json.dumps(data)
@@ -1945,7 +2148,11 @@ class AppBase:
except:
self.logger.info("Error in initial replacement of escaped dollar!")
#self.logger.info("POST input value: %s" % parameter["value"])
# Basic fix in case variant isn't set
try:
self.logger.info("[DEBUG] Parameter variant: %s" % parameter["variant"])
except:
parameter["variant"] = "STATIC_VALUE"
# Regex to find all the things
if parameter["variant"] == "STATIC_VALUE":
@@ -2176,7 +2383,7 @@ class AppBase:
return True
else:
print("[DEBUG] Condition: can't handle %s yet. Setting to true" % check)
return False
def check_branch_conditions(action, fullexecution, self):
@@ -2187,21 +2394,48 @@ class AppBase:
except KeyError:
return True, ""
available_checks = [
"=",
"equals",
"!=",
"does not equal",
">",
"larger than",
"<",
"less than",
">=",
"<=",
"startswith",
"endswith",
"contains",
"contains_any_of",
"re",
"matches regex",
]
relevantbranches = []
correct_branches = 0
matching_branches = 0
for branch in fullexecution["workflow"]["branches"]:
if branch["destination_id"] != action["id"]:
continue
matching_branches += 1
# Remove anything without a condition
try:
if (branch["conditions"]) == 0 or branch["conditions"] == None:
correct_branches += 1
continue
except KeyError:
correct_branches += 1
continue
self.logger.info("[DEBUG] Relevant conditions: %s" % branch["conditions"])
successful_conditions = []
failed_conditions = []
successful_conditions = 0
total_conditions = len(branch["conditions"])
for condition in branch["conditions"]:
self.logger.info("[DEBUG] Getting condition value of %s" % condition)
@@ -2209,6 +2443,7 @@ class AppBase:
sourcevalue = condition["source"]["value"]
check, sourcevalue, is_loop = parse_params(action, fullexecution, condition["source"], self)
if check:
continue
return False, {"success": False, "reason": "Failed condition (1): %s %s %s because %s" % (sourcevalue, condition["condition"]["value"], destinationvalue, check)}
#sourcevalue = sourcevalue.encode("utf-8")
@@ -2217,28 +2452,11 @@ class AppBase:
check, destinationvalue, is_loop = parse_params(action, fullexecution, condition["destination"], self)
if check:
continue
return False, {"success": False, "reason": "Failed condition (2): %s %s %s because %s" % (sourcevalue, condition["condition"]["value"], destinationvalue, check)}
#destinationvalue = destinationvalue.encode("utf-8")
destinationvalue = parse_wrapper_start(destinationvalue, self)
available_checks = [
"=",
"equals",
"!=",
"does not equal",
">",
"larger than",
"<",
"less than",
">=",
"<=",
"startswith",
"endswith",
"contains",
"contains_any_of",
"re",
"matches regex",
]
if not condition["condition"]["value"] in available_checks:
self.logger.warning("Skipping %s %s %s because %s is invalid." % (sourcevalue, condition["condition"]["value"], destinationvalue, condition["condition"]["value"]))
@@ -2255,22 +2473,58 @@ class AppBase:
except KeyError:
pass
if not validation:
self.logger.info("Failed condition check for %s %s %s." % (sourcevalue, condition["condition"]["value"], destinationvalue))
return False, {"success": False, "reason": "Failed condition (3): %s %s %s" % (sourcevalue, condition["condition"]["value"], destinationvalue)}
if validation == True:
successful_conditions += 1
#if not validation:
# self.logger.info("Failed condition check for %s %s %s." % (sourcevalue, condition["condition"]["value"], destinationvalue))
# return False, {"success": False, "reason": "Failed condition (3): %s %s %s" % (sourcevalue, condition["condition"]["value"], destinationvalue)}
# Make a general parser here, at least to get param["name"] = param["value"] in maparameter[string]string
#for condition in branch.conditons:
self.logger.info("CONDITIONS VS SUCCESS: %d vs %d" % (total_conditions, successful_conditions))
if total_conditions == successful_conditions:
correct_branches += 1
if matching_branches == 0:
return True, ""
if matching_branches > 0 and correct_branches > 0:
return True, ""
self.logger.info("[DEBUG] Correct branches vs matching branches: %d vs %d" % (correct_branches, matching_branches))
return False, {"success": False, "reason": "Minimum of one branch's conditions must be correct to continue. Total: %d of %d" % (correct_branches, matching_branches)}
#Correct branches vs matching branches: 1 vs 1
#if
return True, ""
#
#
#
#
# CONT
# CONT
# CONT
# CONT
# CONT
# CONT
# CONT
# CONT
# CONT
# CONT
# CONT
# CONT
# CONT
#
#
#
#
# THE START IS ACTUALLY RIGHT HERE :O
# Checks whether conditions are met, otherwise set
branchcheck, tmpresult = check_branch_conditions(action, fullexecution, self)
if isinstance(tmpresult, object) or isinstance(tmpresult, list):
if isinstance(tmpresult, object) or isinstance(tmpresult, list) or isinstance(tmpresult, dict):
self.logger.info("[DEBUG] Fixing branch return as object -> string")
try:
#tmpresult = tmpresult.replace("'", "\"")
@@ -2278,19 +2532,14 @@ class AppBase:
except json.decoder.JSONDecodeError as e:
self.logger.info(f"[WARNING] Failed condition parsing {tmpresult} to string")
# IF branches fail: Exit!
if not branchcheck:
self.logger.info("Failed one or more branch conditions.")
self.action_result["result"] = tmpresult
self.action_result["status"] = "SKIPPED"
try:
ret = requests.post("%s%s" % (self.base_url, stream_path), headers=headers, json=self.action_result)
self.logger.info("Result: %d" % ret.status_code)
if ret.status_code != 200:
self.logger.info(ret.text)
except requests.exceptions.ConnectionError as e:
self.logger.exception(e)
self.action_result["completed_at"] = int(time.time())
self.logger.info("\n\n[DEBUG] RETURNING BECAUSE A BRANCH FAILED: %s\n\n" % tmpresult)
self.send_result(self.action_result, headers, stream_path)
return
# Replace name cus there might be issues
@@ -2612,7 +2861,13 @@ class AppBase:
for i in range(0, curminlength):
tmpitem = json.loads(json.dumps(parameter["value"]))
for key, value in replacements.items():
replacement = json.dumps(json.loads(value)[i])
replacement = value
try:
replacement = json.dumps(json.loads(value)[i])
except IndexError as e:
self.logger.info(f"[ERROR] Failed handling value parsing with index: {e}")
pass
if replacement.startswith("\"") and replacement.endswith("\""):
replacement = replacement[1:len(replacement)-1]
#except json.decoder.JSONDecodeError as e:
@@ -2667,13 +2922,20 @@ class AppBase:
multi_parameters[parameter["name"]] = resultarray
else:
# Parses things like int(value)
self.logger.info("[DEBUG] Normal parsing (not looping)")#with data %s" % value)
#self.logger.info("[DEBUG] Normal parsing (not looping)")#with data %s" % value)
# This part has fucked over so many random JSON usages because of weird paranthesis parsing
value = parse_wrapper_start(value, self)
#self.logger.info("[DEBUG] Post return: %s" % value)
#self.logger.info("POST data value: %s" % value)
try:
if str(value).startswith("b'") and str(value).endswith("'"):
value = value[2:-1]
except Exception as e:
print(f"Value rawbytes Exception: {e}")
params[parameter["name"]] = value
multi_parameters[parameter["name"]] = value
@@ -2803,7 +3065,7 @@ class AppBase:
except Exception as e:
self.logger.warning("[ERROR] Failed to parse coroutine value for old app: {e}")
self.logger.info("\n[INFO] Returned from execution with types %s" % type(newres))
self.logger.info("\n[INFO] Returned from execution with type(s) %s" % type(newres))
#self.logger.info("\n[INFO] Returned from execution with %s of types %s" % (newres, type(newres)))#, newres)
if isinstance(newres, tuple):
self.logger.info(f"[INFO] Handling return as tuple: {newres}")
@@ -2946,6 +3208,11 @@ class AppBase:
# Send the result :)
self.send_result(self.action_result, headers, stream_path)
try:
self.log_capture_string.close()
except:
pass
return
@classmethod
+13 -13
View File
@@ -3,7 +3,7 @@
### DEFAULT
NAME=shuffle-app_sdk
VERSION=0.9.50
VERSION=0.9.61
docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force
docker build . -f Dockerfile -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION -t ghcr.io/frikky/$NAME:nightly
@@ -19,17 +19,17 @@ docker push ghcr.io/frikky/$NAME:nightly
docker push ghcr.io/frikky/$NAME:latest
#### KALI ###
NAME=shuffle-app_sdk_kali
docker build . -f Dockerfile_kali -t frikky/shuffle:app_sdk_kali -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
docker push frikky/shuffle:app_sdk_kali
docker push ghcr.io/frikky/$NAME:$VERSION
docker push ghcr.io/frikky/$NAME:nightly
#NAME=shuffle-app_sdk_kali
#docker build . -f Dockerfile_kali -t frikky/shuffle:app_sdk_kali -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
#
#docker push frikky/shuffle:app_sdk_kali
#docker push ghcr.io/frikky/$NAME:$VERSION
#docker push ghcr.io/frikky/$NAME:nightly
### BLACKARCH ###
NAME=shuffle-app_sdk_blackarch
docker build . -f Dockerfile_blackarch -t frikky/shuffle:app_sdk_blackarch -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
docker push frikky/shuffle:app_sdk_blackarch
docker push ghcr.io/frikky/$NAME:$VERSION
docker push ghcr.io/frikky/$NAME:nightly
#NAME=shuffle-app_sdk_blackarch
#docker build . -f Dockerfile_blackarch -t frikky/shuffle:app_sdk_blackarch -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
#
#docker push frikky/shuffle:app_sdk_blackarch
#docker push ghcr.io/frikky/$NAME:$VERSION
#docker push ghcr.io/frikky/$NAME:nightly
+4 -3
View File
@@ -14,6 +14,7 @@ import (
"encoding/json"
"errors"
"fmt"
//"github.com/docker/docker"
"github.com/docker/docker/api/types"
//"github.com/docker/docker/api/types/container"
@@ -417,7 +418,7 @@ func stopWebhook(image string, identifier string) error {
// Starts a new webhook
func handleStopHookDocker(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -502,7 +503,7 @@ var webhook = `{
// Starts a new webhook
func handleDeleteHookDocker(resp http.ResponseWriter, request *http.Request) {
ctx := context.Background()
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -619,7 +620,7 @@ func hookTest() {
//https://stackoverflow.com/questions/23935141/how-to-copy-docker-images-from-one-host-to-another-without-using-a-repository
func getDockerImage(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
+12 -11
View File
@@ -1,34 +1,35 @@
module main
go 1.15
go 1.16
replace github.com/shuffle/shuffle-shared => ../../../../git/shuffle-shared
//replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared
//replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi
//replace github.com/frikky/go-elasticsearch => ../../../../git/go-elasticsearch
require (
cloud.google.com/go/datastore v1.6.0
cloud.google.com/go/pubsub v1.17.0
cloud.google.com/go/iam v0.1.1 // indirect
cloud.google.com/go/pubsub v1.17.1
cloud.google.com/go/storage v1.18.2
github.com/basgys/goxml2json v1.1.0
github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82
github.com/docker/docker v20.10.9+incompatible
github.com/docker/docker v20.10.12+incompatible
github.com/frikky/kin-openapi v0.41.0
github.com/fsouza/go-dockerclient v1.7.4
github.com/fsouza/go-dockerclient v1.7.7
github.com/ghodss/yaml v1.0.0
github.com/go-git/go-billy/v5 v5.3.1
github.com/go-git/go-git/v5 v5.4.2
github.com/gorilla/mux v1.8.0
github.com/h2non/filetype v1.1.1
github.com/h2non/filetype v1.1.3
github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20170819232839-0fbfe93532da // indirect
github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.1.79
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/shuffle/shuffle-shared v0.2.7
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
google.golang.org/api v0.58.0
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce
google.golang.org/api v0.65.0
google.golang.org/appengine v1.6.7
google.golang.org/grpc v1.41.0
google.golang.org/grpc v1.43.0
gopkg.in/src-d/go-git.v4 v4.13.1
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)
+131 -32
View File
@@ -1,14 +1,17 @@
package main
import (
uuid "github.com/satori/go.uuid"
"github.com/shuffle/shuffle-shared"
"bufio"
"bytes"
"context"
"crypto/md5"
//"crypto/tls"
//"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
@@ -16,11 +19,13 @@ import (
"io"
"io/ioutil"
"log"
"math/rand"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
//"regexp"
"strings"
"time"
@@ -49,12 +54,14 @@ import (
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/storage/memory"
//cv "github.com/nirasan/go-oauth-pkce-code-verifier"
//githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
// Random
xj "github.com/basgys/goxml2json"
newscheduler "github.com/carlescere/scheduler"
"github.com/satori/go.uuid"
"golang.org/x/crypto/bcrypt"
"gopkg.in/yaml.v3"
@@ -706,7 +713,7 @@ func createNewUser(username, password, role, apikey string, org shuffle.OrgMini)
}
func handleRegister(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -835,7 +842,7 @@ func handleCookie(request *http.Request) bool {
}
func handleInfo(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1118,7 +1125,7 @@ func increaseStatisticsField(ctx context.Context, fieldname, id string, amount i
// FIXME - forward this to emails or whatever CRM system in use
func handleContact(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1163,8 +1170,17 @@ func handleContact(resp http.ResponseWriter, request *http.Request) {
resp.Write([]byte(fmt.Sprintf(`{"success": true, "message": "Thanks for reaching out. We will contact you soon!"}`)))
}
func verifier() (*shuffle.CodeVerifier, error) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
b := make([]byte, 32, 32)
for i := 0; i < 32; i++ {
b[i] = byte(r.Intn(255))
}
return shuffle.CreateCodeVerifierFromBytes(b)
}
func checkAdminLogin(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1186,14 +1202,74 @@ func checkAdminLogin(resp http.ResponseWriter, request *http.Request) {
return
}
//ssoUrl = org.SSOConfig.SOSOEntrypoint
redirectUri := shuffle.SSOUrl
baseSSOUrl := ""
handled := []string{}
for _, user := range users {
if shuffle.ArrayContains(handled, user.ActiveOrg.Id) {
continue
}
handled = append(handled, user.ActiveOrg.Id)
org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id)
if err != nil {
log.Printf("[WARNING] Error getting org in admin check: %s", err)
continue
}
// No childorg setup, only parent org
if len(org.ManagerOrgs) > 0 || len(org.CreatorOrg) > 0 {
continue
}
// Should run calculations
if len(org.SSOConfig.OpenIdAuthorization) > 0 {
log.Printf("[DEBUG] Found OpenID url (PKCE). Extra redirect check: %s", request.URL.String())
baseSSOUrl = org.SSOConfig.OpenIdAuthorization
codeChallenge := uuid.NewV4().String()
//h.Write([]byte(v.Value))
verifier, verifiererr := verifier()
if verifiererr == nil {
codeChallenge = verifier.Value
}
//log.Printf("[DEBUG] Got challenge value %s (pre state)", codeChallenge)
// https://192.168.55.222:3443/api/v1/login_openid
//location := strings.Split(request.URL.String(), "/")
//redirectUrl := url.QueryEscape("http://localhost:5001/api/v1/login_openid")
redirectUrl := url.QueryEscape(fmt.Sprintf("http://%s/api/v1/login_openid", request.Host))
if strings.Contains(request.Host, "shuffle-backend") && !strings.Contains(os.Getenv("BASE_URL"), "shuffle-backend") {
redirectUrl = url.QueryEscape(fmt.Sprintf("%s/api/v1/login_openid", os.Getenv("BASE_URL")))
}
state := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("org=%s&challenge=%s&redirect=%s", org.Id, codeChallenge, redirectUrl)))
// has to happen after initial value is stored
if verifiererr == nil {
codeChallenge = verifier.CodeChallengeS256()
}
//log.Printf("[DEBUG] Got challenge value %s (POST state)", codeChallenge)
baseSSOUrl += fmt.Sprintf("?client_id=%s&response_type=code&scope=openid&redirect_uri=%s&state=%s&code_challenge_method=S256&code_challenge=%s", org.SSOConfig.OpenIdClientId, redirectUrl, state, codeChallenge)
break
}
if len(org.SSOConfig.SSOEntrypoint) > 0 {
log.Printf("[DEBUG] Found SAML SSO url")
baseSSOUrl = org.SSOConfig.SSOEntrypoint
break
}
}
//log.Printf("[DEBUG] OpenID URL: %s", baseSSOUrl)
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "redirect", "sso_url": "%s"}`, redirectUri)))
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "redirect", "sso_url": "%s"}`, baseSSOUrl)))
}
func handleLogin(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1418,8 +1494,12 @@ func fixUserOrg(ctx context.Context, user *shuffle.User) *shuffle.User {
}
// Used for testing only. Shouldn't impact production.
func handleCors(resp http.ResponseWriter, request *http.Request) bool {
allowedOrigins := "http://localhost:3000"
/*
func shuffle.HandleCors(resp http.ResponseWriter, request *http.Request) bool {
// Used for Codespace dev
allowedOrigins := "https://frikky-shuffle-5gvr4xx62w64-3000.githubpreview.dev"
//origin := request.Header["Origin"]
//log.Printf("Origin: %s", origin)
//allowedOrigins := "http://localhost:3002"
resp.Header().Set("Vary", "Origin")
@@ -1436,6 +1516,7 @@ func handleCors(resp http.ResponseWriter, request *http.Request) bool {
return false
}
*/
func parseWorkflowParameters(resp http.ResponseWriter, request *http.Request) (map[string]interface{}, error) {
body, err := ioutil.ReadAll(request.Body)
@@ -1580,7 +1661,7 @@ func SearchNested(obj interface{}, key string) (interface{}, bool) {
}
func handleSetHook(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1768,7 +1849,7 @@ func verifyHook(hook shuffle.Hook) (bool, string) {
}
func setSpecificSchedule(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1828,7 +1909,7 @@ func setSpecificSchedule(resp http.ResponseWriter, request *http.Request) {
}
func getSpecificWebhook(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1880,7 +1961,7 @@ func getSpecificWebhook(resp http.ResponseWriter, request *http.Request) {
// Starts a new webhook
func handleDeleteSchedule(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1935,7 +2016,7 @@ func handleDeleteSchedule(resp http.ResponseWriter, request *http.Request) {
// Starts a new webhook
func handleNewSchedule(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -2229,7 +2310,7 @@ func getSpecificSchedule(resp http.ResponseWriter, request *http.Request) {
return
}
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -2295,7 +2376,7 @@ func loadYaml(fileLocation string) (ApiYaml, error) {
// This should ALWAYS come from an OUTPUT
func executeSchedule(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -2788,7 +2869,7 @@ type Result struct {
// r.HandleFunc("/api/v1/docs/{key}", getDocs).Methods("GET", "OPTIONS")
func getOpenapi(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -3254,7 +3335,7 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User) {
// Creates an app from the app builder
func verifySwagger(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -3316,11 +3397,6 @@ func createFs(basepath, pathname string) (billy.Filesystem, error) {
return err
}
//if strings.Contains(path, "yaml") {
// log.Printf("PATH: %s -> %s", path, fullpath)
// //log.Printf("DATA: %s", string(srcData))
//}
dst, err := fs.Create(fullpath)
if err != nil {
log.Printf("Dst error: %s", err)
@@ -3863,7 +3939,7 @@ func runInitEs(ctx context.Context) {
}
for _, schedule := range schedules {
if schedule.Environment == "cloud" {
if strings.ToLower(schedule.Environment) == "cloud" {
log.Printf("Skipping cloud schedule")
continue
}
@@ -4999,7 +5075,7 @@ func handleStopCloudSync(syncUrl string, org shuffle.Org) (*shuffle.Org, error)
This is here to both enable and disable cloud sync features for an organization
*/
func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -5101,8 +5177,19 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
_, err = handleStopCloudSync(syncPath, *org)
if err != nil {
ret := shuffle.ResultChecker{
Success: false,
Reason: fmt.Sprintf("%s", err),
}
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
b, err := json.Marshal(ret)
if err != nil {
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return
}
resp.Write(b)
} else {
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Successfully disabled cloud sync for org."}`)))
@@ -5687,10 +5774,15 @@ func initHandlers() {
dbclient, err = datastore.NewClient(ctx, gceProject, option.WithGRPCDialOption(grpc.WithNoProxy()))
if err != nil {
if elasticConfig == "" {
log.Fatalf("[ERROR] Database client error during init: %s. Env: SHUFFLE_ELASTIC=false", err)
log.Printf("[ERROR] Database client error during init: %s. Env: SHUFFLE_ELASTIC=false", err)
} else {
log.Printf("[DEBUG] Database client error during init: %s. Here for backwards compatibility: not critical.", err)
if !strings.Contains(fmt.Sprintf("%s", err), "find default credentials") {
log.Printf("[DEBUG] Database client error info during init: %s. Here for backwards compatibility: not critical.", err)
}
dbclient = &datastore.Client{}
}
} else {
//log.Printf("Database client initiated: %s", dbclient)
}
for {
@@ -5783,10 +5875,13 @@ func initHandlers() {
r.HandleFunc("/api/v1/apps/authentication/{appauthId}/config", shuffle.SetAuthenticationConfig).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/authentication/{appauthId}", shuffle.DeleteAppAuthentication).Methods("DELETE", "OPTIONS")
// Related to
// Related to NFT things
r.HandleFunc("/api/v1/workflows/collections/load", shuffle.LoadCollections).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/workflows/collections/{key}", shuffle.HandleGetCollection).Methods("GET", "OPTIONS")
// Related to use-cases that are not directly workflows.
r.HandleFunc("/api/v1/workflows/usecases", shuffle.LoadUsecases).Methods("GET", "OPTIONS")
// Legacy app things
r.HandleFunc("/api/v1/workflows/apps/validate", validateAppInput).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/workflows/apps", getWorkflowApps).Methods("GET", "OPTIONS")
@@ -5868,7 +5963,8 @@ func initHandlers() {
// Docker orborus specific - downloads an image
r.HandleFunc("/api/v1/get_docker_image", getDockerImage).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/migrate_database", migrateDatabase).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/login_sso", shuffle.HandleSSO).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/login_sso", shuffle.HandleSSO).Methods("GET", "POST", "OPTIONS")
r.HandleFunc("/api/v1/login_openid", shuffle.HandleOpenId).Methods("GET", "OPTIONS")
// Important for email, IDS etc. Create this by:
// PS: For cloud, this has to use cloud storage.
@@ -5887,6 +5983,9 @@ func initHandlers() {
r.HandleFunc("/api/v1/notifications/clear", shuffle.HandleClearNotifications).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/notifications/{notificationId}/markasread", shuffle.HandleMarkAsRead).Methods("GET", "OPTIONS")
//r.HandleFunc("/api/v1/notifications/{notificationId}/markasread", shuffle.HandleMarkAsRead).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/users/notifications", shuffle.HandleGetNotifications).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/users/notifications/clear", shuffle.HandleClearNotifications).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/users/notifications/{notificationId}/markasread", shuffle.HandleMarkAsRead).Methods("GET", "OPTIONS")
http.Handle("/", r)
}
+17 -15
View File
@@ -21,6 +21,7 @@ import (
"github.com/docker/docker/api/types"
dockerclient "github.com/docker/docker/client"
//gyaml "github.com/ghodss/yaml"
"github.com/h2non/filetype"
@@ -34,6 +35,7 @@ import (
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/storage/memory"
http2 "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
//"github.com/gorilla/websocket"
//"google.golang.org/appengine"
//"google.golang.org/appengine/memcache"
@@ -142,7 +144,7 @@ func createSchedule(ctx context.Context, scheduleId, workflowId, name, startNode
}
func handleGetWorkflowqueueConfirm(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -243,7 +245,7 @@ func handleGetWorkflowqueueConfirm(resp http.ResponseWriter, request *http.Reque
// FIXME: Authenticate this one? Can org ID be auth enough?
// (especially since we have a default: shuffle)
func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -335,7 +337,7 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
}
func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -393,7 +395,7 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) {
}
func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -666,7 +668,7 @@ func handleExecutionStatistics(execution shuffle.WorkflowExecution) {
}
func deleteWorkflow(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1034,7 +1036,7 @@ func cloudExecuteAction(execution shuffle.WorkflowExecution) error {
}
func executeWorkflow(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1131,7 +1133,7 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) {
}
func stopSchedule(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1281,7 +1283,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) {
}
func stopScheduleGCP(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1390,7 +1392,7 @@ func deleteSchedule(ctx context.Context, id string) error {
}
func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1641,7 +1643,7 @@ func setExampleresult(ctx context.Context, result shuffle.AppExecutionExample) e
}
func getWorkflowApps(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1714,7 +1716,7 @@ func handleGetfile(resp http.ResponseWriter, request *http.Request) ([]byte, err
// Basically a search for apps that aren't activated yet
func getSpecificApps(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1789,7 +1791,7 @@ func getSpecificApps(resp http.ResponseWriter, request *http.Request) {
}
func validateAppInput(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1910,7 +1912,7 @@ func loadGithubWorkflows(url, username, password, userId, branch, orgId string)
}
func loadSpecificWorkflows(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -1972,7 +1974,7 @@ func loadSpecificWorkflows(resp http.ResponseWriter, request *http.Request) {
}
func handleAppHotloadRequest(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
@@ -2290,7 +2292,7 @@ func iterateWorkflowGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra
}
func setNewWorkflowApp(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
+1 -1
View File
@@ -3,7 +3,7 @@
#curl http://localhost:5001/api/v1/files/create -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" -d '{"filename": "file.txt", "org_id": "b199646b-16d2-456d-9fd6-b9972e929466", "workflow_id": "global"}'
#
#echo
#curl http://localhost:5001/api/v1/files/e19cffe4-e2da-47e9-809e-904f5cb03687/upload -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" -F 'shuffle_file=@files.sh'
curl http://localhost:5001/api/v1/apps/upload -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" -F 'shuffle_file=@files.sh'
#
#curl http://localhost:5001/api/v1/files/1915981b-b897-4db1-8a2e-44bc34cead3b/content -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4"
#curl http://localhost:5001/api/v1/files/e19cffe4-e2da-47e9-809e-904f5cb03687 -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4"
+2 -5
View File
@@ -48,7 +48,6 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- SHUFFLE_WORKER_VERSION=nightly
- ORG_ID=${ORG_ID}
- ENVIRONMENT_NAME=${ENVIRONMENT_NAME}
- BASE_URL=http://${OUTER_HOSTNAME}:${BACKEND_PORT}
- DOCKER_API_VERSION=1.40
@@ -64,13 +63,12 @@ services:
- SHUFFLE_SWARM_CONFIG=runn
restart: unless-stopped
opensearch:
image: opensearchproject/opensearch:1.2.3
image: opensearchproject/opensearch:1.2.4
hostname: shuffle-opensearch
container_name: shuffle-opensearch
environment:
- bootstrap.memory_lock=true
- "OPENSEARCH_JAVA_OPTS=-Xms1024m -Xmx1024m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
- plugins.security.disabled=true
- "OPENSEARCH_JAVA_OPTS=-Xms2048m -Xmx2048m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
- cluster.routing.allocation.disk.threshold_enabled=false
- cluster.name=shuffle-cluster
- node.name=shuffle-opensearch
@@ -96,4 +94,3 @@ networks:
driver: bridge
#driver: overlay
#driver: bridge
+3 -1
View File
@@ -1,10 +1,11 @@
{
"name": "shuffler",
"homepage": "https://shuffler.io",
"version": "0.9.50",
"version": "0.9.61",
"private": true,
"dependencies": {
"@babel/core": "^7.15.8",
"@emotion/is-prop-valid": "^1.1.1",
"@emotion/react": "^11.7.0",
"@emotion/styled": "^11.6.0",
"@material-ui/core": "^4.5.2",
@@ -66,6 +67,7 @@
"react-scripts": "^4.0.1",
"react-shepherd": "^3.3.6",
"reactstrap": "^7.1.0",
"reaviz": "^12.1.0",
"shellwords": "^0.1.1",
"simplebar": "^4.2.3",
"styled-components": "^4.4.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

+14 -6
View File
@@ -15,7 +15,7 @@ import theme from "./theme";
import Apps from "./views/Apps";
import AppCreator from "./views/AppCreator";
import Dashboard from "./views/Dashboard";
import Dashboard from "./views/Dashboard.jsx";
import AdminSetup from "./views/AdminSetup";
import Admin from "./views/Admin";
import Docs from "./views/Docs";
@@ -49,6 +49,12 @@ if (window.location.port === "3000") {
//globalUrl = "http://localhost:5002"
}
if (globalUrl.includes("githubpreview.dev")) {
//globalUrl = globalUrl.replace("3000", "5001")
globalUrl = "https://frikky-shuffle-5gvr4xx62w64-5001.githubpreview.dev"
}
console.log("global: ", globalUrl)
const App = (message, props) => {
const [userdata, setUserData] = useState({});
@@ -70,7 +76,7 @@ const App = (message, props) => {
checkLogin();
setDataset(true);
}
});
}, []);
if (
isLoaded &&
@@ -78,17 +84,19 @@ const App = (message, props) => {
!window.location.pathname.startsWith("/login") &&
!window.location.pathname.startsWith("/docs") &&
!window.location.pathname.startsWith("/detectionframework") &&
!window.location.pathname.startsWith("/adminsetup")
!window.location.pathname.startsWith("/adminsetup") &&
!window.location.pathname.startsWith("/usecases")
) {
window.location = "/login";
}
const getUserNotifications = () => {
fetch(`${globalUrl}/api/v1/notifications`, {
fetch(`${globalUrl}/api/v1/users/notifications`, {
credentials: "include",
headers: {
"Content-Type": "application/json",
},
cors: "cors",
})
.then((response) => response.json())
.then((responseJson) => {
@@ -109,7 +117,7 @@ const App = (message, props) => {
const checkLogin = () => {
var baseurl = globalUrl;
fetch(baseurl + "/api/v1/users/getinfo", {
fetch(`${globalUrl}/api/v1/getinfo`, {
credentials: "include",
headers: {
"Content-Type": "application/json",
@@ -399,7 +407,7 @@ const App = (message, props) => {
/>
<Route
exact
path="/dashboard"
path="/usecases"
element={
<Dashboard
isLoaded={isLoaded}
@@ -556,7 +556,7 @@ const ConfigureWorkflow = (props) => {
{action.must_activate ? (
<Button
color="primary"
variant="outlined"
variant="contained"
onClick={() => {
console.log("ACTION: ", action)
activateApp(action.action.app_id, action.app_name, action.app_version);
@@ -666,7 +666,7 @@ const ConfigureWorkflow = (props) => {
}
}}
>
Finish setup
Close window
</Button>
</ButtonGroup>
</div>
@@ -1499,7 +1499,7 @@ const Framework = (props) => {
/>
:
<div>
TBD: Coming in 1.0.0.
Coming in 1.0.0. <a style={{ textDecoration: "none", color: "#f85a3e" }} href="https://shuffler.io/register" target="_blank">Register for Shuffle cloud</a> to try an early version now.
</div>
: null}
</div>
+11
View File
@@ -35,6 +35,7 @@ import {
import {
Analytics as AnalyticsIcon,
Lightbulb as LightbulbIcon,
} from "@mui/icons-material";
//import LogoutIcon from '@mui/icons-material/Logout';
import { useAlert } from "react-alert";
@@ -466,6 +467,16 @@ const Header = (props) => {
<AnalyticsIcon style={{marginRight: 5 }}/> Get Started
</Link>
</MenuItem>
<MenuItem
onClick={(event) => {
event.preventDefault();
handleClose();
}}
>
<Link to="/usecases" style={hrefStyle}>
<LightbulbIcon style={{marginRight: 5 }}/> Use Cases
</Link>
</MenuItem>
<MenuItem
onClick={(event) => {
event.preventDefault();
+202
View File
@@ -0,0 +1,202 @@
import React, {useState, useRef, useImperativeHandle} from 'react'
import {makeStyles} from '@material-ui/core/styles'
import Menu, {MenuProps} from '@material-ui/core/Menu'
import MenuItem, {MenuItemProps} from '@material-ui/core/MenuItem'
import ArrowRight from '@material-ui/icons/ArrowRight'
import clsx from 'clsx'
export interface NestedMenuItemProps extends Omit<MenuItemProps, 'button'> {
/**
* Open state of parent `<Menu />`, used to close decendent menus when the
* root menu is closed.
*/
parentMenuOpen: boolean
/**
* Component for the container element.
* @default 'div'
*/
component?: React.ElementType
/**
* Effectively becomes the `children` prop passed to the `<MenuItem/>`
* element.
*/
label?: React.ReactNode
/**
* @default <ArrowRight />
*/
rightIcon?: React.ReactNode
/**
* Props passed to container element.
*/
ContainerProps?: React.HTMLAttributes<HTMLElement> &
React.RefAttributes<HTMLElement | null>
/**
* Props passed to sub `<Menu/>` element
*/
MenuProps?: Omit<MenuProps, 'children'>
/**
* @see https://material-ui.com/api/list-item/
*/
button?: true | undefined
}
const TRANSPARENT = 'rgba(0,0,0,0)'
const useMenuItemStyles = makeStyles((theme) => ({
root: (props: any) => ({
backgroundColor: props.open ? theme.palette.action.hover : TRANSPARENT
})
}))
/**
* Use as a drop-in replacement for `<MenuItem>` when you need to add cascading
* menu elements as children to this component.
*/
const NestedMenuItem = React.forwardRef<
HTMLLIElement | null,
NestedMenuItemProps
>(function NestedMenuItem(props, ref) {
const {
parentMenuOpen,
component = 'div',
label,
rightIcon = <ArrowRight />,
children,
className,
tabIndex: tabIndexProp,
MenuProps = {},
ContainerProps: ContainerPropsProp = {},
...MenuItemProps
} = props
const {ref: containerRefProp, ...ContainerProps} = ContainerPropsProp
const menuItemRef = useRef<HTMLLIElement>(null)
useImperativeHandle(ref, () => menuItemRef.current)
const containerRef = useRef<HTMLDivElement>(null)
useImperativeHandle(containerRefProp, () => containerRef.current)
const menuContainerRef = useRef<HTMLDivElement>(null)
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false)
const handleMouseEnter = (event: React.MouseEvent<HTMLElement>) => {
setIsSubMenuOpen(true)
if (ContainerProps?.onMouseEnter) {
ContainerProps.onMouseEnter(event)
}
}
const handleMouseLeave = (event: React.MouseEvent<HTMLElement>) => {
setIsSubMenuOpen(false)
if (ContainerProps?.onMouseLeave) {
ContainerProps.onMouseLeave(event)
}
}
// Check if any immediate children are active
const isSubmenuFocused = () => {
const active = containerRef.current?.ownerDocument?.activeElement
for (const child of menuContainerRef.current?.children ?? []) {
if (child === active) {
return true
}
}
return false
}
const handleFocus = (event: React.FocusEvent<HTMLElement>) => {
if (event.target === containerRef.current) {
setIsSubMenuOpen(true)
}
if (ContainerProps?.onFocus) {
ContainerProps.onFocus(event)
}
}
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Escape') {
return
}
if (isSubmenuFocused()) {
event.stopPropagation()
}
const active = containerRef.current?.ownerDocument?.activeElement
if (event.key === 'ArrowLeft' && isSubmenuFocused()) {
containerRef.current?.focus()
}
if (
event.key === 'ArrowRight' &&
event.target === containerRef.current &&
event.target === active
) {
const firstChild = menuContainerRef.current?.children[0] as
| HTMLElement
| undefined
firstChild?.focus()
}
}
const open = isSubMenuOpen && parentMenuOpen
const menuItemClasses = useMenuItemStyles({open})
// Root element must have a `tabIndex` attribute for keyboard navigation
let tabIndex
if (!props.disabled) {
tabIndex = tabIndexProp !== undefined ? tabIndexProp : -1
}
return (
<div
{...ContainerProps}
ref={containerRef}
onFocus={handleFocus}
tabIndex={tabIndex}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onKeyDown={handleKeyDown}
>
<MenuItem
{...MenuItemProps}
className={clsx(menuItemClasses.root, className)}
ref={menuItemRef}
>
{label}
{rightIcon}
</MenuItem>
<Menu
// Set pointer events to 'none' to prevent the invisible Popover div
// from capturing events for clicks and hovers
style={{pointerEvents: 'none'}}
anchorEl={menuItemRef.current}
anchorOrigin={{
vertical: 'top',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left'
}}
open={open}
autoFocus={false}
disableAutoFocus
disableEnforceFocus
onClose={() => {
setIsSubMenuOpen(false)
}}
>
<div ref={menuContainerRef} style={{pointerEvents: 'auto'}}>
{children}
</div>
</Menu>
</div>
)
})
export default NestedMenuItem
+57 -28
View File
@@ -49,6 +49,8 @@ const MenuProps = {
scrollX: "auto",
},
},
variant: "menu",
getContentAnchorEl: null,
};
const AuthenticationOauth2 = (props) => {
@@ -84,6 +86,7 @@ const AuthenticationOauth2 = (props) => {
const [oauthUrl, setOauthUrl] = React.useState("");
const [buttonClicked, setButtonClicked] = React.useState(false);
const [selectedScopes, setSelectedScopes] = React.useState([]);
const [offlineAccess, setOfflineAccess] = React.useState(true);
const allscopes =
authenticationType.scope !== undefined ? authenticationType.scope : [];
@@ -111,8 +114,19 @@ const AuthenticationOauth2 = (props) => {
setButtonClicked(true);
console.log("SCOPES: ", scopes);
client_id = client_id.trim()
client_secret = client_secret.trim()
oauth_url = oauth_url.trim()
var resources = "";
if (scopes !== undefined && (scopes !== null) & (scopes.length > 0)) {
if (offlineAccess === true && !scopes.includes("offline_access")) {
if (authenticationType.redirect_uri.includes("microsoft")) {
console.log("Appending offline access")
scopes.push("offline_access")
}
}
resources = scopes.join(" ");
//resources = scopes.join(",");
}
@@ -496,34 +510,49 @@ const AuthenticationOauth2 = (props) => {
}}
/>
{allscopes.length === 0 ? null : (
<span style={{marginTop: 10}}>
Scopes
<Select
multiple
value={selectedScopes}
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
padding: 5,
}}
onChange={(e) => {
handleScopeChange(e);
}}
fullWidth
input={<Input id="select-multiple-native" />}
renderValue={(selected) => selected.join(", ")}
MenuProps={MenuProps}
>
{allscopes.map((data, index) => {
return (
<MenuItem key={index} value={data}>
<Checkbox checked={selectedScopes.indexOf(data) > -1} />
<ListItemText primary={data} />
</MenuItem>
);
})}
</Select>
</span>
<div style={{width: "100%", marginTop: 10, display: "flex"}}>
<span>
Scopes
<Select
multiple
value={selectedScopes}
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
padding: 5,
minWidth: 300,
maxWidth: 300,
}}
onChange={(e) => {
handleScopeChange(e)
}}
fullWidth
input={<Input id="select-multiple-native" />}
renderValue={(selected) => selected.join(", ")}
MenuProps={MenuProps}
>
{allscopes.map((data, index) => {
return (
<MenuItem key={index} value={data}>
<Checkbox checked={selectedScopes.indexOf(data) > -1} />
<ListItemText primary={data} />
</MenuItem>
);
})}
</Select>
</span>
<span>
<Tooltip
color="primary"
title={"Automatic Refresh (default: true)"}
placement="top"
>
<Checkbox style={{paddingTop: 20}} color="secondary" checked={offlineAccess} onClick={() => {
setOfflineAccess(!offlineAccess)
}}/>
</Tooltip>
</span>
</div>
)}
</span>
)}
+222 -73
View File
@@ -97,6 +97,30 @@ const OrgHeader = (props) => {
? ""
: selectedOrganization.defaults.notification_workflow
);
const [openidClientId, setOpenidClientId] = React.useState(
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.client_id === undefined ||
selectedOrganization.sso_config.client_id.length === 0
? ""
: selectedOrganization.sso_config.client_id
);
const [openidAuthorization, setOpenidAuthorization] = React.useState(
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.openid_authorization === undefined ||
selectedOrganization.sso_config.openid_authorization.length === 0
? ""
: selectedOrganization.sso_config.openid_authorization
);
const [openidToken, setOpenidToken] = React.useState(
selectedOrganization.sso_config === undefined
? ""
: selectedOrganization.sso_config.openid_token === undefined ||
selectedOrganization.sso_config.openid_token.length === 0
? ""
: selectedOrganization.sso_config.openid_token
)
const [file, setFile] = React.useState("");
const [fileBase64, setFileBase64] = React.useState(
@@ -145,6 +169,7 @@ const OrgHeader = (props) => {
defaults,
sso_config
) => {
const data = {
name: name,
description: description,
@@ -216,6 +241,9 @@ const OrgHeader = (props) => {
{
sso_entrypoint: ssoEntrypoint,
sso_certificate: ssoCertificate,
client_id: openidClientId,
openid_authorization: openidAuthorization,
openid_token: openidToken,
}
)
}
@@ -548,79 +576,200 @@ const OrgHeader = (props) => {
</span>
</Grid>
)}
<Grid item xs={6} style={{}}>
<span>
<Typography>SSO Entrypoint (IdP)</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
multiline={true}
rows={2}
disabled={
selectedOrganization.manager_orgs !== undefined &&
selectedOrganization.manager_orgs !== null &&
selectedOrganization.manager_orgs.length > 0
}
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="The entrypoint URL from your provider"
value={ssoEntrypoint}
onChange={(e) => {
setSsoEntrypoint(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
<Grid item xs={6} style={{}}>
<span>
<Typography>SSO Certificate (X509)</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
multiline={true}
rows={2}
placeholder="The X509 certificate to use"
value={ssoCertificate}
onChange={(e) => {
setSsoCertificate(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
{isCloud ? null :
<Grid item xs={12} style={{marginTop: 50 }}>
<Typography variant="h4" style={{textAlign: "center",}}>OpenID connect</Typography>
<Grid container style={{marginTop: 10, }}>
<Grid item xs={4} style={{}}>
<span>
<Typography>Client ID</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
multiline={true}
rows={2}
disabled={
selectedOrganization.manager_orgs !== undefined &&
selectedOrganization.manager_orgs !== null &&
selectedOrganization.manager_orgs.length > 0
}
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="The OpenID client ID from the identity provider"
value={openidClientId}
onChange={(e) => {
setOpenidClientId(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
<Grid item xs={4} style={{}}>
<span>
<Typography>Authorization URL</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
multiline={true}
rows={2}
placeholder="The OpenID authorization URL (usually ends with /authorize)"
value={openidAuthorization}
onChange={(e) => {
setOpenidAuthorization(e.target.value)
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
<Grid item xs={4} style={{}}>
<span>
<Typography>Token URL</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
multiline={true}
rows={2}
placeholder="The OpenID token URL (usually ends with /token)"
value={openidToken}
onChange={(e) => {
setOpenidToken(e.target.value)
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
</Grid>
</Grid>
}
{isCloud ? null :
<Grid item xs={12} style={{marginTop: 50,}}>
<Typography variant="h4" style={{textAlign: "center",}}>SAML SSO (v1.1)</Typography>
<Grid container style={{marginTop: 10, }}>
<Grid item xs={6} style={{}}>
<span>
<Typography>SSO Entrypoint (IdP)</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
multiline={true}
rows={2}
disabled={
selectedOrganization.manager_orgs !== undefined &&
selectedOrganization.manager_orgs !== null &&
selectedOrganization.manager_orgs.length > 0
}
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="The entrypoint URL from your provider"
value={ssoEntrypoint}
onChange={(e) => {
setSsoEntrypoint(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
<Grid item xs={6} style={{}}>
<span>
<Typography>SSO Certificate (X509)</Typography>
<TextField
required
style={{
flex: "1",
marginTop: "5px",
marginRight: "15px",
backgroundColor: theme.palette.inputColor,
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
multiline={true}
rows={2}
placeholder="The X509 certificate to use"
value={ssoCertificate}
onChange={(e) => {
setSsoCertificate(e.target.value);
}}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
},
style: {
color: "white",
},
}}
/>
</span>
</Grid>
</Grid>
</Grid>
}
{/*
<span style={{textAlign: "center"}}>
{expanded ?
@@ -0,0 +1,19 @@
import React, {useState, useEffect, useLayoutEffect} from 'react';
import Draggable from "react-draggable";
import {
Paper
} from "@material-ui/core";
const PaperComponent = (props) => {
return (
<Draggable
handle="#draggable-dialog-title"
cancel={'[class*="MuiDialogContent-root"]'}
>
<Paper {...props} />
</Draggable>
)
}
export default PaperComponent;
+173 -91
View File
@@ -1,11 +1,12 @@
import React, { useState, useEffect, useLayoutEffect } from "react";
import { makeStyles, createStyles } from "@material-ui/core/styles";
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
import { GetParsedPaths } from "../views/Apps.jsx";
import { GetIconInfo } from "../views/Workflows.jsx";
import { sortByKey } from "../views/AngularWorkflow.jsx";
import { useTheme } from "@material-ui/core/styles";
import NestedMenuItem from "material-ui-nested-menu-item";
import { useAlert } from "react-alert";
import theme from '../theme';
//import NestedMenuItem from "./NestedMenu.jsx";
@@ -168,6 +169,7 @@ const ParsedAction = (props) => {
//const theme = useTheme();
const classes = useStyles();
const alert = useAlert()
const [expansionModalOpen, setExpansionModalOpen] = React.useState(false);
const [hideBody, setHideBody] = React.useState(true);
@@ -178,23 +180,21 @@ const ParsedAction = (props) => {
const [hiddenDescription, setHiddenDescription] = React.useState(true);
useEffect(() => {
//if (data.startsWith("${") && data.endsWith("}")) {
//}
// PARAM FIX - Gonna use the ID field, even though it's a hack
const paramcheck = selectedAction.parameters.find(param => param.name === "body")
console.log("LOADED! Change hideBody based on input? Action: ", selectedAction, paramcheck)
if (paramcheck !== undefined && paramcheck !== null) {
if (paramcheck.id === "TOGGLED"){
setHideBody(false)
setActivateHidingBodyButton(false)
console.log("TOGGLED BODY!")
} else {
setHideBody(true)
if (paramcheck.id === "UNTOGGLED") {
if (selectedAction.parameters !== null && selectedAction.parameters !== undefined) {
const paramcheck = selectedAction.parameters.find(param => param.name === "body")
//console.log("LOADED! Change hideBody based on input? Action: ", selectedAction, paramcheck)
if (paramcheck !== undefined && paramcheck !== null) {
if (paramcheck.id === "TOGGLED"){
setHideBody(false)
setActivateHidingBodyButton(false)
console.log("UNTOGGLED!")
console.log("TOGGLED BODY!")
} else {
setHideBody(true)
if (paramcheck.id === "UNTOGGLED") {
setActivateHidingBodyButton(false)
console.log("UNTOGGLED!")
}
}
}
}
@@ -304,6 +304,8 @@ const ParsedAction = (props) => {
});
};
const defineStartnode = () => {
if (cy === undefined) {
return;
@@ -391,14 +393,46 @@ const ParsedAction = (props) => {
if (actionlist.length === 0) {
// FIXME: Have previous execution values in here
actionlist.push({
type: "Execution Argument",
name: "Execution Argument",
value: "$exec",
highlight: "exec",
autocomplete: "exec",
example: "",
});
if (workflowExecutions.length > 0) {
for (var key in workflowExecutions) {
if (
workflowExecutions[key].execution_argument === undefined ||
workflowExecutions[key].execution_argument === null ||
workflowExecutions[key].execution_argument.length === 0
) {
continue;
}
console.log("EXEC: ", workflowExecutions[key].execution_argument)
const valid = validateJson(workflowExecutions[key].execution_argument)
console.log("VALID: ", valid)
if (valid.valid) {
actionlist.push({
type: "Execution Argument",
name: "Execution Argument",
value: "$exec",
highlight: "exec",
autocomplete: "exec",
example: valid.result,
})
break
}
}
}
if (actionlist.length === 0) {
actionlist.push({
type: "Execution Argument",
name: "Execution Argument",
value: "$exec",
highlight: "exec",
autocomplete: "exec",
example: "",
})
}
actionlist.push({
type: "Shuffle DB",
name: "Shuffle DB",
@@ -454,7 +488,8 @@ const ParsedAction = (props) => {
continue;
}
var exampledata = item.example === undefined ? "" : item.example;
var exampledata = item.example === undefined || item.example === null ? "" : item.example;
console.log("EXAMPLE: ", exampledata)
// Find previous execution and their variables
//exampledata === "" &&
if (workflowExecutions.length > 0) {
@@ -471,55 +506,22 @@ const ParsedAction = (props) => {
var foundResult = workflowExecutions[key].results.find(
(result) => result.action.id === item.id
);
if (foundResult === undefined) {
if (foundResult === undefined || foundResult === null) {
continue;
}
foundResult.result = foundResult.result.trim();
foundResult.result = foundResult.result
.split(" None")
.join(' "None"');
foundResult.result = foundResult.result
.split(" False")
.join(" false");
foundResult.result = foundResult.result
.split(" True")
.join(" true");
if (foundResult.result !== undefined && foundResult.result !== null) {
foundResult = foundResult.result
}
console.log("VALID RESULT: ", foundResult)
var jsonvalid = true;
try {
const tmp = String(JSON.parse(foundResult.result));
if (
!foundResult.result.includes("{") &&
!foundResult.result.includes("[")
) {
jsonvalid = false;
}
} catch (e) {
try {
foundResult.result = foundResult.result
.split("'")
.join('"');
const tmp = String(JSON.parse(foundResult.result));
if (
!foundResult.result.includes("{") &&
!foundResult.result.includes("[")
) {
jsonvalid = false;
}
} catch (e) {
jsonvalid = false;
}
}
// Finds the FIRST json only
if (jsonvalid) {
exampledata = JSON.parse(foundResult.result);
const valid = validateJson(foundResult)
if (valid.valid) {
exampledata = valid.result;
break;
}
//else {
// console.log("Invalid JSON: ", foundResult.result)
//}
} else {
exampledata = foundResult;
}
}
}
@@ -528,6 +530,7 @@ const ParsedAction = (props) => {
item.label === null || item.label === undefined
? ""
: item.label.split(" ").join("_");
const actionvalue = {
type: "action",
id: item.id,
@@ -539,11 +542,65 @@ const ParsedAction = (props) => {
}
}
//console.log("ACTIONLIST: ", actionlist)
setActionlist(actionlist);
}
}
});
const calculateHelpertext = (input_data) => {
var helperText = ""
var looperText = ""
const found = input_data.match(/[$]{1}([a-zA-Z0-9_-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,}/g)
if (found !== null) {
try {
// When the found array is empty.
for (var i = 0; i < found.length; i++) {
const variableSplit = found[i].split(".#")
if ((variableSplit.length-1) > 1) {
//console.log("Larger than 1: ", variableSplit)
if (looperText.length === 0) {
looperText += "PS: Double looping (.#) may cause problems."
}
}
var foundSlice = false
for (var j = 0; j < actionlist.length; j++) {
//console.log("ACTION: ", found[i], actionlist[j])
//console.log("ACTION :", found[i].split(".")[0].slice(1,).toLowerCase(), actionlist[j].autocomplete.toLowerCase())
if(found[i].split(".")[0].slice(1,).toLowerCase() == actionlist[j].autocomplete.toLowerCase()){
//console.log("Found: ", found[i])
// Validate path?
foundSlice = true
}
}
if (!foundSlice) {
if (!helperText.includes("Invalid variables")) {
helperText+= "Invalid variables: "
}
helperText+= found[i] + ", "
}
}
} catch (e) {
console.log("Parsing error: ", e)
}
}
if (looperText.length > 0) {
if (helperText.length > 0) {
helperText += ". "
}
helperText += looperText
}
return helperText
}
const changeActionParameter = (event, count, data) => {
//console.log("Action change: ", selectedAction, data)
if (data.name.startsWith("${") && data.name.endsWith("}")) {
@@ -1282,21 +1339,27 @@ const ParsedAction = (props) => {
const clickedFieldId = "rightside_field_" + count;
const shufflecode = <ShuffleCodeEditor
fieldCount = {fieldCount}
setFieldCount = {setFieldCount}
actionlist = {actionlist}
changeActionParameterCodeMirror = {changeActionParameterCodeMirror}
codedata={codedata}
setcodedata={setcodedata}
expansionModalOpen={expansionModalOpen}
setExpansionModalOpen={setExpansionModalOpen}
/>
const shufflecode = fieldCount !== count ? null :
(
<ShuffleCodeEditor
fieldCount = {fieldCount}
setFieldCount = {setFieldCount}
actionlist = {actionlist}
changeActionParameterCodeMirror = {changeActionParameterCodeMirror}
codedata={codedata}
setcodedata={setcodedata}
expansionModalOpen={expansionModalOpen}
setExpansionModalOpen={setExpansionModalOpen}
/>
)
//<TextareaAutosize
// <CodeMirror
//fullWidth
var baseHelperText = ""
if (data !== undefined && data !== null && data.value !== undefined && data.value !== null && data.value.length > 0) {
baseHelperText = calculateHelpertext(data.value)
}
var datafield = (
<TextField
@@ -1405,7 +1468,7 @@ const ParsedAction = (props) => {
//changeActionParameterCodemirror(event, count, data)
changeActionParameter(event, count, data);
}}
helperText={
helperText={baseHelperText.length > 0 ? baseHelperText :
selectedApp.generated &&
selectedApp.activated &&
data.name === "body" ? (
@@ -1423,15 +1486,7 @@ const ParsedAction = (props) => {
) : null
}
onBlur={(event) => {
// Super basic check
//if (event.target.value.startsWith("{")) {
// console.log("VALIDATING JSON")
// try {
// JSON.parse(event.target.value)
// } catch (e) {
// alert.error("Failed to parse json: ", e)
// }
//}
baseHelperText = calculateHelpertext(event.target.value)
}}
/>
);
@@ -1536,6 +1591,9 @@ const ParsedAction = (props) => {
datafield = (
<Select
MenuProps={{
disableScrollLock: true,
}}
SelectDisplayProps={{
style: {
marginLeft: 10,
@@ -2036,6 +2094,9 @@ const ParsedAction = (props) => {
Autocomplete
</InputLabel>
<Select
MenuProps={{
disableScrollLock: true,
}}
labelId="action-autocompleter"
SelectDisplayProps={{
style: {
@@ -2333,11 +2394,17 @@ const ParsedAction = (props) => {
selectedApp.versions !== undefined &&
selectedApp.versions.length > 1 ? (
<Select
MenuProps={{
disableScrollLock: true,
}}
defaultValue={selectedAction.app_version}
onChange={(event) => {
console.log("VAL: ", event.target.value)
console.log("App: ", selectedApp)
const newversion = selectedApp.versions.find(
(tmpApp) => tmpApp.version == event.target.value
);
console.log("NEWVERSION: ", newversion);
if (newversion !== undefined && newversion !== null) {
getApp(newversion.id, true);
@@ -2408,6 +2475,7 @@ const ParsedAction = (props) => {
if (param.value.includes(baselabel)) {
//if (param.value.toLowerCase().includes(baselabel)) {
console.log("FOUND: ", param);
workflow.actions[key].parameters[subkey].value.replaceAll(
baselabel,
e.target.value
@@ -2497,6 +2565,9 @@ const ParsedAction = (props) => {
<Typography>Authentication</Typography>
<div style={{ display: "flex" }}>
<Select
MenuProps={{
disableScrollLock: true,
}}
labelId="select-app-auth"
value={
Object.getOwnPropertyNames(
@@ -2508,6 +2579,7 @@ const ParsedAction = (props) => {
SelectDisplayProps={{
style: {
marginLeft: 10,
maxWidth: 250,
},
}}
fullWidth
@@ -2581,6 +2653,7 @@ const ParsedAction = (props) => {
>
<IconButton
color="primary"
variant="outlined"
style={{}}
onClick={() => {
setAuthenticationModalOpen(true);
@@ -2597,6 +2670,9 @@ const ParsedAction = (props) => {
<div style={{ marginTop: "20px" }}>
<Typography>Environment</Typography>
<Select
MenuProps={{
disableScrollLock: true,
}}
value={
selectedActionEnvironment === undefined ||
selectedActionEnvironment.Name === undefined
@@ -2651,6 +2727,9 @@ const ParsedAction = (props) => {
<div style={{ marginTop: "20px" }}>
<Typography>Set execution variable (optional)</Typography>
<Select
MenuProps={{
disableScrollLock: true,
}}
value={
selectedAction.execution_variable !== undefined
? selectedAction.execution_variable.name
@@ -2843,6 +2922,9 @@ const ParsedAction = (props) => {
{/*setNewSelectedAction !== undefined ?
<Select
MenuProps={{
disableScrollLock: true,
}}
value={selectedAction.name}
fullWidth
onChange={setNewSelectedAction}
+12 -1
View File
@@ -23,6 +23,7 @@ import {
import { useTheme } from '@material-ui/core/styles';
import { validateJson } from "../views/Workflows.jsx";
import ReactJson from "react-json-view";
import PaperComponent from "../components/PaperComponent.jsx"
import CodeMirror from '@uiw/react-codemirror';
import 'codemirror/keymap/sublime';
@@ -89,16 +90,24 @@ const CodeEditor = (props) => {
return (
<Dialog
disableEnforceFocus={true}
hideBackdrop={true}
disableBackdropClick={true}
open={expansionModalOpen}
onClose={() => {
//setExpansionModalOpen(false)
console.log("In closer")
changeActionParameterCodeMirror({target: {value: ""}}, fieldCount, localcodedata)
}}
PaperComponent={PaperComponent}
aria-labelledby="draggable-dialog-title"
PaperProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
minWidth: 600,
padding: 25,
border: theme.palette.defaultBorder,
zIndex: 10012,
},
}}
>
@@ -109,7 +118,9 @@ const CodeEditor = (props) => {
>
<div style={{display: "flex"}}>
<DialogTitle
id="draggable-dialog-title"
style={{
cursor: "move",
paddingBottom:20,
paddingLeft: 10,
}}
+287
View File
@@ -0,0 +1,287 @@
import React, { useState, useEffect, useLayoutEffect } from "react";
import theme from '../theme';
import {
Chip,
Typography,
Paper,
Avatar,
Grid,
Tooltip,
} from "@material-ui/core";
import {
AvatarGroup,
} from "@mui/material"
import {
Restore as RestoreIcon,
Edit as EditIcon,
BubbleChart as BubbleChartIcon,
MoreVert as MoreVertIcon,
} from '@material-ui/icons';
import { useNavigate, Link, useParams } from "react-router-dom";
const workflowActionStyle = {
display: "flex",
width: 160,
height: 44,
justifyContent: "space-between",
}
const paperAppStyle = {
minHeight: 130,
maxHeight: 130,
overflow: "hidden",
width: "100%",
color: "white",
backgroundColor: theme.palette.surfaceColor,
padding: "12px 12px 0px 15px",
borderRadius: 5,
display: "flex",
boxSizing: "border-box",
position: "relative",
}
const chipStyle = {
backgroundColor: "#3d3f43",
marginRight: 5,
paddingLeft: 5,
paddingRight: 5,
height: 28,
cursor: "pointer",
borderColor: "#3d3f43",
color: "white",
}
const WorkflowPaper = (props) => {
const { data } = props;
let navigate = useNavigate();
const [open, setOpen] = React.useState(false);
const [anchorEl, setAnchorEl] = React.useState(null);
const appGroup = data.action_references === undefined || data.action_references === null ? [] : data.action_references
//console.log("Workflow: ", data)
var boxColor = "#86c142";
var parsedName = data.name;
if (
parsedName !== undefined &&
parsedName !== null &&
parsedName.length > 20
) {
parsedName = parsedName.slice(0, 21) + "..";
}
const imageStyle = {
width: 24,
height: 24,
marginRight: 10,
border: "1px solid rgba(255,255,255,0.3)",
}
var image = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.image !== undefined && data.creator_info.image !== null && data.creator_info.image.length > 0 ? <Avatar alt={data.creator} src={data.creator_info.image} style={imageStyle}/> : <Avatar alt={"shuffle_image"} src={theme.palette.defaultImage} style={imageStyle}/>
const creatorname = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.username !== undefined && data.creator_info.username !== null && data.creator_info.username.length > 0 ? data.creator_info.username : ""
var orgName = "";
var orgId = "";
if ((data.objectID === undefined || data.objectID === null) && data.id !== undefined && data.id !== null) {
data.objectID = data.id
}
//console.log("IMG: ", data)
return (
<div style={{width: "100%", position: "relative",}}>
<Paper square style={paperAppStyle}>
<div
style={{
position: "absolute",
bottom: 1,
left: 1,
height: 12,
width: 12,
backgroundColor: boxColor,
borderRadius: "0 100px 0 0",
}}
/>
<Grid
item
style={{ display: "flex", flexDirection: "column", width: "100%" }}
>
<Grid item style={{ display: "flex", maxHeight: 34 }}>
<Tooltip title={`${creatorname}`} placement="bottom">
<div
style={{ cursor: data.creator_info !== undefined ? "pointer" : "inherit" }}
onClick={() => {
if (data.creator_info !== undefined) {
navigate("/creators/"+data.creator_info.username)
}
}}
>
{image}
</div>
</Tooltip>
<Tooltip title={`Edit ${data.name}`} placement="bottom">
<Typography
variant="body1"
style={{
marginBottom: 0,
paddingBottom: 0,
maxHeight: 30,
flex: 10,
}}
>
<Link
to={"/workflows/" + data.objectID}
style={{ textDecoration: "none", color: "inherit" }}
>
{parsedName}
</Link>
</Typography>
</Tooltip>
</Grid>
<Grid item style={workflowActionStyle}>
{appGroup.length > 0 ?
<div style={{display: "flex", marginTop: 8, }}>
<AvatarGroup max={4} style={{marginLeft: 5, maxHeight: 24,}}>
{appGroup.map((app, index) => {
return (
<div
key={index}
style={{
height: 24,
width: 24,
filter: "brightness(0.6)",
cursor: "pointer",
}}
onClick={() => {
navigate("/apps/"+app.id)
}}
>
<Tooltip color="primary" title={app.name} placement="bottom">
<Avatar alt={app.name} src={app.image_url} style={{width: 24, height: 24}}/>
</Tooltip>
</div>
)
})}
</AvatarGroup>
</div>
:
<Tooltip color="primary" title="Action amount" placement="bottom">
<span style={{ color: "#979797", display: "flex" }}>
<BubbleChartIcon
style={{ marginTop: "auto", marginBottom: "auto" }}
/>
<Typography
style={{
marginLeft: 5,
marginTop: "auto",
marginBottom: "auto",
}}
>
{data.actions === undefined || data.actions === null ? 1 : data.actions.length}
</Typography>
</span>
</Tooltip>
}
<Tooltip
color="primary"
title="Trigger amount"
placement="bottom"
>
<span
style={{ marginLeft: 15, color: "#979797", display: "flex" }}
>
<RestoreIcon
style={{
color: "#979797",
marginTop: "auto",
marginBottom: "auto",
}}
/>
<Typography
style={{
marginLeft: 5,
marginTop: "auto",
marginBottom: "auto",
}}
>
{data.triggers === undefined || data.triggers === null ? 1 : data.triggers.length}
</Typography>
</span>
</Tooltip>
<Tooltip color="primary" title="Subflows used" placement="bottom">
<span
style={{
marginLeft: 15,
display: "flex",
color: "#979797",
cursor: "pointer",
}}
onClick={() => {
}}
>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{
color: "#979797",
marginTop: "auto",
marginBottom: "auto",
}}
>
<path
d="M0 0H15V15H0V0ZM16 16H18V18H16V16ZM16 13H18V15H16V13ZM16 10H18V12H16V10ZM16 7H18V9H16V7ZM16 4H18V6H16V4ZM13 16H15V18H13V16ZM10 16H12V18H10V16ZM7 16H9V18H7V16ZM4 16H6V18H4V16Z"
fill="#979797"
/>
</svg>
<Typography
style={{
marginLeft: 5,
marginTop: "auto",
marginBottom: "auto",
}}
>
{0}
</Typography>
</span>
</Tooltip>
</Grid>
<Grid
item
style={{
justifyContent: "left",
overflow: "hidden",
marginTop: 5,
}}
>
{data.tags !== undefined && data.tags !== null
? data.tags.map((tag, index) => {
if (index >= 3) {
return null;
}
return (
<Chip
key={index}
style={chipStyle}
label={tag}
variant="outlined"
color="primary"
/>
);
})
: null}
</Grid>
</Grid>
</Paper>
</div>
)
}
export default WorkflowPaper
+1
View File
@@ -16,6 +16,7 @@ const theme = createMuiTheme({
surfaceColor: "#27292d",
inputColor: "#383B40",
borderRadius: 5,
defaultBorder: "1px solid rgba(255,255,255,0.3)",
jsonTheme: "brewer",
reactJsonStyle: {
borderRadius: 5,
+31 -12
View File
@@ -1154,8 +1154,8 @@ const Admin = (props) => {
})
.then((respdata) => {
if (respdata.length === 0) {
alert.error("Failed getting file");
return;
alert.error("Failed getting file. Is it deleted?");
return;
}
var blob = new Blob([respdata], {
@@ -3101,12 +3101,12 @@ const Admin = (props) => {
{fileNamespaces !== undefined &&
fileNamespaces !== null &&
fileNamespaces.length > 1 ? (
<FormControl>
<InputLabel id="input-namespace-label">Namespace</InputLabel>
<FormControl style={{minWidth: 150, maxWidth: 150,}}>
<InputLabel id="input-namespace-label">File Category</InputLabel>
<Select
labelId="input-namespace-select-label"
id="input-namespace-select-id"
style={{ color: "white", minWidth: 100, float: "right" }}
style={{ color: "white", minWidth: 150, maxWidth: 150, float: "right" }}
value={selectedNamespace}
onChange={(event) => {
console.log("CHANGE NAMESPACE: ", event.target);
@@ -3185,7 +3185,7 @@ const Admin = (props) => {
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItem key={index} style={{ backgroundColor: bgColor, maxHeight: 100, overflow: "hidden",}}>
<ListItemText
style={{
maxWidth: 225,
@@ -3637,8 +3637,8 @@ const Admin = (props) => {
style={{ minWidth: 125, maxWidth: 125, overflow: "hidden" }}
/>
<ListItemText
primary="Last Edited"
style={{ minWidth: 225, maxWidth: 225, overflow: "hidden" }}
primary="Created"
style={{ minWidth: 230, maxWidth: 230, overflow: "hidden" }}
/>
<ListItemText primary="Actions" />
</ListItem>
@@ -3650,7 +3650,26 @@ const Admin = (props) => {
bgColor = "#1f2023";
}
console.log("Auth data: ", data)
//console.log("Auth data: ", data)
if (data.type === "oauth2") {
data.fields = [
{
"key": "url",
"value": "Secret. Replaced during app execution!",
},
{
"key": "client_id",
"value": "Secret. Replaced during app execution!",
},
{
"key": "client_secret",
"value": "Secret. Replaced during app execution!",
},
{
"key": "scope",
"value": "Secret. Replaced during app execution!",
}]
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
@@ -3720,11 +3739,11 @@ const Admin = (props) => {
/>
<ListItemText
style={{
maxWidth: 225,
minWidth: 225,
maxWidth: 230,
minWidth: 230,
overflow: "hidden",
}}
primary={new Date(data.edited * 1000).toISOString()}
primary={new Date(data.created * 1000).toISOString()}
/>
<ListItemText>
<IconButton
File diff suppressed because it is too large Load Diff
+63 -27
View File
@@ -275,7 +275,6 @@ const AppCreator = (defaultprops) => {
const [urlPathQueries, setUrlPathQueries] = useState([]);
const [update, setUpdate] = useState("");
const [urlPathParameters] = useState([]);
const [firstrequest, setFirstrequest] = React.useState(true);
const [basedata, setBasedata] = React.useState({});
const [actions, setActions] = useState([]);
const [filteredActions, setFilteredActions] = useState([]);
@@ -343,16 +342,13 @@ const AppCreator = (defaultprops) => {
window.location.host === "shuffler.io";
useEffect(() => {
if (firstrequest) {
setFirstrequest(false);
if (window.location.pathname.includes("apps/edit")) {
setIsEditing(true);
handleEditApp();
} else {
checkQuery();
}
}
});
if (window.location.pathname.includes("apps/edit")) {
setIsEditing(true);
handleEditApp();
} else {
checkQuery();
}
}, []);
const handleEditApp = () => {
fetch(globalUrl + "/api/v1/apps/" + props.match.params.appid + "/config", {
@@ -484,10 +480,29 @@ const AppCreator = (defaultprops) => {
// Sets the data up as it should be at later points
// This is the data FROM the database, not what's being saved
const parseIncomingOpenapiData = (data) => {
console.log("Data: ", data)
var parsedDecoded = ""
try {
const decoded = base64_decode(data.openapi)
parsedDecoded = decoded
} catch (e) {
console.log("Failed JSON parsing: ", e)
parsedDecoded = data
}
if (data.openapi === null) {
alert.info("Failed to load OpenAPI for app. Please contact support if this persists.")
setIsAppLoaded(true);
return
}
console.log("Decoded: ", parsedDecoded)
const parsedapp =
data.openapi === undefined
data.openapi === undefined || data.openapi === null
? data
: JSON.parse(base64_decode(data.openapi));
: JSON.parse(parsedDecoded);
data = parsedapp.body === undefined ? parsedapp : parsedapp.body;
var jsonvalid = false;
@@ -618,6 +633,7 @@ const AppCreator = (defaultprops) => {
continue;
}
//console.log("METHOD: ", methodvalue)
var tmpname = methodvalue.summary;
if (
methodvalue.operationId !== undefined &&
@@ -628,7 +644,13 @@ const AppCreator = (defaultprops) => {
tmpname = methodvalue.operationId;
}
tmpname = tmpname.replaceAll(".", " ");
if (tmpname !== undefined && tmpname !== null) {
tmpname = tmpname.replaceAll(".", " ");
}
if ((tmpname === undefined || tmpname === null) && methodvalue.description !== undefined && methodvalue.description !== null && methodvalue.description.length > 0) {
tmpname = methodvalue.description.replaceAll(".", " ").replaceAll("_", " ")
}
var newaction = {
name: tmpname,
@@ -739,9 +761,9 @@ const AppCreator = (defaultprops) => {
var newbody = {};
// Can handle default, required, description and type
for (var propkey in retRef.properties) {
const parsedkey = propkey
.replaceAll(" ", "_")
.toLowerCase();
console.log("replace: ", propkey)
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase();
newbody[parsedkey] = "${" + parsedkey + "}";
}
@@ -885,9 +907,8 @@ const AppCreator = (defaultprops) => {
) {
var newbody = {};
for (var propkey in parameter.properties) {
const parsedkey = propkey
.replaceAll(" ", "_")
.toLowerCase();
console.log("propkey2: ", propkey)
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase();
if (parameter.properties[propkey].type === undefined) {
console.log(
"Skipping (4): ",
@@ -1022,9 +1043,8 @@ const AppCreator = (defaultprops) => {
) {
var newbody = {};
for (var propkey in parameter.properties) {
const parsedkey = propkey
.replaceAll(" ", "_")
.toLowerCase();
console.log("propkey3: ", propkey)
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase();
if (
parameter.properties[propkey].type === undefined
) {
@@ -1112,9 +1132,8 @@ const AppCreator = (defaultprops) => {
) {
var newbody = {};
for (var propkey in parameter.properties) {
const parsedkey = propkey
.replaceAll(" ", "_")
.toLowerCase();
console.log("propkey4: ", propkey)
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase();
if (
parameter.properties[propkey].type ===
undefined
@@ -1197,6 +1216,7 @@ const AppCreator = (defaultprops) => {
) {
var newbody = {};
for (var propkey in parameter.properties) {
console.log("propkey5: ", propkey)
const parsedkey = propkey
.replaceAll(" ", "_")
.toLowerCase();
@@ -1606,6 +1626,15 @@ const AppCreator = (defaultprops) => {
id: props.match.params.appid,
};
if (isEditing === false) {
var urlParams = new URLSearchParams(window.location.search);
if (urlParams !== undefined && urlParams !== null && urlParams.has("id")) {
data.id = urlParams.get("id")
}
//id: props.match.params.appid,
}
if (basedata.info !== undefined && basedata.info.contact !== undefined) {
data.info["contact"] = basedata.info.contact;
} else if (contact === "") {
@@ -2068,6 +2097,7 @@ const AppCreator = (defaultprops) => {
return;
}
console.log("Paramname: ", parameterName)
var newparamName = parameterName.replaceAll('"', "");
newparamName = newparamName.replaceAll("'", "");
@@ -2077,6 +2107,9 @@ const AppCreator = (defaultprops) => {
name: newparamName,
description: refreshUrl,
}
console.log("Full auth component: ", data.components.securitySchemes["ApiKeyAuth"])
} else if (authenticationOption === "Bearer auth") {
data.components.securitySchemes["BearerAuth"] = {
type: "http",
@@ -2098,6 +2131,7 @@ const AppCreator = (defaultprops) => {
scheme: "basic",
};
} else if (authenticationOption === "Oauth2") {
console.log("oauth2: ", parameterName)
var newparamName = parameterName.replaceAll('"', "");
newparamName = newparamName.replaceAll("'", "");
@@ -5029,6 +5063,8 @@ const AppCreator = (defaultprops) => {
marginTop: "5px",
marginRight: "15px",
backgroundColor: inputColor,
maxHeight: 250,
overflow: "auto",
}}
fullWidth={true}
type="name"
@@ -5228,7 +5264,7 @@ const AppCreator = (defaultprops) => {
);
const loadedCheck =
isLoaded && isAppLoaded && !firstrequest ? (
isLoaded && isAppLoaded ? (
<div>
<div style={bodyDivStyle}>{landingpageDataBrowser}</div>
{newActionModal}
+28 -15
View File
@@ -225,7 +225,11 @@ const Apps = (props) => {
document.title = "Shuffle - Apps";
if (!isLoggedIn && isLoaded) {
navigate("/login")
if (isCloud) {
navigate("/search?tab=apps")
} else {
navigate("/login")
}
}
setFirstrequest(false);
@@ -287,9 +291,9 @@ const Apps = (props) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
if (isCloud) {
window.location.pathname = "/search";
}
//if (isCloud) {
// window.location.pathname = "/search";
//}
}
return response.json();
@@ -515,7 +519,11 @@ const Apps = (props) => {
valid = "false";
}
if (data.actions === null || data.actions.length === 0) {
if (data.actions === undefined || data.actions === null) {
data.actions = []
}
if (data === undefined || data.actions === undefined || data.actions === null || data.actions.length === 0) {
valid = "false";
}
@@ -717,6 +725,8 @@ const Apps = (props) => {
</Tooltip>
) : null;
// FIXME: Add /apps/new?id=<PUBLIC> to allow for changes of the original
// Should always reference the original ID.
var editButton =
selectedApp.activated &&
selectedApp.private_id !== undefined &&
@@ -737,10 +747,10 @@ const Apps = (props) => {
) : null;
//var editNewButton = editButton === null ?
var editNewButton = null
/*
<Link to={editUrl} style={{ textDecoration: "none" }}>
<Tooltip title={"Add your version"}>
console.log("User, & genrrated, activate: ", props.userdata, selectedApp.generated, selectedApp.activated)
var editNewButton = selectedApp.generated && selectedApp.activated && props.userdata.id !== selectedApp.owner ?
<Link to={activateUrl} style={{ textDecoration: "none" }}>
<Tooltip title={"Edit this public app to your liking"}>
<Button
variant="contained"
component="label"
@@ -751,9 +761,9 @@ const Apps = (props) => {
</Button>
</Tooltip>
</Link>
*/
: null
const activateButton =
const activateButton =
selectedApp.generated && !selectedApp.activated ? (
<div>
<Link to={activateUrl} style={{ textDecoration: "none" }}>
@@ -993,15 +1003,14 @@ const Apps = (props) => {
) : null}
{activateButton}
{editNewButton}
{(props.userdata !== undefined &&
(props.userdata.role === "admin" ||
props.userdata.id === selectedApp.owner ||
selectedApp.owner === ""
)) ||
!selectedApp.generated ? (
)) || !selectedApp.generated ? (
<div>
{editButton}
{editNewButton}
{downloadButton}
{deleteButton}
</div>
@@ -1817,7 +1826,11 @@ const Apps = (props) => {
if (responseJson.success) {
alert.success("Successfully updated app configuration");
} else {
alert.error("Error updating app configuration");
if (responseJson.reason !== undefined && responseJson.reason !== null) {
alert.error("Error: "+responseJson.reason);
} else {
alert.error("Error updating app configuration");
}
}
})
.catch((error) => {
+524 -185
View File
@@ -1,46 +1,327 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { useInterval } from "react-powerhooks";
// nodejs library that concatenates classes
import classNames from "classnames";
import theme from '../theme';
// react plugin used to create charts
import { Line, Bar } from "react-chartjs-2";
//import { Line, Bar } from "react-chartjs-2";
import { useAlert } from "react-alert";
// https://demos.creative-tim.com/black-dashboard-react/?ref=appseed#/admin/dashboard
// reactstrap components
import {
Button,
ButtonGroup,
Card,
CardHeader,
CardBody,
CardTitle,
DropdownToggle,
DropdownMenu,
DropdownItem,
UncontrolledDropdown,
Label,
FormGroup,
Input,
Table,
Row,
Col,
UncontrolledTooltip,
} from "reactstrap";
Tooltip,
IconButton,
Typography,
Grid,
Paper,
Chip,
} from "@material-ui/core";
import {
Close as CloseIcon,
DoneAll as DoneAllIcon,
} from "@material-ui/icons";
import WorkflowPaper from "../components/WorkflowPaper.jsx"
// core components
import {
chartExample1,
chartExample2,
chartExample3,
chartExample4,
} from "../charts.js";
//import {
// chartExample1,
// chartExample2,
// chartExample3,
// chartExample4,
//} from "../charts.js";
import {
RadialBarChart,
RadialAreaChart,
RadialAxis,
StackedBarSeries,
TooltipArea,
ChartTooltip,
TooltipTemplate,
RadialAreaSeries,
RadialPointSeries,
RadialArea,
RadialLine,
TreeMap,
TreeMapSeries,
TreeMapLabel,
TreeMapRect,
} from 'reaviz';
const UsecaseListComponent = ({keys, isCloud}) => {
const [expandedIndex, setExpandedIndex] = useState(-1);
const [expandedItem, setExpandedItem] = useState(-1);
if (keys === undefined || keys === null || keys.length === 0) {
return null
}
return (
<div style={{marginTop: 25, minHeight: 1000,}}>
<Typography variant="h1">
Shuffle usecases
</Typography>
<Typography variant="body1">
Usecases in Shuffle are divided into {keys.length} type{keys.length === 1 ? "" : "s"}.
</Typography>
{keys.map((usecase, index) => {
return (
<div key={index} style={{marginTop: index === 0 ? 50 : 100}}>
<Typography variant="h6">
{usecase.name}
</Typography>
<Grid container spacing={3} style={{marginTop: 25}}>
{usecase.list.map((subcase, subindex) => {
if (subcase.matches === undefined || subcase.matches === null) {
subcase.matches = []
}
const selectedItem = subindex === expandedItem && index === expandedIndex
const finished = subcase.matches.length > 0
//const backgroundColor = selectedItem ? "inherit" : finished ? "inherit" : usecase.color
const backgroundColor = "inherit"
const itemBorder = `${selectedItem ? "3px" : expandedItem >= 0 ? "0px" : "1px"} solid ${usecase.color}`
return (
<Grid item xs={selectedItem ? 12 : 4} key={subindex} style={{minHeight: 110,}} onClick={() => {
if (selectedItem) {
} else {
setExpandedIndex(index)
setExpandedItem(subindex)
}
}}>
<Paper style={{padding: "30px 30px 30px 30px", minHeight: 75, cursor: !selectedItem ? "pointer" : "default", border: itemBorder, backgroundColor: backgroundColor,}} onClick={() => {
console.log("Clicked: ", subcase)
}}>
{!selectedItem ?
<div style={{textAlign: "left", position: "relative",}}>
<Typography variant="h6">
<b>{subcase.name}</b>
</Typography>
{finished ?
<Tooltip
title="A workflow has been assigned"
placement="top"
>
<IconButton
style={{ position: "absolute", top: -20, right: -20}}
onClick={(e) => {
}}
>
<DoneAllIcon style={{ color: usecase.color }} />
</IconButton>
</Tooltip>
: null}
</div>
:
<div style={{textAlign: "left", position: "relative",}}>
<Typography variant="h6">
<b>{subcase.name}</b>
</Typography>
<Typography variant="body2">
Description: {subcase.description}
</Typography>
<Tooltip
title="Close window"
placement="top"
style={{ zIndex: 10011 }}
>
<IconButton
style={{ position: "absolute", top: 0, right: 0}}
onClick={(e) => {
setExpandedItem(-1)
setExpandedIndex(-1)
}}
>
<CloseIcon style={{ color: "white" }} />
</IconButton>
</Tooltip>
<div style={{marginTop: 25, display: "flex", minHeight: 400, maxHeight: 400, }}>
<img
alt={subcase.name}
src={"/images/detectionframework.png"}
style={{
flex: 1,
height: 400,
width: 400,
borderRadius: theme.palette.borderRadius,
border: "1px solid rgba(255,255,255,0.3)",
}}
/>
<div style={{flex: 1, marginLeft: 10, textAlign: "center",}}>
<Typography variant="h6">
Your workflow{subcase.matches.length === 1 ? "" : "s"} ({subcase.matches.length})
</Typography>
{subcase.matches.length > 0 ?
<Grid container xs={3} style={{maxWidth: 325, margin: "auto", marginTop: 10, itemAlign: "center", }}>
{subcase.matches.map((workflow, workflowindex) => {
return (
<Grid index={workflowindex} xs={12}>
<WorkflowPaper key={workflowindex} data={workflow} />
</Grid>
)
})}
</Grid>
:
<div>
<Typography variant="body1" color="textSecondary">
No workflow selected yet.
</Typography>
</div>
}
{isCloud !== false ?
<Typography variant="h6">
Public workflows
</Typography>
: null}
</div>
</div>
</div>
}
</Paper>
</Grid>
)
})}
</Grid>
</div>
)
})}
</div>
)
}
const TreeChart = ({keys}) => {
const [hovered, setHovered] = useState("");
return (
<div style={{cursor: "pointer",}} onClick={() => {
console.log("Click: ", hovered)
}}>
<TreeMap
id="all_categories"
data={keys}
margins={10}
series={
<TreeMapSeries
colorScheme={(info) => {
return info.color
}}
label={
<TreeMapLabel
fontSize="15px"
fill="#ffffff"
wrap={false}
/>
}
rect={
<TreeMapRect
cursor="pointer"
animated={true}
onClick={(event) => {
console.log("Click: ", event)
}}
/>
}
/>
}
/>
</div>
)
//axis={<RadialAxis type="category" />}
}
const RadialChart = ({keys, setSelectedCategory}) => {
const [hovered, setHovered] = useState("");
return (
<div style={{cursor: "pointer",}} onClick={() => {
console.log("Click: ", hovered)
if (setSelectedCategory !== undefined) {
setSelectedCategory(hovered)
}
}}>
<RadialAreaChart
id="workflow_categories"
height={500}
width={500}
data={keys}
axis={<RadialAxis type="category" />}
series={
<RadialAreaSeries
interpolation="smooth"
colorScheme={(colorInput) => {
return '#f86a3e'
}}
animated={false}
id="workflow_series_id"
style={{cursor: "pointer",}}
line={
<RadialLine
color={"#000000"}
data={(data, color) => {
console.log("INFO: ", data, color)
return (
null
)
}}
/>
}
tooltip={
<TooltipArea
color={"#000000"}
style={{
backgroundColor: "red",
}}
isRadial={true}
onValueEnter={(event) => {
if (hovered !== event.value.x) {
setHovered(event.value.x)
}
}}
tooltip={
<ChartTooltip
followCursor={true}
modifiers={{
offset: '5px, 5px'
}}
content={(data, color) => {
return (
<div style={{borderRadius: theme.palette.borderRadius, backgroundColor: theme.palette.inputColor, border: "1px solid rgba(255,255,255,0.3)", color: "white", padding: 5, cursor: "pointer",}}>
<Typography variant="body1">
{data.x}
</Typography>
</div>
)
/*
<TooltipTemplate
color={"#ffffff"}
value={{
x: data.x,
}}
/>
)
*/
}
}
/>
}
/>
}
/>
}
/>
</div>
)
//axis={<RadialAxis type="category" />}
}
// This is the start of a dashboard that can be used.
// What data do we fill in here? Idk
const Dashboard = (props) => {
const { globalUrl } = props;
const { globalUrl, isLoggedIn } = props;
const alert = useAlert();
const [bigChartData, setBgChartData] = useState("data1");
const [dayAmount, setDayAmount] = useState(7);
@@ -48,11 +329,172 @@ const Dashboard = (props) => {
const [stats, setStats] = useState({});
const [changeme, setChangeme] = useState("");
const [statsRan, setStatsRan] = useState(false);
const [keys, setKeys] = useState([])
const [treeKeys, setTreeKeys] = useState([])
document.title = "Shuffle - dashboard";
const [selectedUsecaseCategory, setSelectedUsecaseCategory] = useState("");
const [selectedUsecases, setSelectedUsecases] = useState([]);
const [usecases, setUsecases] = useState([]);
const [workflows, setWorkflows] = useState([]);
const isCloud =
window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io";
const getAvailableWorkflows = () => {
fetch(globalUrl + "/api/v1/workflows", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
fetchUsecases()
console.log("Status not 200 for workflows :O!: ", response.status);
return;
}
return response.json();
})
.then((responseJson) => {
fetchUsecases(responseJson)
console.log("Resp: ", responseJson)
if (responseJson !== undefined) {
//setWorkflows(responseJson);
//fetchUsecases(responseJson)
}
})
.catch((error) => {
fetchUsecases()
//alert.error(error.toString());
});
}
useEffect(() => {
console.log("Changed: ", selectedUsecaseCategory)
if (selectedUsecaseCategory.length === 0) {
setSelectedUsecases(usecases)
} else {
const foundUsecase = usecases.find(data => data.name === selectedUsecaseCategory)
if (foundUsecase !== undefined && foundUsecase !== null) {
console.log("FOUND: ", foundUsecase)
setSelectedUsecases([foundUsecase])
}
}
}, [selectedUsecaseCategory])
document.title = "Shuffle - usecases";
var dayGraphLabels = [60, 80, 65, 130, 80, 105, 90, 130, 70, 115, 60, 130];
var dayGraphData = [60, 80, 65, 130, 80, 105, 90, 130, 70, 115, 60, 130];
const handleKeysetting = (categorydata) => {
var allCategories = []
var treeCategories = []
for (key in categorydata) {
const category = categorydata[key]
allCategories.push({"key": category.name, "data": category.list.length, "color": category.color})
treeCategories.push({"key": category.name, "data": 100, "color": category.color,})
for (var subkey in category.list) {
treeCategories.push({"key": category.list[subkey].name, "data": 20, "color": category.color})
}
}
setKeys(allCategories)
setTreeKeys(treeCategories)
}
const fetchUsecases = (workflows) => {
fetch(globalUrl + "/api/v1/workflows/usecases", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for usecases");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success !== false) {
console.log("Usecases: ", responseJson)
if (workflows !== undefined && workflows !== null && workflows.length > 0) {
console.log("Got workflows: ", workflows)
var categorydata = responseJson
var newcategories = []
for (var key in categorydata) {
var category = categorydata[key]
category.matches = []
for (var subcategorykey in category.list) {
var subcategory = category.list[subcategorykey]
subcategory.matches = []
for (var workflowkey in workflows) {
const workflow = workflows[workflowkey]
if (workflow.usecase_ids !== undefined && workflow.usecase_ids !== null) {
for (var usecasekey in workflow.usecase_ids) {
if (workflow.usecase_ids[usecasekey].toLowerCase() === subcategory.name.toLowerCase()) {
console.log("Got match: ", workflow.usecase_ids[usecasekey])
category.matches.push({
"workflow": workflow.id,
"category": subcategory.name,
})
subcategory.matches.push(workflow)
break
}
}
}
if (subcategory.matches.length > 0) {
break
}
}
}
newcategories.push(category)
}
console.log("Categories: ", newcategories)
if (newcategories !== undefined && newcategories !== null && newcategories.length > 0) {
handleKeysetting(newcategories)
setUsecases(newcategories)
setSelectedUsecases(newcategories)
} else {
handleKeysetting(responseJson)
setUsecases(responseJson)
setSelectedUsecases(responseJson)
}
} else {
handleKeysetting(responseJson)
setUsecases(responseJson)
setSelectedUsecases(responseJson)
}
}
})
.catch((error) => {
//alert.error("ERROR: " + error.toString());
console.log("ERROR: " + error.toString());
});
};
useEffect(() => {
getAvailableWorkflows()
//fetchUsecases()
}, []);
const fetchdata = (stats_id) => {
fetch(globalUrl + "/api/v1/stats/" + stats_id, {
method: "GET",
@@ -76,7 +518,8 @@ const Dashboard = (props) => {
setChangeme(stats_id);
})
.catch((error) => {
alert.error("ERROR: " + error.toString());
//alert.error("ERROR: " + error.toString());
console.log("ERROR: " + error.toString());
});
};
@@ -201,8 +644,8 @@ const Dashboard = (props) => {
if (firstRequest) {
console.log("HELO");
setFirstRequest(false);
start();
runUpdate();
//start();
//runUpdate();
} else if (!statsRan) {
// FIXME: Run this under runUpdate schedule?
// 1. Fix labels in dayGraphy.data
@@ -301,158 +744,54 @@ const Dashboard = (props) => {
) : null;
const data = (
<div className="content">
<div className="content" style={{width: 1000, margin: "auto", paddingBottom: 200, textAlign: "center",}}>
<div style={{width: 500, margin: "auto"}}>
{keys.length > 0 ?
<RadialChart keys={keys} setSelectedCategory={setSelectedUsecaseCategory} />
: null}
</div>
{usecases !== null && usecases !== undefined && usecases.length > 0 ?
<div style={{ display: "flex", marginLeft: 120,}}>
{usecases.map((usecase, index) => {
return (
<Chip
key={usecase.name}
style={{
backgroundColor: selectedUsecaseCategory === usecase.name ? usecase.color : theme.palette.surfaceColor,
marginRight: 10,
paddingLeft: 5,
paddingRight: 5,
height: 28,
cursor: "pointer",
border: `1px solid ${usecase.color}`,
color: "white",
}}
label={`${usecase.name} (${usecase.list.length})`}
onClick={() => {
console.log("Clicked: ", usecase.name)
if (selectedUsecaseCategory === usecase.name) {
setSelectedUsecaseCategory("")
} else {
setSelectedUsecaseCategory(usecase.name)
}
//addFilter(usecase.name.slice(3,usecase.name.length))
}}
variant="outlined"
color="primary"
/>
)
})}
</div>
: null}
<UsecaseListComponent keys={selectedUsecases} isCloud={isCloud} />
{treeKeys.length > 0 ?
<TreeChart keys={treeKeys} />
: null}
{newdata}
<Row>
<Col xs="12">
<div className="chart-area">
<Line data={dayGraph.data} options={dayGraph.options} />
</div>
</Col>
<Col xs="12">
<Card className="card-chart">
<CardHeader>
<Row>
<Col className="text-left" sm="6">
<h5 className="card-category">Total Shipments</h5>
<CardTitle tag="h2">Workflows</CardTitle>
</Col>
<Col sm="6">
<ButtonGroup
className="btn-group-toggle float-right"
data-toggle="buttons"
>
<Button
tag="label"
className={classNames("btn-simple", {
active: bigChartData === "data1",
})}
color="info"
id="0"
size="sm"
onClick={() => setBgChartData("data1")}
>
<input
defaultChecked
className="d-none"
name="options"
type="radio"
/>
<span className="d-none d-sm-block d-md-block d-lg-block d-xl-block">
Accounts
</span>
<span className="d-block d-sm-none">
<i className="tim-icons icon-single-02" />
</span>
</Button>
<Button
color="info"
id="1"
size="sm"
tag="label"
className={classNames("btn-simple", {
active: bigChartData === "data2",
})}
onClick={() => setBgChartData("data2")}
>
<input className="d-none" name="options" type="radio" />
<span className="d-none d-sm-block d-md-block d-lg-block d-xl-block">
Purchases
</span>
<span className="d-block d-sm-none">
<i className="tim-icons icon-gift-2" />
</span>
</Button>
<Button
color="info"
id="2"
size="sm"
tag="label"
className={classNames("btn-simple", {
active: bigChartData === "data3",
})}
onClick={() => setBgChartData("data3")}
>
<input className="d-none" name="options" type="radio" />
<span className="d-none d-sm-block d-md-block d-lg-block d-xl-block">
Sessions
</span>
<span className="d-block d-sm-none">
<i className="tim-icons icon-tap-02" />
</span>
</Button>
</ButtonGroup>
</Col>
</Row>
</CardHeader>
<CardBody>
<div className="chart-area">
<Line
data={chartExample1[bigChartData]}
options={chartExample1.options}
/>
</div>
</CardBody>
</Card>
</Col>
</Row>
<Row>
<Col lg="4">
<Card className="card-chart">
<CardHeader>
<h5 className="card-category">Total Shipments</h5>
<CardTitle tag="h3">
<i className="tim-icons icon-bell-55 text-info" /> 763,215
</CardTitle>
</CardHeader>
<CardBody>
<div className="chart-area">
<Line
data={chartExample2.data}
options={chartExample2.options}
/>
</div>
</CardBody>
</Card>
</Col>
<Col lg="4">
<Card className="card-chart">
<CardHeader>
<h5 className="card-category">Daily Sales</h5>
<CardTitle tag="h3">
<i className="tim-icons icon-delivery-fast text-primary" />{" "}
3,500
</CardTitle>
</CardHeader>
<CardBody>
<div className="chart-area">
<Bar
data={chartExample3.data}
options={chartExample3.options}
/>
</div>
</CardBody>
</Card>
</Col>
<Col lg="4">
<Card className="card-chart">
<CardHeader>
<h5 className="card-category">Completed Tasks</h5>
<CardTitle tag="h3">
<i className="tim-icons icon-send text-success" /> 12,100K
</CardTitle>
</CardHeader>
<CardBody>
<div className="chart-area">
<Line
data={chartExample4.data}
options={chartExample4.options}
/>
</div>
</CardBody>
</Card>
</Col>
</Row>
</div>
);
+9 -3
View File
@@ -102,7 +102,7 @@ const Docs = (defaultprops) => {
maxHeight: "83vh",
overflowX: "hidden",
overflowY: "auto",
zIndex: 10003,
zIndex: 1000,
};
const fetchDocList = () => {
@@ -141,6 +141,11 @@ const Docs = (defaultprops) => {
setData(responseJson.reason);
document.title = "Shuffle " + docId + " documentation";
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.includes("404: Not Found")) {
navigate("/docs")
return
}
if (responseJson.meta !== undefined) {
setSelectedMeta(responseJson.meta);
}
@@ -352,7 +357,7 @@ const Docs = (defaultprops) => {
}
function Img(props) {
return <img style={{ maxWidth: "100%" }} alt={props.alt} src={props.src} />;
return <img style={{ borderRadius: theme.palette.borderRadius, maxWidth: "100%", marginTop: 15, marginBottom: 15, }} alt={props.alt} src={props.src} />;
}
function CodeHandler(props) {
@@ -517,7 +522,8 @@ const Docs = (defaultprops) => {
const newname =
item.charAt(0).toUpperCase() +
item.substring(1).split("_").join(" ").split("-").join(" ");
const itemMatching =
const itemMatching = props.match.params.key === undefined ? false :
props.match.params.key.toLowerCase() === item.toLowerCase();
//const [tocLines, setTocLines] = React.useState([]);
return (
+103 -384
View File
@@ -2,6 +2,7 @@ import React, { useEffect, useContext } from "react";
import { makeStyles } from "@material-ui/core/styles";
import { useTheme } from "@material-ui/core/styles";
import ReactGA from 'react-ga';
import SecurityFramework from '../components/SecurityFramework.jsx';
import {
@@ -67,7 +68,7 @@ import { DataGrid, GridToolbar } from "@material-ui/data-grid";
//import JSONPrettyMon from 'react-json-pretty/dist/monikai'
import Dropzone from "../components/Dropzone";
import { Link } from "react-router-dom";
import { useNavigate, Link, useParams } from "react-router-dom";
import { useAlert } from "react-alert";
import ChipInput from "material-ui-chip-input";
import { v4 as uuidv4 } from "uuid";
@@ -109,263 +110,7 @@ const useStyles = makeStyles((theme) => ({
},
}));
// Takes an action in Shuffle and
// Returns information about the icon, the color etc to be used
// This can be used for actions of all types
export const GetIconInfo = (action) => {
// Finds the icon based on the action. Should be verbs.
const iconList = [
{ key: "cache_add", values: ["set_cache"] },
{ key: "cache_get", values: ["get_cache"] },
{ key: "filter", values: ["filter", "route", "router"] },
{ key: "merge", values: ["join", "merge"] },
{
key: "search",
values: ["search", "find", "locate", "index", "analyze", "anal", "match", "check cache", "check", "verify", "validate"],
},
{ key: "list", values: ["list", "head", "options"] },
{
key: "download",
values: [
"capture",
"get",
"download",
"return",
"hello_world",
"curl",
"request",
"export",
"preview",
],
},
{ key: "add", values: ["add", "accept", ] },
{ key: "delete", values: ["delete", "remove", "clear", "clean", "dismiss",] },
{
key: "send",
values: [
"send",
"dispatch",
"mail",
"forward",
"post",
"submit",
"mark",
"set",
"release",
],
},
{
key: "repeat",
values: ["repeat", "retry", "pause", "skip", "copy", "replicat", "demo", ],
},
{ key: "execute", values: ["execute", "run", "play", "raise"] },
{ key: "extract", values: ["extract", "unpack", "decompress", "open"] },
{ key: "inflate", values: ["inflate", "pack", "compress"] },
{
key: "edit",
values: [
"modify",
"update",
"create",
"edit",
"put",
"patch",
"change",
"replace",
"conver",
"map",
"format",
"escape",
"describe",
],
},
{
key: "compare",
values: ["compare", "convert", "to", "filter", "translate", "parse"],
},
{ key: "close", values: ["close", "stop", "cancel", "block"] },
];
var selectedKey = "";
if (action.name === undefined || action.name === null) {
} else {
const actionname = action.name.toLowerCase();
for (var key in iconList) {
//console.log(iconList[key], actionname)
const found = iconList[key].values.find((value) =>
actionname.includes(value)
);
if (found !== null && found !== undefined) {
selectedKey = iconList[key].key;
break;
}
}
}
// Some of these are manually parsed or created instead of material ui
//M8 0C3.58 0 0 1.79 0 4C0 6.21 3.58 8 8 8C12.42 8 16 6.21 16 4C16 1.79 12.42 0 8 0ZM0 6V9C0 11.21 3.58 13 8 13C12.42 13 16 11.21 16 9V6C16 8.21 12.42 10 8 10C3.58 10 0 8.21 0 6ZM0 11V14C0 16.21 3.58 18 8 18C9.41 18 10.79 17.81 12 17.46V14.46C10.79 14.81 9.41 15 8 15C3.58 15 0 13.21 0 11ZM17 11V14H14V16H17V19H19V16H22V14H19V11
//https://www.figma.com/file/uCfnMs5w6wnLx6ehPHEV74/Figma-Material-Design-System-v3_0?node-id=834%3A21
//COLORS: https://www.pinterest.co.uk/pin/326299935499972946/
const defaultColor = "#f76b1c";
const defaultGradient = ["#fad961", "#f76b1c"];
const parsedIcons = {
cache_add: {
icon: "M11 3C6.58 3 3 4.79 3 7C3 9.21 6.58 11 11 11C15.42 11 19 9.21 19 7C19 4.79 15.42 3 11 3ZM3 9V12C3 14.21 6.58 16 11 16C15.42 16 19 14.21 19 12V9C19 11.21 15.42 13 11 13C6.58 13 3 11.21 3 9ZM3 14V17C3 19.21 6.58 21 11 21C12.41 21 13.79 20.81 15 20.46V17.46C13.79 17.81 12.41 18 11 18C6.58 18 3 16.21 3 14ZM20 14V17H17V19H20V22H22V19H25V17H22V14",
iconColor: "white",
iconBackgroundColor: "#8acc3f",
originalIcon: "",
fillGradient: ["#8acc3f", "#459622"],
},
cache_get: {
icon: "M12 2C7.58 2 4 3.79 4 6C4 8.06 7.13 9.74 11.15 9.96C12.45 8.7 14.19 8 16 8C16.8 8 17.59 8.14 18.34 8.41C19.37 7.74 20 6.91 20 6C20 3.79 16.42 2 12 2ZM4 8V11C4 12.68 6.08 14.11 9 14.71C9.06 13.7 9.32 12.72 9.77 11.82C6.44 11.34 4 9.82 4 8ZM15.93 9.94C14.75 9.95 13.53 10.4 12.46 11.46C8.21 15.71 13.71 22.5 18.75 19.17L23.29 23.71L24.71 22.29L20.17 17.75C22.66 13.97 19.47 9.93 15.93 9.94ZM15.9 12C17.47 11.95 19 13.16 19 15C19 15.7956 18.6839 16.5587 18.1213 17.1213C17.5587 17.6839 16.7956 18 16 18C13.33 18 12 14.77 13.88 12.88C14.47 12.29 15.19 12 15.9 12ZM4 13V16C4 18.05 7.09 19.72 11.06 19.95C10.17 19.07 9.54 17.95 9.22 16.74C6.18 16.17 4 14.72 4 13Z",
iconColor: "white",
iconBackgroundColor: "#8acc3f",
originalIcon: "",
fillGradient: ["#8acc3f", "#459622"],
},
repeat: {
icon: "M19 8l-4 4h3c0 3.31-2.69 6-6 6-1.01 0-1.97-.25-2.8-.7l-1.46 1.46C8.97 19.54 10.43 20 12 20c4.42 0 8-3.58 8-8h3l-4-4zM6 12c0-3.31 2.69-6 6-6 1.01 0 1.97.25 2.8.7l1.46-1.46C15.03 4.46 13.57 4 12 4c-4.42 0-8 3.58-8 8H1l4 4 4-4H6z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <CachedIcon />,
},
add: {
icon: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <AddIcon />,
},
edit: {
icon: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <EditIcon />,
},
filter: {
icon: "M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39c.51-.66.04-1.61-.79-1.61H5.04c-.83 0-1.3.95-.79 1.61z",
iconColor: "white",
iconBackgroundColor: "#f5515f",
originalIcon: "",
fillGradient: ["#f5515f", "#a1051d"],
},
merge: {
icon: "M17 20.41 18.41 19 15 15.59 13.59 17 17 20.41zM7.5 8H11v5.59L5.59 19 7 20.41l6-6V8h3.5L12 3.5 7.5 8z",
iconColor: "white",
iconBackgroundColor: "#f5515f",
originalIcon: "",
fillGradient: ["#f5515f", "#a1051d"],
},
compare: {
icon: "M10 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h5v2h2V1h-2v2zm0 15H5l5-6v6zm9-15h-5v2h5v13l-5-6v9h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <CompareIcon />,
},
extract: {
icon: "M3 3h18v2H3z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <MaximizeIcon />,
},
inflate: {
icon: "M6 19h12v2H6z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <MinimizeIcon />,
},
list: {
icon: "M3 9h14V7H3v2zm0 4h14v-2H3v2zm0 4h14v-2H3v2zm16 0h2v-2h-2v2zm0-10v2h2V7h-2zm0 6h2v-2h-2v2z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <TocIcon />,
},
execute: {
icon: "M8 5v14l11-7z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <PlayArrowIcon />,
},
delete: {
icon: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z",
iconColor: "white",
iconBackgroundColor: "#03030e",
originalIcon: <DeleteIcon />,
fillGradient: ["#03030e", "#205d66"],
},
close: {
icon: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
iconColor: "white",
iconBackgroundColor: "#03030e",
originalIcon: <CloseIcon />,
fillGradient: ["#03030e", "#205d66"],
},
send: {
icon: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z",
iconColor: "white",
iconBackgroundColor: "#0373da",
originalIcon: <SendIcon />,
fillGradient: ["#0bc8bf", "#0373da"],
},
download: {
icon: "M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z",
iconColor: "white",
iconBackgroundColor: "#0373da",
originalIcon: <GetAppIcon />,
fillGradient: ["#0bc8bf", "#0373da"],
},
search: {
icon: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z",
iconColor: "white",
iconBackgroundColor: "green",
originalIcon: <SearchIcon />,
},
};
var selectedItem = parsedIcons[selectedKey];
if (selectedItem === undefined || selectedItem === null) {
return {
icon: "",
iconColor: "",
iconBackground: "black",
originalIcon: "",
};
}
if (selectedItem.fillGradient === undefined) {
selectedItem.fillGradient = defaultGradient;
selectedItem.iconBackgroundColor = defaultColor;
}
if (selectedItem.icon === "" || selectedItem.icon === undefined) {
console.log(
`MISSING PATH FOR ${selectedKey} (find in scope): `,
selectedItem.originalIcon.type.type
);
}
if (
(selectedItem.originalIcon === undefined ||
selectedItem.originalIcon === "") &&
selectedItem.icon !== "" &&
selectedItem.icon !== undefined
) {
const svg_pin = (
<svg
width={svgSize}
height={svgSize}
viewBox={`0 0 ${svgSize} ${svgSize}`}
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<path d={selectedItem.icon} fill={selectedItem.iconColor}></path>
</svg>
);
selectedItem.originalIcon = svg_pin;
}
return selectedItem;
};
const chipStyle = {
backgroundColor: "#3d3f43",
marginRight: 5,
@@ -377,62 +122,6 @@ const chipStyle = {
color: "white",
};
export const validateJson = (showResult) => {
//showResult = showResult.split(" None").join(" \"None\"")
showResult = showResult.split(" False").join(" false");
showResult = showResult.split(" True").join(" true");
var jsonvalid = true;
try {
if (!showResult.includes("{") && !showResult.includes("[")) {
jsonvalid = false;
}
} catch (e) {
showResult = showResult.split("'").join('"');
try {
if (!showResult.includes("{") && !showResult.includes("[")) {
jsonvalid = false;
}
} catch (e) {
jsonvalid = false;
}
}
var result = showResult;
try {
result = jsonvalid ? JSON.parse(showResult) : showResult;
} catch (e) {
////console.log("Failed parsing JSON even though its valid: ", e)
jsonvalid = false;
}
if (jsonvalid === false) {
if (typeof showResult === 'string') {
showResult = showResult.trim()
}
try {
var newstr = showResult.replaceAll("'", '"')
//console.log("Try replacements and trimming with new value: ", newstr)
result = JSON.parse(newstr)
jsonvalid = true
} catch (e) {
//console.log("Failed parsing JSON even though its valid (2): ", e)
jsonvalid = false
}
}
//console.log("VALID: ", jsonvalid, result)
return {
valid: jsonvalid,
result: result,
};
};
const GettingStarted = (props) => {
const { globalUrl, isLoggedIn, isLoaded, userdata } = props;
@@ -440,6 +129,7 @@ const GettingStarted = (props) => {
const theme = useTheme();
const alert = useAlert();
const classes = useStyles(theme);
let navigate = useNavigate();
const imgSize = 60;
const referenceUrl = globalUrl + "/api/v1/hooks/";
@@ -459,8 +149,8 @@ const GettingStarted = (props) => {
"https://github.com/frikky/shuffle-workflows"
);
const [downloadBranch, setDownloadBranch] = React.useState("master");
const [loadWorkflowsModalOpen, setLoadWorkflowsModalOpen] =
React.useState(false);
const [loadWorkflowsModalOpen, setLoadWorkflowsModalOpen] = React.useState(false);
const [videoViewOpen, setVideoViewOpen] = React.useState(false);
const [exportModalOpen, setExportModalOpen] = React.useState(false);
const [exportData, setExportData] = React.useState("");
@@ -825,6 +515,8 @@ const GettingStarted = (props) => {
credentials: "include",
})
.then((response) => {
setVideoViewOpen(true)
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!: ", response.status);
@@ -879,6 +571,8 @@ const GettingStarted = (props) => {
}
})
.catch((error) => {
setVideoViewOpen(true)
alert.error(error.toString());
});
};
@@ -2446,70 +2140,27 @@ const GettingStarted = (props) => {
maxWidth: 600,
};
const WorkflowView = () => {
/*
if (workflows.length === 0) {
return (
<div style={emptyWorkflowStyle}>
<Paper style={boxStyle}>
<div>
<h2>Welcome to Shuffle</h2>
</div>
<div>
<p>
<b>Shuffle</b> is a flexible, easy to use, automation platform
allowing users to integrate their services and devices freely.
It's made to significantly reduce the amount of manual labor,
and is focused on security applications.{" "}
<a
href="/docs/about"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Click here to learn more.
</a>
</p>
</div>
<div>
If you want to jump straight into it, click here to create your
first workflow:
</div>
<div style={{ display: "flex" }}>
<Button
id="second-step"
color="primary"
style={{ marginTop: "20px" }}
variant="outlined"
onClick={() => setModalOpen(true)}
>
New workflow
</Button>
<span style={{ paddingTop: 20, display: "flex" }}>
<Typography
style={{ marginTop: 5, marginLeft: 30, marginRight: 15 }}
>
..OR
</Typography>
{workflowButtons}
</span>
</div>
</Paper>
</div>
);
}
*/
const WorkflowView = () => {
var workflowDelay = -150
var appDelay = -75
const textSpacingDiff = 8
const textType = "body2"
// Discover <a target="_blank" href="https://shuffler.io/creators" style={{textDecoration: "none", color: "#f86a3e",}}>use-cases made by other creators</a>!
// Discover <a target="_blank" href="https://shuffler.io/search?tab=workflows" style={{textDecoration: "none", color: "#f86a3e",}}>use-cases made by us and other creators</a>!
const steps = [
{
html: (
<Typography variant={textType} style={{marginTop: textSpacingDiff}}>
<Link to="/detectionframework" style={{textDecoration: "none", color: "#f86a3e",}}>Find your integrations</Link> by following our simple detection framework!
<Typography variant={textType} style={{marginTop: textSpacingDiff}} onClick={() => {
if (isCloud) {
ReactGA.event({
category: "getting-started",
action: `integerations_find_click`,
})
}
}}>
<Link to="/detectionframework" style={{textDecoration: "none", color: "#f86a3e",}}>Find relevant apps</Link> and start your automation journey
</Typography>
),
tutorial: "find_integrations",
@@ -2517,31 +2168,50 @@ const GettingStarted = (props) => {
{
html:
<Typography variant={textType} style={{marginTop: textSpacingDiff}}>
Discover <span style={{cursor: "pointer", textDecoration: "none", color: "#f86a3e",}} onClick={() => {
Discover <Link to="/usecases" style={{cursor: "pointer", textDecoration: "none", color: "#f86a3e",}}>Use Case ideas</Link> and&nbsp;
<span style={{cursor: "pointer", textDecoration: "none", color: "#f86a3e",}} onClick={() => {
if (isCloud) {
navigate(`/search?tab=workflows`)
ReactGA.event({
category: "getting-started",
action: `workflow_find_click`,
})
return
} else {
alert.success("TBD: Coming in version 1.0.0");
}
const ele = document.getElementById("shuffle_search_field")
if (ele !== undefined && ele !== null) {
console.log("Found ele: ", ele)
ele.focus()
ele.style.borderColor = "#f86a3e"
ele.style.borderWidth = "2px"
const ele = document.getElementById("shuffle_search_field")
if (ele !== undefined && ele !== null) {
console.log("Found ele: ", ele)
ele.focus()
ele.style.borderColor = "#f86a3e"
ele.style.borderWidth = "2px"
} else {
alert.success("TBD: Coming in version 1.0.0");
}
}}>
use-cases made by other creators</span>!
} else {
//alert.success("TBD: Coming in version 1.0.0");
}
}}>
workflows made by other creators</span>!
</Typography>,
tutorial: "discover_workflows",
},
{
html: (
<Typography variant={textType} style={{marginTop: textSpacingDiff}}>
<Typography variant={textType} style={{marginTop: textSpacingDiff}} onClick={() => {
if (isCloud) {
ReactGA.event({
category: "getting-started",
action: `create_workflow_click`,
})
}
}}>
Learn to use Shuffle by&nbsp;
<span style={{cursor: "pointer", color: "#f86a3e",}} onClick={() => {setModalOpen(true)}}>
creating your first workflow
</span> and <Link to="/docs" style={{textDecoration: "none", color: "#f86a3e",}}>reading the docs.</Link>
</span> and <Link to="/docs/getting_started" style={{textDecoration: "none", color: "#f86a3e",}}>reading the docs.</Link>
</Typography>
),
tutorial: "learn_shuffle",
@@ -2557,6 +2227,55 @@ const GettingStarted = (props) => {
return (
<div style={viewStyle}>
{isCloud ?
<Dialog
open={videoViewOpen}
onClose={() => {
setVideoViewOpen(false)
}}
PaperProps={{
style: {
backgroundColor: surfaceColor,
color: "white",
minWidth: 560,
minHeight: 415,
textAlign: "center",
},
}}
>
<DialogTitle>
Welcome to Shuffle!
</DialogTitle>
<Tooltip
title="Close window"
placement="top"
style={{ zIndex: 10011 }}
>
<IconButton
style={{ zIndex: 5000, position: "absolute", top: 10, right: 34 }}
onClick={(e) => {
e.preventDefault();
setVideoViewOpen(false)
}}
>
<CloseIcon style={{ color: "white" }} />
</IconButton>
</Tooltip>
<iframe
width="560"
height="315"
style={{margin: "0px auto 0px auto", width: 560, height: 315,}}
src="https://www.youtube-nocookie.com/embed/rO7k9q3OgC0"
title="Introduction video"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
>
</iframe>
</Dialog>
: null}
<div style={workflowViewStyle}>
<Typography variant="h1" style={{fontSize: 30, marginTop: 25, }}>
Getting Started with Shuffle
+39 -9
View File
@@ -1,5 +1,5 @@
/* eslint-disable react/no-multi-comp */
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { makeStyles } from "@material-ui/styles";
import { useInterval } from "react-powerhooks";
@@ -32,9 +32,6 @@ const useStyles = makeStyles({
});
const LoginDialog = (props) => {
const theme = useTheme();
let navigate = useNavigate();
const {
globalUrl,
isLoaded,
@@ -44,6 +41,11 @@ const LoginDialog = (props) => {
register,
checkLogin,
} = props;
const theme = useTheme();
let navigate = useNavigate();
const classes = useStyles();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [firstRequest, setFirstRequest] = useState(true);
@@ -54,9 +56,13 @@ const LoginDialog = (props) => {
const [MFAField, setMFAField] = useState(false);
const [MFAValue, setMFAValue] = useState("");
// Used to swap from login to register. True = login, false = register
const classes = useStyles();
useEffect(() => {
checkAdmin()
}, [loginViewLoading])
// Error messages etc
const [loginInfo, setLoginInfo] = useState("");
@@ -143,7 +149,7 @@ const LoginDialog = (props) => {
var baseurl = globalUrl;
if (register) {
var url = baseurl + "/api/v1/users/login";
var url = baseurl + "/api/v1/login";
fetch(url, {
mode: "cors",
method: "POST",
@@ -180,7 +186,8 @@ const LoginDialog = (props) => {
setIsLoggedIn(true);
navigate("/workflows")
//navigate("/workflows")
window.location.href = "/workflows"
}
})
)
@@ -264,6 +271,7 @@ const LoginDialog = (props) => {
}}
/>
</div>
{loginViewLoading ? (
<div style={{ textAlign: "center", marginTop: 50 }}>
<Typography
@@ -466,13 +474,15 @@ const LoginDialog = (props) => {
<div style={{ textAlign: "center", margin: 10 }}>
<Button
fullWidth
id="sso_button"
color="secondary"
variant="outlined"
type="button"
style={{ flex: "1", marginTop: 5 }}
onClick={() => {
console.log("CLICK");
navigate(ssoUrl)
//console.log("CLICK SSO");
window.location.href = ssoUrl
//navigate(ssoUrl)
}}
>
Use SSO
@@ -488,6 +498,26 @@ const LoginDialog = (props) => {
const loadedCheck = isLoaded ? <div>{basedata}</div> : <div></div>;
useEffect(() => {
setTimeout(() => {
if (ssoUrl !== undefined && ssoUrl !== null && ssoUrl.length > 0) {
//id="sso_button"
const ssoBtn = document.getElementById("sso_button");
if (ssoBtn !== undefined && ssoBtn !== null) {
console.log("SSO BTN: ", ssoBtn)
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
var tmpView = new URLSearchParams(cursearch).get("autologin");
if (tmpView !== undefined && tmpView !== null) {
if (tmpView === "true") {
console.log("Tmp: ", tmpView)
ssoBtn.click()
}
}
}
}
}, 200);
}, [ssoUrl])
return <div>{loadedCheck}</div>;
};
+29 -21
View File
@@ -77,7 +77,7 @@ const Settings = (props) => {
//Returns the value from a storage position at a given address.
const isCloud =
window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io";
window.location.host === "shuffler.io"
const bodyDivStyle = {
margin: "auto",
@@ -257,6 +257,7 @@ const Settings = (props) => {
console.log("Status not 200 for WORKFLOW EXECUTION :O!");
}
return response.json();
})
.then((responseJson) => {
@@ -402,11 +403,12 @@ const Settings = (props) => {
userdata.eth_info.account.length > 0 && userdata.eth_info.parsed_balance !== undefined
// Random names for type & autoComplete. Didn't research :^)
var imageData = file.length > 0 ? file : fileBase64;
imageData =
imageData === undefined || imageData.length === 0
? theme.palette.defaultImage
: imageData;
//var imageData = file.length > 0 ? file : fileBase64;
//imageData = imageData === undefined || imageData.length === 0
// ? theme.palette.defaultImage
// : imageData;
const imageData = userSettings.image === undefined || userSettings.image == null || userSettings.image.length === 0 ? theme.palette.defaultImage : userSettings.image
const imageInfo = (
<img
src={imageData}
@@ -729,17 +731,22 @@ const Settings = (props) => {
<div style={{ display: runFlex ? "flex" : "", width: "100%" }}>
<div>
{isCloud ?
<Button
style={{ height: 40, marginTop: 10 }}
variant="outlined"
color="primary"
fullWidth={true}
onClick={() => {
handleGithubConnection();
}}
>
Connect to Github
</Button>
<span>
<Typography variant="body1" color="textSecondary">
By connecting your Github account, you agree to our <a href="/docs/terms_of_service" target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}}>Terms of Service</a>, and acknowledge that your non-sensitive data will be turned into a <a target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}} href="https://shuffler.io/search?tab=creators">creator account</a>. This enables you to earn a passive income from Shuffle. This IS reversible.
</Typography>
<Button
style={{ height: 40, marginTop: 10 }}
variant="outlined"
color="primary"
fullWidth={true}
onClick={() => {
handleGithubConnection();
}}
>
Connect to Github
</Button>
</span>
: null}
</div>
<div style={{ flex: 1, display: "flex" }}>
@@ -859,7 +866,7 @@ const Settings = (props) => {
handleEthereumConnection();
}}
>
Authenticate
Authenticate Metamask Wallet
</Button>
)}
</div>
@@ -917,11 +924,11 @@ const Settings = (props) => {
};
const handleGithubConnection = () => {
console.log("GITHUB CONNECT WOO")
console.log("GITHUB CONNECT WOO: ", isCloud)
//result = RestClient.post('https://github.com/login/oauth/access_token',
console.log("HOST: ", window.location.host);
console.log("HOST: ", window.location);
console.log("Location: ", window.location);
const redirectUri = isCloud
? window.location.host === "localhost:3002"
? "http%3A%2F%2Flocalhost:3002%2Fset_authentication"
@@ -931,10 +938,11 @@ const Settings = (props) => {
:
`https%3A%2F%2F${window.location.host}%2Fset_authentication`
console.log("redirect: ", redirectUri)
const client_id = "3d272b1b782b100b1e61"
const username = userdata.id;
const scopes = "user:email";
const scopes = "read:user";
const url = `https://github.com/login/oauth/authorize?access_type=offline&prompt=consent&client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&scope=${scopes}&state=username%3D${username}%26type%3Dgithub`
+472 -136
View File
@@ -6,12 +6,16 @@ import { Navigate } from "react-router-dom";
import SecurityFramework from '../components/SecurityFramework.jsx';
import { ShepherdTour, ShepherdTourContext } from 'react-shepherd'
import { isMobile } from "react-device-detect"
import {
Badge,
Avatar,
Grid,
InputLabel,
Select,
ListSubheader,
Paper,
Tooltip,
Divider,
@@ -31,8 +35,15 @@ import {
DialogTitle,
DialogActions,
DialogContent,
OutlinedInput,
Checkbox,
ListItemText,
} from "@material-ui/core";
import {
AvatarGroup,
} from "@mui/material"
import {
GridOn as GridOnIcon,
List as ListIcon,
@@ -58,6 +69,8 @@ import {
Publish as PublishIcon,
CloudUpload as CloudUploadIcon,
CloudDownload as CloudDownloadIcon,
ExpandLess as ExpandLessIcon,
ExpandMore as ExpandMoreIcon,
} from "@material-ui/icons";
import NestedMenuItem from "material-ui-nested-menu-item";
@@ -381,9 +394,27 @@ const chipStyle = {
};
export const validateJson = (showResult) => {
//showResult = showResult.split(" None").join(" \"None\"")
showResult = showResult.split(" False").join(" false");
showResult = showResult.split(" True").join(" true");
//console.log("INPUT: ", showResult, typeof showResult)
if (typeof showResult === 'string') {
//showResult = showResult.split(" None").join(" \"None\"")
showResult = showResult.split(" False").join(" false");
showResult = showResult.split(" True").join(" true");
//return {
// valid: false,
// result: showResult,
//};
}
//if (typeof showResult === undefined) {
//}
if (typeof showResult === "object" || typeof showResult === "array") {
return {
valid: true,
result: showResult,
};
}
var jsonvalid = true;
try {
@@ -453,6 +484,8 @@ const Workflows = (props) => {
var upload = "";
const [workflows, setWorkflows] = React.useState([]);
const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove
const [selectedUsecases, setSelectedUsecases] = React.useState([]);
const [filteredWorkflows, setFilteredWorkflows] = React.useState([]);
const [selectedWorkflow, setSelectedWorkflow] = React.useState({});
const [workflowDone, setWorkflowDone] = React.useState(false);
@@ -488,6 +521,8 @@ const Workflows = (props) => {
const [actionImageList, setActionImageList] = React.useState([]);
const [firstLoad, setFirstLoad] = React.useState(true);
const [showMoreClicked, setShowMoreClicked] = React.useState(false);
const [usecases, setUsecases] = React.useState([]);
const isCloud =
window.location.host === "localhost:3002" ||
@@ -834,7 +869,7 @@ const Workflows = (props) => {
console.log("Status not 200 for workflows :O!: ", response.status);
if (isCloud) {
window.location.pathname = "/login";
window.location.pathname = "/search?tab=workflows";
}
alert.info("Failed getting workflows.");
@@ -847,8 +882,10 @@ const Workflows = (props) => {
.then((responseJson) => {
if (responseJson !== undefined) {
setWorkflows(responseJson);
fetchUsecases(responseJson)
if (responseJson !== undefined) {
var actionnamelist = [];
var parsedactionlist = [];
for (var key in responseJson) {
@@ -888,6 +925,83 @@ const Workflows = (props) => {
});
};
const handleKeysetting = (categorydata, workflows) => {
console.log("Workflows: ", workflows)
//workflows[0].category = ["detect"]
//workflows[0].usecase_ids = ["Correlate tickets"]
if (workflows !== undefined && workflows !== null) {
var newcategories = []
for (var key in categorydata) {
var category = categorydata[key]
category.matches = []
for (var subcategorykey in category.list) {
var subcategory = category.list[subcategorykey]
subcategory.matches = []
for (var workflowkey in workflows) {
const workflow = workflows[workflowkey]
if (workflow.usecase_ids !== undefined && workflow.usecase_ids !== null) {
for (var usecasekey in workflow.usecase_ids) {
if (workflow.usecase_ids[usecasekey].toLowerCase() === subcategory.name.toLowerCase()) {
console.log("Got match: ", workflow.usecase_ids[usecasekey])
category.matches.push({
"workflow": workflow.id,
"category": subcategory.name,
})
subcategory.matches.push(workflow.id)
break
}
}
}
if (subcategory.matches.length > 0) {
break
}
}
}
newcategories.push(category)
}
console.log("Categories: ", newcategories)
setUsecases(newcategories)
} else {
setUsecases(categorydata)
}
}
const fetchUsecases = (workflows) => {
fetch(globalUrl + "/api/v1/workflows/usecases", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for usecases");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success !== false) {
console.log("Usecases: ", responseJson)
handleKeysetting(responseJson, workflows)
}
})
.catch((error) => {
//alert.error("ERROR: " + error.toString());
console.log("ERROR: " + error.toString());
});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
if (workflows.length <= 0) {
@@ -896,7 +1010,6 @@ const Workflows = (props) => {
setView(tmpView);
}
//setFirstrequest(false);
getAvailableWorkflows();
}
}, [])
@@ -905,8 +1018,8 @@ const Workflows = (props) => {
color: "#ffffff",
width: "100%",
display: "flex",
minWidth: 1024,
maxWidth: 1024,
minWidth: isMobile ? "100%" : 1024,
maxWidth: isMobile ? "100%" : 1024,
margin: "auto",
};
@@ -926,6 +1039,7 @@ const Workflows = (props) => {
flexDirection: "column",
};
//flexDirection: !isMobile ? "column" : "row",
const paperAppContainer = {
display: "flex",
flexWrap: "wrap",
@@ -1270,7 +1384,7 @@ const Workflows = (props) => {
};
return (
<Grid item xs={4} style={{ padding: "12px 10px 12px 10px" }}>
<Grid item xs={isMobile ? 12 : 4} style={{ padding: "12px 10px 12px 10px" }}>
<Paper
square
style={setupPaperStyle}
@@ -1292,6 +1406,30 @@ const Workflows = (props) => {
);
};
const getWorkflowAppgroup = (data) => {
if (data.actions === undefined || data.actions === null) {
return []
}
var appsFound = []
for (var key in data.actions) {
const parsedAction = data.actions[key]
if (parsedAction.large_image === undefined || parsedAction.large_image === null || parsedAction.large_image === "") {
continue
}
if (parsedAction.app_name === "Shuffle Tools" || parsedAction.app_id === "bc78f35c6c6351b07a09b7aed5d29652") {
continue
}
if (appsFound.findIndex(data => data.app_name === parsedAction.app_name) < 0){
appsFound.push(parsedAction)
}
}
return appsFound
}
const WorkflowPaper = (props) => {
const { data } = props;
const [open, setOpen] = React.useState(false);
@@ -1321,6 +1459,7 @@ const Workflows = (props) => {
}
const actions = data.actions !== null ? data.actions.length : 0;
const appGroup = getWorkflowAppgroup(data)
const [triggers, subflows] = getWorkflowMeta(data);
const workflowMenuButtons = (
@@ -1345,6 +1484,11 @@ const Workflows = (props) => {
if (data.tags !== undefined && data.tags !== null) {
setNewWorkflowTags(JSON.parse(JSON.stringify(data.tags)));
}
console.log("Editing: ", data)
if (data.usecase_ids !== undefined && data.usecase_ids !== null && data.usecase_ids.length > 0) {
setSelectedUsecases(data.usecase_ids)
}
}}
key={"change"}
>
@@ -1543,22 +1687,49 @@ const Workflows = (props) => {
</Tooltip>
</Grid>
<Grid item style={workflowActionStyle}>
<Tooltip color="primary" title="Action amount" placement="bottom">
<span style={{ color: "#979797", display: "flex" }}>
<BubbleChartIcon
style={{ marginTop: "auto", marginBottom: "auto" }}
/>
<Typography
style={{
marginLeft: 5,
marginTop: "auto",
marginBottom: "auto",
}}
>
{actions}
</Typography>
</span>
</Tooltip>
{appGroup.length > 0 ?
<div style={{display: "flex", marginTop: 8, }}>
<AvatarGroup max={4} style={{marginLeft: 5, maxHeight: 24,}}>
{appGroup.map((data, index) => {
return (
<div
key={index}
style={{
height: 24,
width: 24,
filter: "brightness(0.6)",
cursor: "pointer",
}}
onClick={() => {
addFilter(data.app_name);
}}
>
<Tooltip color="primary" title={data.app_name} placement="bottom">
<Avatar alt={data.app_name} src={data.large_image} style={{width: 24, height: 24}}/>
</Tooltip>
</div>
)
})}
</AvatarGroup>
</div>
:
<Tooltip color="primary" title="Action amount" placement="bottom">
<span style={{ color: "#979797", display: "flex" }}>
<BubbleChartIcon
style={{ marginTop: "auto", marginBottom: "auto" }}
/>
<Typography
style={{
marginLeft: 5,
marginTop: "auto",
marginBottom: "auto",
}}
>
{actions}
</Typography>
</span>
</Tooltip>
}
<Tooltip
color="primary"
title="Trigger amount"
@@ -1661,9 +1832,11 @@ const Workflows = (props) => {
justifyContent: "left",
overflow: "hidden",
marginTop: 5,
maxHeight: 28,
overflow: "hidden",
}}
>
{data.tags !== undefined
{data.tags !== undefined && data.tags !== null
? data.tags.map((tag, index) => {
if (index >= 3) {
return null;
@@ -1709,7 +1882,8 @@ const Workflows = (props) => {
tags,
defaultReturnValue,
editingWorkflow,
redirect
redirect,
currentUsecases,
) => {
var method = "POST";
var extraData = "";
@@ -1736,6 +1910,12 @@ const Workflows = (props) => {
workflowdata["default_return_value"] = defaultReturnValue;
}
if (currentUsecases !== undefined && currentUsecases !== null) {
workflowdata["usecase_ids"] = currentUsecases
//workflows[0].category = ["detect"]
//workflows[0].usecase_ids = ["Correlate tickets"]
}
return fetch(globalUrl + "/api/v1/workflows" + extraData, {
method: method,
headers: {
@@ -1982,30 +2162,54 @@ const Workflows = (props) => {
const data = params.row.record;
const actions = data.actions !== null ? data.actions.length : 0;
let [triggers, subflows] = getWorkflowMeta(data);
const appGroup = getWorkflowAppgroup(data)
return (
<Grid item>
<div style={{ display: "flex" }}>
<Tooltip
color="primary"
title="Action amount"
placement="bottom"
>
<span style={{ color: "#979797", display: "flex" }}>
<BubbleChartIcon
style={{ marginTop: "auto", marginBottom: "auto" }}
/>
<Typography
style={{
marginLeft: 5,
marginTop: "auto",
marginBottom: "auto",
}}
>
{actions}
</Typography>
</span>
</Tooltip>
{appGroup.length > 0 ?
<div style={{display: "flex", marginTop: 3, }}>
<AvatarGroup max={4} style={{marginLeft: 5, maxHeight: 24,}}>
{appGroup.map((data, index) => {
return (
<div
key={index}
style={{
height: 24,
width: 24,
filter: "brightness(0.6)",
cursor: "pointer",
}}
onClick={() => {
addFilter(data.app_name);
}}
>
<Tooltip color="primary" title={data.app_name} placement="bottom">
<Avatar alt={data.app_name} src={data.large_image} style={{width: 24, height: 24}}/>
</Tooltip>
</div>
)
})}
</AvatarGroup>
</div>
:
<Tooltip color="primary" title="Action amount" placement="bottom">
<span style={{ color: "#979797", display: "flex" }}>
<BubbleChartIcon
style={{ marginTop: "auto", marginBottom: "auto" }}
/>
<Typography
style={{
marginLeft: 5,
marginTop: "auto",
marginBottom: "auto",
}}
>
{actions}
</Typography>
</span>
</Tooltip>
}
<Tooltip
color="primary"
title="Trigger amount"
@@ -2189,6 +2393,7 @@ const Workflows = (props) => {
return <div style={gridContainer}>{workflowData}</div>;
};
var total_count = 0
const modalView = modalOpen ? (
<Dialog
open={modalOpen}
@@ -2199,7 +2404,8 @@ const Workflows = (props) => {
style: {
backgroundColor: surfaceColor,
color: "white",
minWidth: "800px",
minWidth: isMobile ? "90%" : "800px",
maxWidth: isMobile ? "90%" : "800px",
},
}}
>
@@ -2231,6 +2437,7 @@ const Workflows = (props) => {
}}
color="primary"
placeholder="Name"
required
margin="dense"
defaultValue={newWorkflowName}
autoFocus
@@ -2246,47 +2453,117 @@ const Workflows = (props) => {
color="primary"
defaultValue={newWorkflowDescription}
placeholder="Description"
rows="3"
multiline
margin="dense"
fullWidth
/>
<ChipInput
style={{ marginTop: 10 }}
InputProps={{
style: {
color: "white",
},
}}
placeholder="Tags"
color="primary"
fullWidth
value={newWorkflowTags}
onAdd={(chip) => {
newWorkflowTags.push(chip);
setNewWorkflowTags(newWorkflowTags);
}}
onDelete={(chip, index) => {
newWorkflowTags.splice(index, 1);
setNewWorkflowTags(newWorkflowTags);
}}
/>
<div style={{display: "flex", marginTop: 10, }}>
<ChipInput
style={{ flex: 1}}
InputProps={{
style: {
color: "white",
},
}}
placeholder="Tags"
color="primary"
fullWidth
value={newWorkflowTags}
onAdd={(chip) => {
newWorkflowTags.push(chip);
setNewWorkflowTags(newWorkflowTags);
}}
onDelete={(chip, index) => {
newWorkflowTags.splice(index, 1);
setNewWorkflowTags(newWorkflowTags);
}}
/>
{usecases !== null && usecases !== undefined && usecases.length > 0 ?
<FormControl style={{flex: 1, marginLeft: 5, }}>
<InputLabel htmlFor="grouped-select-usecase">Usecases</InputLabel>
<Select
defaultValue=""
id="grouped-select"
label="Matching Usecase"
multiple
value={selectedUsecases}
renderValue={(selected) => selected.join(', ')}
onChange={(event) => {
console.log("Changed: ", event)
}}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{usecases.map((usecase, index) => {
//console.log(usecase)
return (
<span key={index}>
<ListSubheader
style={{color: usecase.color}}
>
{usecase.name}
</ListSubheader>
{usecase.list.map((subcase, subindex) => {
//console.log(subcase)
total_count += 1
return (
<MenuItem key={subindex} value={total_count} onClick={(event) => {
if (selectedUsecases.includes(subcase.name)) {
const itemIndex = selectedUsecases.indexOf(subcase.name)
if (itemIndex > -1) {
selectedUsecases.splice(itemIndex, 1)
}
} else {
selectedUsecases.push(subcase.name)
}
setUpdate(Math.random());
setSelectedUsecases(selectedUsecases)
}}>
<Checkbox style={{color: selectedUsecases.includes(subcase.name) ? usecase.color : theme.palette.inputColor}} checked={selectedUsecases.includes(subcase.name)} />
<ListItemText primary={subcase.name} />
</MenuItem>
)
})}
</span>
)
})}
</Select>
</FormControl>
: null}
</div>
{showMoreClicked ?
<span>
<TextField
onBlur={(event) => setDefaultReturnValue(event.target.value)}
InputProps={{
style: {
color: "white",
},
}}
color="primary"
defaultValue={defaultReturnValue}
placeholder="Default return value (used for Subflows if the subflow fails)"
rows="3"
multiline
margin="dense"
fullWidth
/>
</span>
: null}
<Tooltip color="primary" title={"Add more details"} placement="top">
<IconButton
style={{ color: "white", margin: "auto", marginTop: 10, textAlign: "center", width: 50,}}
onClick={() => {
setShowMoreClicked(!showMoreClicked);
}}
>
{showMoreClicked ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
</Tooltip>
<TextField
onBlur={(event) => setDefaultReturnValue(event.target.value)}
InputProps={{
style: {
color: "white",
},
}}
color="primary"
defaultValue={defaultReturnValue}
placeholder="Default return value (used for Subflows if the subflow fails)"
rows="3"
multiline
margin="dense"
fullWidth
/>
</DialogContent>
<DialogActions>
<Button
@@ -2298,6 +2575,7 @@ const Workflows = (props) => {
setEditingWorkflow({});
setNewWorkflowTags([]);
setModalOpen(false);
setSelectedUsecases([])
}}
color="primary"
>
@@ -2316,8 +2594,10 @@ const Workflows = (props) => {
newWorkflowTags,
defaultReturnValue,
editingWorkflow,
false
false,
selectedUsecases,
);
setNewWorkflowName("");
setDefaultReturnValue("");
setNewWorkflowDescription("");
@@ -2330,11 +2610,13 @@ const Workflows = (props) => {
newWorkflowTags,
defaultReturnValue,
{},
true
true,
selectedUsecases,
);
}
setSubmitLoading(true);
setSelectedUsecases([])
}}
color="primary"
>
@@ -2366,7 +2648,7 @@ const Workflows = (props) => {
{view === "list" && (
<Tooltip color="primary" title={"Grid View"} placement="top">
<Button
color="primary"
color="secondary"
variant="text"
onClick={() => {
localStorage.setItem("view", "grid");
@@ -2380,7 +2662,7 @@ const Workflows = (props) => {
{view === "grid" && (
<Tooltip color="primary" title={"List View"} placement="top">
<Button
color="primary"
color="secondary"
variant="text"
onClick={() => {
localStorage.setItem("view", "list");
@@ -2393,12 +2675,12 @@ const Workflows = (props) => {
)}
<Tooltip color="primary" title={"Import workflows"} placement="top">
{importLoading ? (
<Button color="primary" style={{}} variant="text" onClick={() => {}}>
<Button color="secondary" style={{}} variant="text" onClick={() => {}}>
<CircularProgress style={{ maxHeight: 15, maxWidth: 15 }} />
</Button>
) : (
<Button
color="primary"
color="secondary"
style={{}}
variant="text"
onClick={() => upload.click()}
@@ -2421,7 +2703,7 @@ const Workflows = (props) => {
placement="top"
>
<Button
color="primary"
color="secondary"
style={{}}
variant="text"
onClick={() => {
@@ -2433,9 +2715,9 @@ const Workflows = (props) => {
</Tooltip>
) : null}
{isCloud ? null : (
<Tooltip color="primary" title={"Download workflows"} placement="top">
<Tooltip color="primary" title={"Import workflows to Shuffle"} placement="top">
<Button
color="primary"
color="secondary"
style={{}}
variant="text"
onClick={() => setLoadWorkflowsModalOpen(true)}
@@ -2606,9 +2888,54 @@ const Workflows = (props) => {
return (
<div style={viewStyle}>
<div style={workflowViewStyle}>
<div style={{ display: "flex" }}>
<div style={{ flex: 3 }}>
<h2>Workflows</h2>
<div style={{ display: "flex", marginTop: 25, }}>
<div style={{ flex: 1 }}>
<Typography variant="h1" style={{fontSize: 30}}>
Workflows
</Typography>
</div>
{/*
<div style={{ flex: 1 }}>
<Typography style={{ marginTop: 7, marginBottom: "auto" }}>
<a
rel="noopener noreferrer"
target="_blank"
href="https://shuffler.io/docs/workflows"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more about Workflows
</a>
</Typography>
</div>
*/}
{isMobile ? null :
<div style={{ display: "flex", margin: "0px 0px 20px 0px" }}>
<div style={{ flex: 1, float: "right" }}>
<ChipInput
style={{}}
InputProps={{
style: {
color: "white",
maxWidth: 275,
minWidth: 275,
},
}}
placeholder="Add Filter"
color="primary"
fullWidth
value={filters}
onAdd={(chip) => {
addFilter(chip);
}}
onDelete={(_, index) => {
removeFilter(index);
}}
/>
</div>
</div>
}
<div style={{ flex: 1, textAlign: "right" }}>
{workflowButtons}
</div>
</div>
{/*
@@ -2658,43 +2985,52 @@ const Workflows = (props) => {
)
}}
*/}
<div style={{ display: "flex", margin: "0px 0px 20px 0px" }}>
<div style={{ flex: 1 }}>
<Typography style={{ marginTop: 7, marginBottom: "auto" }}>
<a
rel="noopener noreferrer"
target="_blank"
href="https://shuffler.io/docs/workflows"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
Learn more about Workflows
</a>
</Typography>
</div>
<div style={{ flex: 1, float: "right" }}>
<ChipInput
style={{}}
InputProps={{
style: {
color: "white",
},
}}
placeholder="Add Filter"
color="primary"
fullWidth
value={filters}
onAdd={(chip) => {
addFilter(chip);
}}
onDelete={(_, index) => {
removeFilter(index);
}}
/>
</div>
<div style={{ float: "right", flex: 1, textAlign: "right" }}>
{workflowButtons}
</div>
</div>
<div style={{width: "100%",}}>
{!isMobile && usecases !== null && usecases !== undefined && usecases.length > 0 ?
<div style={{ display: "flex", }}>
{usecases.map((usecase, index) => {
//console.log(usecase)
return (
<Paper
key={usecase.name}
style={{
flex: 1,
backgroundColor: filters.includes(usecase.name.toLowerCase()) ? usecase.color : theme.palette.surfaceColor,
borderRadius: theme.palette.borderRadius,
marginRight: index === usecases.length-1 ? 0 : 10,
height: 60,
cursor: "pointer",
border: `2px solid ${usecase.color}`,
overflow: "hidden",
padding: 10,
}}
onClick={() => {
console.log("Clicked!")
return
if (filters.includes(usecase.name.toLowerCase())) {
addFilter(usecase.name)
} else {
const foundIndex = filters.indexOf(usecase.name.toLowerCase())
removeFilter(foundIndex)
}
}}
>
<a href={`/usecases?selected=${usecase.name}`} rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", }}>
<Typography variant="body1" color="textPrimary">
{usecase.name}
</Typography>
<Typography variant="body2" color="textSecondary">
In use: {usecase.matches.length}/{usecase.list.length}
</Typography>
</a>
</Paper>
)
})}
</div>
: null}
</div>
<div style={{ marginTop: 15 }} />
{actionImageList !== undefined &&
actionImageList !== null &&
@@ -2805,7 +3141,7 @@ const Workflows = (props) => {
workflowDelay += 75
} else {
return (
<Grid item xs={4} style={{ padding: "12px 10px 12px 10px" }}>
<Grid key={index} item xs={isMobile ? 12 : 4} style={{ padding: "12px 10px 12px 10px" }}>
<WorkflowPaper key={index} data={data} />
</Grid>
)
@@ -2813,7 +3149,7 @@ const Workflows = (props) => {
return (
<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>
<Grid item xs={4} style={{ padding: "12px 10px 12px 10px" }}>
<Grid item xs={isMobile ? 12 : 4} style={{ padding: "12px 10px 12px 10px" }}>
<WorkflowPaper key={index} data={data} />
</Grid>
</Zoom>
@@ -3008,7 +3344,7 @@ const Workflows = (props) => {
}}
color="primary"
>
Submit Submit
Submit
</Button>
</DialogActions>
</Dialog>
@@ -3024,7 +3360,7 @@ const Workflows = (props) => {
*/}
<Dropzone
style={{
maxWidth: window.innerWidth > 1366 ? 1366 : 1200,
maxWidth: window.innerWidth > 1366 ? 1366 : isMobile ? "100%" : 1200,
margin: "auto",
padding: 20,
}}
@@ -0,0 +1,2 @@
# AWS Lambda forwarder to Shuffle
This function is made to forward S3 notifications to Shuffle to run a workflow when an object is made or updated.
@@ -0,0 +1,21 @@
import json
import urllib.parse
import urllib3
import os
print('Loading function')
def lambda_handler(event, context):
# Get the object from the event and show its content type
bucket = event['Records'][0]['s3']['bucket']['name']
webhook = os.environ.get("SHUFFLE_WEBHOOK")
if not webhook:
return "No webhook environment defined: SHUFFLE_WEBHOOK"
http = urllib3.PoolManager()
ret = http.request('POST', webhook, body=json.dumps(event["Records"][0]).encode("utf-8"))
if ret.status != 200:
return "Bad status code for webhook: %d" % ret.status_code
print("Status code: %d\nData: %s" % (ret.status, ret.data))
+1 -1
View File
@@ -1,5 +1,5 @@
NAME=shuffle-orborus
VERSION=0.9.50
VERSION=0.9.61
echo "Running docker build with $NAME:$VERSION"
#docker rmi frikky/shuffle:$NAME --force
+23 -9
View File
@@ -37,9 +37,11 @@ import (
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
//"github.com/docker/docker/api/types/filters"
dockerclient "github.com/docker/docker/client"
"github.com/satori/go.uuid"
uuid "github.com/satori/go.uuid"
//network "github.com/docker/docker/api/types/network"
//natting "github.com/docker/go-connections/nat"
"github.com/mackerelio/go-osstat/cpu"
@@ -63,7 +65,7 @@ var baseimagename = os.Getenv("SHUFFLE_BASE_IMAGE_NAME")
var baseimageregistry = os.Getenv("SHUFFLE_BASE_IMAGE_REGISTRY")
var baseimagetagsuffix = os.Getenv("SHUFFLE_BASE_IMAGE_TAG_SUFFIX")
var orgId = os.Getenv("ORG_ID")
//var orgId = os.Getenv("ORG_ID")
var baseUrl = os.Getenv("BASE_URL")
var environment = os.Getenv("ENVIRONMENT_NAME")
var dockerApiVersion = os.Getenv("DOCKER_API_VERSION")
@@ -199,7 +201,7 @@ func deployServiceWorkers(image string) {
// Looks for and cleans up all existing items in swarm we can't re-use (Shuffle only)
cleanupExistingNodes(ctx)
// frikky@debian:~/git/shuffle/functions/onprem/worker$ docker service create --replicas 5 --name shuffle-workers --env SHUFFLE_SWARM_CONFIG=run --publish published=33333,target=33333 ghcr.io/frikky/shuffle-worker:nightly
networkName := "shuffle-executions"
networkName := "shuffle_swarm_executions"
if len(swarmNetworkName) > 0 {
networkName = swarmNetworkName
}
@@ -538,8 +540,14 @@ func deployWorker(image string, identifier string, env []string, executionReques
nil,
identifier+"-2",
)
if err != nil {
log.Printf("[ERROR] Failed to CREATE container (2): %s", err)
}
err = dockercli.ContainerStart(context.Background(), cont.ID, containerStartOptions)
if err != nil {
log.Printf("[ERROR] Failed to start container (2): %s", err)
}
} else {
log.Printf("[ERROR] Failed initial container start. Quitting as this is NOT a simple network issue. Err: %s", err)
}
@@ -759,8 +767,12 @@ func main() {
//baseUrl = "http://localhost:5001"
}
if orgId == "" {
log.Printf("[ERROR] Org not defined. Set variable ORG_ID based on your org")
//if orgId == "" {
// log.Printf("[ERROR] Org not defined. Set variable ORG_ID based on your org")
// os.Exit(3)
//}
if environment == "" {
log.Printf("[ERROR] Environment not defined. Set variable ENVIRONMENT_NAME to configure it.")
os.Exit(3)
}
@@ -797,7 +809,7 @@ func main() {
// Run by default from now
zombiecheck(ctx, workerTimeout)
log.Printf("[INFO] Running towards %s with Org %s", baseUrl, orgId)
log.Printf("[INFO] Running towards %s (BASE_URL) with environment name %s", baseUrl, environment)
httpProxy := os.Getenv("HTTP_PROXY")
httpsProxy := os.Getenv("HTTPS_PROXY")
@@ -845,6 +857,8 @@ func main() {
}
}
client.Timeout = 10 * time.Second
fullUrl := fmt.Sprintf("%s/api/v1/workflows/queue", baseUrl)
req, err := http.NewRequest(
"GET",
@@ -859,8 +873,8 @@ func main() {
zombiecounter := 0
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Org-Id", orgId)
log.Printf("[INFO] Waiting for executions at %s with Org ID %s", fullUrl, orgId)
req.Header.Add("Org-Id", environment)
log.Printf("[INFO] Waiting for executions at %s with Environment %s", fullUrl, environment)
hasStarted := false
for {
//go getStats()
@@ -1045,7 +1059,7 @@ func main() {
}
result.Header.Add("Content-Type", "application/json")
result.Header.Add("Org-Id", orgId)
result.Header.Add("Org-Id", environment)
resultResp, err := client.Do(result)
if err != nil {
+54
View File
@@ -0,0 +1,54 @@
#
# curl -H "Org-id: Shuffle" --proxy "http://192.168.86.45:8081" http://192.168.86.45:5001/api/v1/workflows/queue
import SocketServer
import SimpleHTTPServer
import requests
import json
PORT = 8082
class MyProxy(SimpleHTTPServer.SimpleHTTPRequestHandler):
def do_GET(self):
url=self.path[:]
allheaders = {}
for item in ("%s" % self.headers).split("\n"):
headersplit = item.split(":")
if len(headersplit) == 2:
allheaders[headersplit[0]] = (headersplit[1][:-1]).strip()
ret = requests.get(url, headers=allheaders)
print("RESP (%s) - %d - %s" % (url, ret.status_code, ret.text))
self.send_response(ret.status_code)
self.end_headers()
self.wfile.write(ret.text)
def do_POST(self):
url=self.path[:]
allheaders = {}
for item in ("%s" % self.headers).split("\n"):
headersplit = item.split(":")
if len(headersplit) == 2:
allheaders[headersplit[0]] = (headersplit[1][:-1]).strip()
length = int(self.headers.getheader('content-length'))
try:
message = json.loads(self.rfile.read(length))
print("Got message: %s" % message)
ret = requests.post(url, headers=allheaders, json=message)
except:
message = self.rfile.read(length)
print("Got message: %s" % message)
ret = requests.post(url, headers=allheaders, data=message)
print("RESP (%s) - %d - %s" % (url, ret.status_code, ret.text))
self.send_response(ret.status_code)
self.end_headers()
self.wfile.write(ret.text)
httpd = SocketServer.ForkingTCPServer(('', PORT), MyProxy)
print("Now serving at %d" % PORT)
httpd.serve_forever()
+7 -6
View File
@@ -1,9 +1,10 @@
docker run \
--env ORG_ID=$ORG_ID \
--env DOCKER_API_VERSION=1.40 \
--env ENVIRONMENT_NAME="Shuffle" \
--env BASE_URL=http://shuffle-backend:5001 \
--env DOCKER_API_VERSION=1.42 \
--env RUNNING_MODE="Docker" \
--network "shuffle_shuffle" \
--env BASE_URL="http://192.168.86.45:5001" \
--env HTTP_PROXY="http://192.168.86.45:8082" \
--env HTTPS_PROXY="https://192.168.86.45:8082" \
--env SHUFFLE_PASS_WORKER_PROXY=true \
--env SHUFFLE_PASS_APP_PROXY=true \
-v /var/run/docker.sock:/var/run/docker.sock \
frikky/shuffle:orborus
ghcr.io/frikky/shuffle-orborus:nightly
+1 -1
View File
@@ -1,5 +1,5 @@
NAME=shuffle-worker
VERSION=0.9.50
VERSION=0.9.59
echo "Running docker build with $NAME:$VERSION"
#CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin .
+2
View File
@@ -595,6 +595,8 @@ github.com/shuffle/shuffle-shared v0.1.78 h1://YsgQ85Ep40AA3pLUXb+85BUrNz5sqGqh0
github.com/shuffle/shuffle-shared v0.1.78/go.mod h1:cW8LBv8P24rCPyJqGV6czxqrpnrv/R1d97EOqNgIvSk=
github.com/shuffle/shuffle-shared v0.1.81 h1:/lOt7NSuMTWlRzgOKg2e7j95eakg3MgW6i/4Fp30kd4=
github.com/shuffle/shuffle-shared v0.1.81/go.mod h1:cW8LBv8P24rCPyJqGV6czxqrpnrv/R1d97EOqNgIvSk=
github.com/shuffle/shuffle-shared v0.1.83 h1:xfmcqceBGXJVUyZNBmI+c6RKndrtKJyBIqdHD86v/XA=
github.com/shuffle/shuffle-shared v0.1.83/go.mod h1:cW8LBv8P24rCPyJqGV6czxqrpnrv/R1d97EOqNgIvSk=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+67 -7
View File
@@ -358,6 +358,7 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env []
log.Printf("\n\n[DEBUG] Result for %s already found - returning\n\n", newExecId)
return nil
}
cacheData := []byte("1")
err = shuffle.SetCache(ctx, newExecId, cacheData)
if err != nil {
@@ -371,17 +372,19 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env []
waitTime := time.Duration(action.ExecutionDelay) * time.Second
time.AfterFunc(waitTime, func() {
DeployContainer(ctx, cli, config, hostConfig, identifier, workflowExecution)
DeployContainer(ctx, cli, config, hostConfig, identifier, workflowExecution, newExecId)
})
} else {
log.Printf("[DEBUG] Running app %s in docker NORMALLY as there is no delay set", action.Name)
return DeployContainer(ctx, cli, config, hostConfig, identifier, workflowExecution)
log.Printf("[DEBUG] Running app %s in docker NORMALLY as there is no delay set with identifier %s", action.Name, identifier)
returnvalue := DeployContainer(ctx, cli, config, hostConfig, identifier, workflowExecution, newExecId)
log.Printf("[DEBUG] Normal deploy ret: %s", returnvalue)
return returnvalue
}
return nil
}
func DeployContainer(ctx context.Context, cli *dockerclient.Client, config *container.Config, hostConfig *container.HostConfig, identifier string, workflowExecution shuffle.WorkflowExecution) error {
func DeployContainer(ctx context.Context, cli *dockerclient.Client, config *container.Config, hostConfig *container.HostConfig, identifier string, workflowExecution shuffle.WorkflowExecution, newExecId string) error {
cont, err := cli.ContainerCreate(
ctx,
config,
@@ -396,6 +399,11 @@ func DeployContainer(ctx context.Context, cli *dockerclient.Client, config *cont
if !strings.Contains(err.Error(), "Conflict. The container name") {
log.Printf("[ERROR] Container CREATE error (1): %s", err)
cacheErr := shuffle.DeleteCache(ctx, newExecId)
if cacheErr != nil {
log.Printf("[ERROR] FAILED Deleting cache for %s: %s", newExecId, cacheErr)
}
return err
} else {
parsedUuid := uuid.NewV4()
@@ -414,6 +422,12 @@ func DeployContainer(ctx context.Context, cli *dockerclient.Client, config *cont
if err != nil {
log.Printf("[ERROR] Container create error (2): %s", err)
cacheErr := shuffle.DeleteCache(ctx, newExecId)
if cacheErr != nil {
log.Printf("[ERROR] FAILED Deleting cache for %s: %s", newExecId, cacheErr)
}
return err
}
@@ -445,6 +459,12 @@ func DeployContainer(ctx context.Context, cli *dockerclient.Client, config *cont
if err != nil {
log.Printf("[ERROR] Container create error (3): %s", err)
cacheErr := shuffle.DeleteCache(ctx, newExecId)
if cacheErr != nil {
log.Printf("[ERROR] FAILED Deleting cache for %s: %s", newExecId, cacheErr)
}
return err
}
@@ -454,6 +474,12 @@ func DeployContainer(ctx context.Context, cli *dockerclient.Client, config *cont
if err != nil {
log.Printf("[ERROR] Failed to start container in environment %s: %s", environment, err)
cacheErr := shuffle.DeleteCache(ctx, newExecId)
if cacheErr != nil {
log.Printf("[ERROR] FAILED Deleting cache for %s: %s", newExecId, cacheErr)
}
//shutdown(workflowExecution, workflowExecution.Workflow.ID, true)
return err
}
@@ -1163,7 +1189,6 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) {
// if everything is generated during execution
//log.Printf("[DEBUG][%s] Deployed with CALLBACK_URL %s and BASE_URL %s", workflowExecution.ExecutionId, appCallbackUrl, baseUrl)
env := []string{
fmt.Sprintf("ACTION=%s", string(actionData)),
fmt.Sprintf("EXECUTIONID=%s", workflowExecution.ExecutionId),
fmt.Sprintf("AUTHORIZATION=%s", workflowExecution.Authorization),
fmt.Sprintf("CALLBACK_URL=%s", baseUrl),
@@ -1171,6 +1196,40 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) {
fmt.Sprintf("TZ=%s", timezone),
}
if len(actionData) >= 100000 {
log.Printf("[WARNING] Omitting some data from action execution. Length: %d. Fix in SDK!", len(actionData))
newParams := []shuffle.WorkflowAppActionParameter{}
for _, param := range action.Parameters {
paramData, err := json.Marshal(param)
if err != nil {
log.Printf("[WARNING] Failed to marshal param %s: %s", param.Name, err)
newParams = append(newParams, param)
continue
}
if len(paramData) >= 50000 {
log.Printf("[WARNING] Removing a lot of data from param %s with length %d", param.Name, len(paramData))
param.Value = "SHUFFLE_AUTO_REMOVED"
}
newParams = append(newParams, param)
}
action.Parameters = newParams
actionData, err = json.Marshal(action)
if err == nil {
log.Printf("[DEBUG] Ran data replace on action %s. new length: %d", action.Name, len(actionData))
} else {
log.Printf("[WARNING] Failed to marshal new actionData: %s", err)
}
} else {
log.Printf("[DEBUG] Actiondata is NOT 100000 in length. Adding as normal.")
}
actionEnv := fmt.Sprintf("ACTION=%s", string(actionData))
env = append(env, actionEnv)
if strings.ToLower(os.Getenv("SHUFFLE_PASS_APP_PROXY")) == "true" {
//log.Printf("APPENDING PROXY TO THE APP!")
env = append(env, fmt.Sprintf("HTTP_PROXY=%s", os.Getenv("HTTP_PROXY")))
@@ -1296,6 +1355,7 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) {
} else {
err = deployApp(dockercli, images[0], identifier, env, workflowExecution, action)
log.Printf("[DEBUG] Failed deploying app? %s", err)
if err != nil && !strings.Contains(err.Error(), "Conflict. The container name") {
if strings.Contains(err.Error(), "exited prematurely") {
log.Printf("[DEBUG] Shutting down (9)")
@@ -1517,7 +1577,7 @@ func executionInit(workflowExecution shuffle.WorkflowExecution) error {
onpremApps := []string{}
toExecuteOnprem := []string{}
for _, action := range workflowExecution.Workflow.Actions {
if action.Environment != environment {
if strings.ToLower(action.Environment) != strings.ToLower(environment) {
continue
}
@@ -1599,7 +1659,7 @@ func handleDefaultExecution(client *http.Client, req *http.Request, workflowExec
err := executionInit(workflowExecution)
if err != nil {
log.Printf("[INFO] Workflow setup failed: %s", workflowExecution.ExecutionId, err)
log.Printf("[INFO] Workflow setup failed for %s: %s", workflowExecution.ExecutionId, err)
log.Printf("[DEBUG] Shutting down (18)")
shutdown(workflowExecution, "", "", true)
}
+16
View File
@@ -0,0 +1,16 @@
# Mindmap exporter
Shuffle has a mindmap for Workflow use-cases. These can be changed and exported, with the most important piece being that they're explorable and editable. This has and will come in handy for us as we build it into the product.
https://www.mindmeister.com/map/2172644474
## Editing the Mindmap
There are a few categories. To edit them, click the small plus next to the branch you want to change.
## Exporting the Mindmap
Click "Export as RTF" in the top left corner of the URL. Download it there.
## Generating the Shuffle-comaptible mindmap
1. Move the rtf file here
2. Rename it categories.rtf
3. Run the read_categories.py file (python3 read_categories.py)
4. You now have a file called categories.json locally with all the categories in JSON format, ready to be used in graphs.
+260
View File
@@ -0,0 +1,260 @@
[
{
"name": "1. Collect & Distribute",
"color": "#c51152",
"list": [
{
"name": "2-way Ticket synchronization",
"items": {}
},
{
"name": "Email management",
"items": {
"name": "Release a quarantined message",
"items": {}
}
},
{
"name": "EDR to ticket",
"items": {
"name": "Get host information",
"items": {}
}
},
{
"name": "SIEM to ticket",
"items": {}
},
{
"name": "ChatOps",
"items": {}
},
{
"name": "Threat Intel received",
"items": {}
},
{
"name": "Domain investigation with LetsEncrypt",
"items": {}
},
{
"name": "Botnet tracker",
"items": {}
},
{
"name": "Get running containers",
"items": {}
},
{
"name": "Assign tickets",
"items": {}
},
{
"name": "Firewall alerts",
"items": {
"name": "URL filtering",
"items": {}
}
},
{
"name": "IDS/IPS alerts",
"items": {
"name": "Manage policies",
"items": {}
}
},
{
"name": "Deduplicate information",
"items": {}
},
{
"name": "Correlate information",
"items": {}
}
]
},
{
"name": "2. Enrich",
"color": "#f4c20d",
"list": [
{
"name": "Internal Enrichment",
"items": {
"name": "...",
"items": {}
}
},
{
"name": "External historical Enrichment",
"items": {
"name": "...",
"items": {}
}
},
{
"name": "Realtime",
"items": {
"name": "Analyze screenshots",
"items": {}
}
},
{
"name": "Ticketing webhook verification",
"items": {}
}
]
},
{
"name": "3. Detect",
"color": "#3cba54",
"list": [
{
"name": "Search SIEM (Sigma)",
"items": {
"name": "Endpoint",
"items": {}
}
},
{
"name": "Search EDR (OSQuery)",
"items": {}
},
{
"name": "Search emails (Phish)",
"items": {
"name": "Check headers and IOCs",
"items": {}
}
},
{
"name": "Search IOCs (ioc-finder)",
"items": {}
},
{
"name": "Search files (Yara)",
"items": {}
},
{
"name": "Correlate tickets",
"items": {}
},
{
"name": "Honeypot access",
"items": {
"name": "...",
"items": {}
}
}
]
},
{
"name": "4. Respond",
"color": "#4a148c",
"list": [
{
"name": "Eradicate malware",
"items": {}
},
{
"name": "Quarantine host(s)",
"items": {}
},
{
"name": "Trigger scans",
"items": {}
},
{
"name": "Update indicators (FW, EDR, SIEM...)",
"items": {}
},
{
"name": "Autoblock activity when threat intel is received",
"items": {}
},
{
"name": "Lock/Delete/Reset account",
"items": {}
},
{
"name": "Lock vault",
"items": {}
},
{
"name": "Increase authentication",
"items": {}
},
{
"name": "Get policies from assets",
"items": {}
}
]
},
{
"name": "5. Verify",
"color": "#4885ed",
"list": [
{
"name": "Discover vulnerabilities",
"items": {}
},
{
"name": "Discover assets",
"items": {}
},
{
"name": "Ensure policies are followed",
"items": {}
},
{
"name": "Find Inactive users",
"items": {}
},
{
"name": "Ensure access rights match HR systems",
"items": {}
},
{
"name": "Ensure onboarding is followed",
"items": {}
},
{
"name": "Third party apps in SaaS",
"items": {}
},
{
"name": "Devices used for your cloud account",
"items": {}
},
{
"name": "Too much access in GCP/Azure/AWS/ other clouds",
"items": {}
},
{
"name": "Certificate validation",
"items": {}
},
{
"name": "Monitor new DNS entries for domain with passive DNS",
"items": {}
},
{
"name": "Monitor and track password dumps",
"items": {}
},
{
"name": "Monitor for mentions of domain on darknet sites",
"items": {}
},
{
"name": "Reporting",
"items": {
"name": "Monthly reports",
"items": {
"name": "...",
"items": {}
}
}
}
]
}
]
+555
View File
@@ -0,0 +1,555 @@
{\rtf1\ansi\deff0\deflang2057\plain\fs24\fet1
{\fonttbl
{\f0\froman Arial;}
}
{\info
{\createim\yr2022\mo2\dy20\hr1\min15}
}
\paperw11907\paperh16840\margl1800\margr1800\margt1440\margb1440
\slmult0\ltrpar\li0
{\b\fs28
Shuffle categories
}
\par\pard\plain
\slmult0\ltrpar\li200
{\fs24
1. Collect & Distribute
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
2-way Ticket synchronization
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Email management
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Attachments
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Manage senders
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Manage URLs
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Encode & Decode URLs
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Release a quarantined message
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
EDR to ticket
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Fetch incidents & events
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Quarantine files
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Quarantine host (respond)
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Get host information
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
SIEM to ticket
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
ChatOps
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Threat Intel received
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Domain investigation with LetsEncrypt
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Botnet tracker
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Get running containers
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Assign tickets
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Firewall alerts
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Block/accept policies
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Add addresses and ports to groups
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Support custom URL categories
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Fetch logs for specific address
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
URL filtering
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
IDS/IPS alerts
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Get/Fetch alerts
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Receive alerts real-time
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Get PCAP files
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Get network logs
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Manage policies
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Deduplicate information
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Correlate information
}
\par\pard\plain
\slmult0\ltrpar\li200
{\fs24
3. Detect
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Search SIEM (Sigma)
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Network
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Endpoint
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Search EDR (OSQuery)
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Search emails (Phish)
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Check malware
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Check targeted
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Check headers and IOCs
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Search IOCs (ioc-finder)
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Search files (Yara)
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Correlate tickets
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Honeypot access
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
S3 Honeypot
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
SSH Honeypot
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
FTP honeypot
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Network honeypot
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
...
}
\par\pard\plain
\slmult0\ltrpar\li200
{\fs24
rich
}
\par\pard\plain
\slmult0\ltrpar\li200
{\fs24
5. Verify
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Discover vulnerabilities
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Discover assets
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Ensure policies are followed
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Find Inactive users
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Ensure access rights match HR systems
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Ensure onboarding is followed
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Third party apps in SaaS
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Devices used for your cloud account
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Too much access in GCP/Azure/AWS/ other clouds
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Certificate validation
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Monitor new DNS entries for domain with passive DNS
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Monitor and track password dumps
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Monitor for mentions of domain on darknet sites
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Reporting
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Automation time saved
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Automation money saved
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Incident response report
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Department cost
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Monthly reports
}
\par\pard\plain
\slmult0\ltrpar\li800
{\fs24
EDR alerts
}
\par\pard\plain
\slmult0\ltrpar\li800
{\fs24
SIEM alerts
}
\par\pard\plain
\slmult0\ltrpar\li800
{\fs24
Emails quarantined
}
\par\pard\plain
\slmult0\ltrpar\li800
{\fs24
...
}
\par\pard\plain
\slmult0\ltrpar\li200
{\fs24
4. Respond
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Eradicate malware
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Quarantine host(s)
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Trigger scans
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Update indicators (FW, EDR, SIEM...)
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Autoblock activity when threat intel is received
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Lock/Delete/Reset account
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Lock vault
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Increase authentication
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Get policies from assets
}
\par\pard\plain
\slmult0\ltrpar\li200
{\fs24
2. Enrich
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Internal Enrichment
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Users
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Hostnames
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
IPs
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Departments
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Role
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Software
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
...
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
External historical Enrichment
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
IPs
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
URLs
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Hashes
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Files
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
...
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Realtime
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
File detonation
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
URL detonation
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
PCAP analysis
}
\par\pard\plain
\slmult0\ltrpar\li600
{\fs24
Analyze screenshots
}
\par\pard\plain
\slmult0\ltrpar\li400
{\fs24
Ticketing webhook verification
}
\par\pard\plain
}
+66
View File
@@ -0,0 +1,66 @@
data = ""
with open("categories.rtf", "r") as tmp:
data = tmp.read()
fixed_json = []
linearity = 0
heading = ""
subheading = ""
subsubheading = ""
cnt = -1
subcnt = -1
colors = ["#c51152", "#3cba54", "#4885ed", "#4a148c", "#f4c20d"]
for line in data.split("\n"):
if line == "rich":
continue
if "li" in line:
lisplit = line.split("\\")
try:
linearity = int(lisplit[-1][2])
except:
pass
#print("Linearity: %s" % linearity)
if line.startswith("{") or line.startswith("}"):
continue
if line.startswith("\\"):
continue
if linearity == 0:
continue
if linearity == 2:
#if cnt >= 0:
# for key, value in fixed_json[cnt].items():
# print(key, value)
cnt += 1
subcnt = -1
fixed_json.append({"name": line, "color": colors[cnt], "list": []})
heading = line
elif linearity == 4:
subheading = line
fixed_json[cnt]["list"].append({"name": line, "items": {}})
subcnt += 1
elif linearity == 6:
fixed_json[cnt]["list"][subcnt]["items"] = {"name": line, "items": {}}
elif linearity == 8:
fixed_json[cnt]["list"][subcnt]["items"]["items"] = {"name": line, "items": {}}
else:
print("No handler for %s" % line)
#print(line)
#print(data)
import json
filename = "categories.json"
fixed_json.sort(key=lambda x: x["name"])
with open(filename, "w+") as tmp:
tmp.write(json.dumps(fixed_json, indent=4))
print("Wrote to file %s" % filename)