From 727466adb5a9492410394151aebc4cb79f36ed18 Mon Sep 17 00:00:00 2001 From: Frikky Date: Wed, 28 Feb 2024 21:50:00 +0100 Subject: [PATCH 01/25] Removed verbosity from shuffle tools --- shuffle-tools/1.2.0/api.yaml | 2 +- shuffle-tools/1.2.0/src/app.py | 39 ++-------------------------------- 2 files changed, 3 insertions(+), 38 deletions(-) diff --git a/shuffle-tools/1.2.0/api.yaml b/shuffle-tools/1.2.0/api.yaml index 66704dd8..b67bc534 100644 --- a/shuffle-tools/1.2.0/api.yaml +++ b/shuffle-tools/1.2.0/api.yaml @@ -1,7 +1,7 @@ --- app_version: 1.2.0 name: Shuffle Tools -description: A tool app for Shuffle. Gives access to most missing features along with Liquid. +description: A tool app for Shuffle. Gives access to most missing features along with Liquid. tags: - Testing - Shuffle diff --git a/shuffle-tools/1.2.0/src/app.py b/shuffle-tools/1.2.0/src/app.py index 2708309c..5b19f78d 100644 --- a/shuffle-tools/1.2.0/src/app.py +++ b/shuffle-tools/1.2.0/src/app.py @@ -684,8 +684,6 @@ def filter_list(self, input_list, field, check, value, opposite): new_list.append(item) found = True break - else: - print("Nothing matching") if not found: failed_list.append(item) @@ -1533,7 +1531,7 @@ def fix_json(self, json_data): del json_data[key] except Exception as e: - print("[DEBUG] Problem in JSON (fix_json): %s" % e) + pass return json_data @@ -1587,8 +1585,6 @@ def compare_relative_date( ): if timestamp== "None": return False - - print("Converting input date.") if date_format == "autodetect": input_dt = dateutil_parser(timestamp).replace(tzinfo=None) @@ -1632,12 +1628,7 @@ def compare_relative_date( comparison_dt = formatted_dt + delta #comparison_dt = datetime.datetime.utcnow() - print("{} {} {} is {}. Delta: {}".format(offset, units, direction, comparison_dt, delta)) - diff = int((input_dt - comparison_dt).total_seconds()) - print( - "\nDifference between {} and {} is {} seconds ({} days)\n".format(timestamp, comparison_dt, diff, int(diff/86400)) - ) if units == "seconds": diff = diff @@ -1675,19 +1666,6 @@ def compare_relative_date( if direction == "ahead" and diff != 0: result = not (result) - print( - "At {}, is {} {} to {} {} {}? {}. Diff {}".format( - formatted_dt, - timestamp, - equality_test, - offset, - units, - direction, - result, - diff, - ) - ) - parsed_string = "%s %s %s %s" % (equality_test, offset, units, direction) newdiff = diff if newdiff < 0: @@ -1804,11 +1782,8 @@ def check_cache_contains(self, key, value, append): except json.decoder.JSONDecodeError as e: parsedvalue = [str(allvalues["value"])] except Exception as e: - print("Error parsing JSON - overriding: %s" % e) parsedvalue = [str(allvalues["value"])] - print("In ELSE2: '%s'" % parsedvalue) - try: for item in parsedvalue: #return "%s %s" % (item, value) @@ -1835,7 +1810,6 @@ def check_cache_contains(self, key, value, append): # Lol break except Exception as e: - print("Error in check_cache_contains: %s" % e) parsedvalue = [str(parsedvalue)] append = True @@ -1888,7 +1862,6 @@ def check_cache_contains(self, key, value, append): #return allvalues except Exception as e: - print("[ERROR] Failed to handle cache contains: %s" % e) return { "success": False, "key": key, @@ -2197,7 +2170,6 @@ def get_jwt(sa_keyfile, #signer = crypt.RSASigner.from_service_account_file(sa_keyfile) signer = crypt.RSASigner.from_string(sa_keyfile) jwt_token = jwt.encode(signer, payload) - # print(jwt_token.decode('utf-8')) return jwt_token @@ -2435,7 +2407,6 @@ def run_ssh_command(self, host, port, user_name, private_key_file_id, password, except Exception as e: return {"success":"false","message":str(e)} else: - #print("AUTH WITH PASSWORD") try: ssh_client.connect(hostname=host,username=user_name,port=port, password=str(password)) except Exception as e: @@ -2540,14 +2511,10 @@ def _format_result(self, result): for i in val: final_result[key].append(i) elif isinstance(val, dict): - #print(key,":::",val) if key in final_result: if isinstance(val, dict): for k,v in val.items(): - #print("k:",k,"v:",v) val[k].append(v) - #print(val) - #final_result[key].append([i for i in val if len(val) > 0]) else: final_result[key] = val @@ -2560,7 +2527,6 @@ def _with_concurency(self, array_of_strings, ioc_types): # Workers dont matter..? # What can we use instead? - print("Strings:", len(array_of_strings)) workers = 4 with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor: @@ -2577,7 +2543,6 @@ def _with_concurency(self, array_of_strings, ioc_types): # Retrieve the results if needed results = [future.result() for future in futures] - #print("Total time taken:", time.perf_counter()-start) return self._format_result(results) # FIXME: Make this good and actually faster than normal @@ -2646,7 +2611,7 @@ def parse_ioc_new(self, input_string, input_type="all"): try: newarray[i]["is_private_ip"] = ipaddress.ip_address(item["data"]).is_private except Exception as e: - print("Error parsing %s: %s" % (item["data"], e)) + pass try: newarray = json.dumps(newarray) From 7bac3f164f7695f2e66389f60f07b5b7f2786ce8 Mon Sep 17 00:00:00 2001 From: Frikky Date: Thu, 29 Feb 2024 04:25:09 +0100 Subject: [PATCH 02/25] Removed more verbosity for tools --- shuffle-tools/1.2.0/src/app.py | 97 +++++----------------------------- 1 file changed, 13 insertions(+), 84 deletions(-) diff --git a/shuffle-tools/1.2.0/src/app.py b/shuffle-tools/1.2.0/src/app.py index 5b19f78d..740edbd5 100644 --- a/shuffle-tools/1.2.0/src/app.py +++ b/shuffle-tools/1.2.0/src/app.py @@ -111,7 +111,6 @@ def base64_conversion(self, string, operation): except Exception as e: #return string.decode("utf-16") - self.logger.info(f"[WARNING] Error in normal decoding: {e}") return { "success": False, "reason": f"Error decoding the base64: {e}", @@ -121,7 +120,6 @@ def base64_conversion(self, string, operation): # if str(newvar).startswith("b'") and str(newvar).endswith("'"): # newvar = newvar[2:-1] #except Exception as e: - # self.logger.info(f"Encoding issue in base64: {e}") #return newvar #try: @@ -200,7 +198,7 @@ def send_email_shuffle(self, apikey, recipients, subject, body, attachments=""): data["attachments"] = files except Exception as e: - self.logger.info(f"Error in attachment parsing for email: {e}") + pass url = "https://shuffler.io/api/v1/functions/sendmail" @@ -303,7 +301,6 @@ def get_length(self, item): return str(len(item)) def set_json_key(self, json_object, key, value): - self.logger.info(f"OBJ: {json_object}\nKEY: {key}\nVAL: {value}") if isinstance(json_object, str): try: json_object = json.loads(json_object) @@ -350,7 +347,6 @@ def set_json_key(self, json_object, key, value): buildstring += f"[\"{subkey}\"]" buildstring += f" = {value}" - self.logger.info("BUILD: %s" % buildstring) #output = exec(buildstring) @@ -434,7 +430,6 @@ def regex_capture_group(self, input_data, regex): } matches = re.findall(regex, input_data) - self.logger.info(f"{matches}") found = False for item in matches: if isinstance(item, str): @@ -467,19 +462,12 @@ def regex_replace( self, input_data, regex, replace_string="", ignore_case="False" ): - #self.logger.info("=" * 80) - #self.logger.info(f"Regex: {regex}") - #self.logger.info(f"replace_string: {replace_string}") - #self.logger.info("=" * 80) - if ignore_case.lower().strip() == "true": return re.sub(regex, replace_string, input_data, flags=re.IGNORECASE) else: return re.sub(regex, replace_string, input_data) def execute_python(self, code): - self.logger.info(f"Python code {len(code)}. If uuid, we'll try to download and use the file.") - if len(code) == 36 and "-" in code: filedata = self.get_file(code) if filedata["success"] == False: @@ -523,7 +511,6 @@ def custom_print(*args, **kwargs): #try: # s = s.encode("utf-8") #except Exception as e: - # self.logger.info(f"Failed utf-8 encoding response: {e}") try: return { @@ -559,7 +546,6 @@ def execute_bash(self, code, shuffle_input): stdout = process.communicate() item = "" if len(stdout[0]) > 0: - self.logger.info("[DEBUG] Succesfully ran bash!") item = stdout[0] else: self.logger.info(f"[ERROR] FAILED to run bash command {code}!") @@ -588,7 +574,6 @@ def check_wildcard(self, wildcardstring, matching_string): return False def filter_list(self, input_list, field, check, value, opposite): - self.logger.info(f"\nRunning function with list {input_list}") # Remove hashtags on the fly # E.g. #.fieldname or .#.fieldname @@ -621,7 +606,6 @@ def filter_list(self, input_list, field, check, value, opposite): if str(value).lower() == "null" or str(value).lower() == "none": value = "none" - self.logger.info(f"\nRunning with check \"%s\" on list of length %d\n" % (check, len(input_list))) found_items = [] new_list = [] failed_list = [] @@ -642,10 +626,8 @@ def filter_list(self, input_list, field, check, value, opposite): try: tmp = json.dumps(tmp) except json.decoder.JSONDecodeError as e: - self.logger.info("FAILED DECODING: %s" % e) pass - #self.logger.info("PRE CHECKS FOR TMP: %") # EQUALS JUST FOR STR if check == "equals": @@ -653,15 +635,12 @@ def filter_list(self, input_list, field, check, value, opposite): # value = tmp.lower() if str(tmp).lower() == str(value).lower(): - self.logger.info("APPENDED BECAUSE %s %s %s" % (field, check, value)) new_list.append(item) else: failed_list.append(item) elif check == "equals any of": - self.logger.info("Inside equals any of") checklist = value.split(",") - self.logger.info("Checklist and tmp: %s - %s" % (checklist, tmp)) found = False for subcheck in checklist: subcheck = str(subcheck).strip() @@ -732,7 +711,6 @@ def filter_list(self, input_list, field, check, value, opposite): elif check == "contains any of": value = self.parse_list_internal(value) checklist = value.split(",") - self.logger.info("CHECKLIST: %s. Value: %s" % (checklist, tmp)) found = False for checker in checklist: if str(checker).lower() in str(tmp).lower() or self.check_wildcard(checker, tmp): @@ -745,7 +723,6 @@ def filter_list(self, input_list, field, check, value, opposite): # CONTAINS FIND FOR LIST AND IN FOR STR elif check == "field is unique": - #self.logger.info("FOUND: %s" if tmp.lower() not in found_items: new_list.append(item) found_items.append(tmp.lower()) @@ -761,13 +738,12 @@ def filter_list(self, input_list, field, check, value, opposite): new_list.append(item) list_set = True except AttributeError as e: - self.logger.info("FAILED CHECKING LARGER THAN: %s" % e) pass try: value = len(json.loads(value)) except Exception as e: - self.logger.info(f"[WARNING] Failed to convert destination to list: {e}") + pass try: # Check if it's a list in autocast and if so, check the length @@ -775,7 +751,7 @@ def filter_list(self, input_list, field, check, value, opposite): new_list.append(item) list_set = True except Exception as e: - self.logger.info(f"[WARNING] Failed to check if larger than as list: {e}") + pass if not list_set: failed_list.append(item) @@ -793,13 +769,12 @@ def filter_list(self, input_list, field, check, value, opposite): new_list.append(item) list_set = True except AttributeError as e: - self.logger.info("FAILED CHECKING LARGER THAN: %s" % e) pass try: value = len(json.loads(value)) except Exception as e: - self.logger.info(f"[WARNING] Failed to convert destination to list: {e}") + pass try: # Check if it's a list in autocast and if so, check the length @@ -807,7 +782,7 @@ def filter_list(self, input_list, field, check, value, opposite): new_list.append(item) list_set = True except Exception as e: - self.logger.info(f"[WARNING] Failed to check if larger than as list: {e}") + pass if not list_set: failed_list.append(item) @@ -859,7 +834,6 @@ def filter_list(self, input_list, field, check, value, opposite): failed_list.append(item) except Exception as e: - self.logger.info("[WARNING] FAILED WITH EXCEPTION: %s" % e) failed_list.append(item) # return @@ -954,7 +928,6 @@ def get_file_meta(self, file_id): headers=headers, verify=False, ) - self.logger.info(f"RET: {ret}") return ret.text @@ -963,7 +936,6 @@ def delete_file(self, file_id): headers = { "Authorization": "Bearer %s" % self.authorization, } - self.logger.info("HEADERS: %s" % headers) ret = requests.delete( "%s/api/v1/files/%s?execution_id=%s" @@ -974,8 +946,6 @@ def delete_file(self, file_id): return ret.text def create_file(self, filename, data): - self.logger.info("Inside function") - try: if str(data).startswith("b'") and str(data).endswith("'"): data = data[2:-1] @@ -1013,7 +983,6 @@ def get_file_value(self, filedata): if filedata is None: return "File is empty?" - self.logger.info("INSIDE APP DATA: %s" % filedata) try: return filedata["data"].decode() except: @@ -1064,7 +1033,6 @@ def extract_archive(self, file_id, fileformat="zip", password=None): item = self.get_file(file_id) return_ids = None - self.logger.info("Working with fileformat %s" % fileformat) with tempfile.TemporaryDirectory() as tmpdirname: # Get archive and save phisically @@ -1076,13 +1044,10 @@ def extract_archive(self, file_id, fileformat="zip", password=None): # Zipfile for zipped archive if fileformat.strip().lower() == "zip": try: - self.logger.info("Starting zip extraction") with zipfile.ZipFile(os.path.join(tmpdirname, "archive")) as z_file: if password: - self.logger.info("In zip extraction with password") z_file.setpassword(bytes(password.encode())) - self.logger.info("Past zip extraction") for member in z_file.namelist(): filename = os.path.basename(member) if not filename: @@ -1216,10 +1181,8 @@ def extract_archive(self, file_id, fileformat="zip", password=None): else: return "No such format: %s" % fileformat - self.logger.info("Breaking as this only handles one archive at a time.") if len(to_be_uploaded) > 0: return_ids = self.set_files(to_be_uploaded) - self.logger.info(f"Got return ids from files: {return_ids}") for i in range(len(return_ids)): return_data["archive_id"] = file_id @@ -1239,7 +1202,6 @@ def extract_archive(self, file_id, fileformat="zip", password=None): } ) else: - self.logger.info(f"No file ids to upload.") return_data["success"] = False return_data["files"].append( { @@ -1273,7 +1235,6 @@ def create_archive(self, file_ids, fileformat, name, password=None): "reason": "Make sure to send valid file ids. Example: file_13eea837-c56a-4d52-a067-e673c7186483,file_13eea837-c56a-4d52-a067-e673c7186484", } - self.logger.info("picking {}".format(file_ids)) # GET all items from shuffle items = [self.get_file(file_id) for file_id in file_ids] @@ -1283,14 +1244,12 @@ def create_archive(self, file_ids, fileformat, name, password=None): # Dump files on disk, because libs want path :( with tempfile.TemporaryDirectory() as tmpdir: paths = [] - self.logger.info("Number 1") for item in items: with open(os.path.join(tmpdir, item["filename"]), "wb") as f: f.write(item["data"]) paths.append(os.path.join(tmpdir, item["filename"])) # Create archive temporary - self.logger.info("{} items to inflate".format(len(items))) with tempfile.NamedTemporaryFile() as archive: if fileformat == "zip": @@ -1336,7 +1295,6 @@ def add_list_to_list(self, list_one, list_two): try: list_one = json.loads(list_one) except json.decoder.JSONDecodeError as e: - self.logger.info("Failed to parse list1 as json: %s" % e) if list_one == None: list_one = [] else: @@ -1352,7 +1310,6 @@ def add_list_to_list(self, list_one, list_two): try: list_two = json.loads(list_two) except json.decoder.JSONDecodeError as e: - self.logger.info("Failed to parse list2 as json: %s" % e) if list_one == None: list_one = [] else: @@ -1376,7 +1333,6 @@ def diff_lists(self, list_one, list_two): try: list_one = json.loads(list_one) except json.decoder.JSONDecodeError as e: - self.logger.info("Failed to parse list1 as json: %s" % e) return { "success": False, "reason": "list_one is not a valid list." @@ -1386,7 +1342,6 @@ def diff_lists(self, list_one, list_two): try: list_two = json.loads(list_two) except json.decoder.JSONDecodeError as e: - self.logger.info("Failed to parse list2 as json: %s" % e) return { "success": False, "reason": "list_two is not a valid list." @@ -1433,13 +1388,13 @@ def merge_lists(self, list_one, list_two, set_field="", sort_key_list_one="", so try: list_one = json.loads(list_one) except json.decoder.JSONDecodeError as e: - self.logger.info("Failed to parse list1 as json: %s" % e) + pass if isinstance(list_two, str): try: list_two = json.loads(list_two) except json.decoder.JSONDecodeError as e: - self.logger.info("Failed to parse list2 as json: %s" % e) + pass if not isinstance(list_one, list) or not isinstance(list_two, list): if isinstance(list_one, dict) and isinstance(list_two, dict): @@ -1454,19 +1409,15 @@ def merge_lists(self, list_one, list_two, set_field="", sort_key_list_one="", so return {"success": False, "message": "Lists length must be the same. %d vs %d" % (len(list_one), len(list_two))} if len(sort_key_list_one) > 0: - self.logger.info("Sort 1 %s by key: %s" % (list_one, sort_key_list_one)) try: list_one = sorted(list_one, key=lambda k: k.get(sort_key_list_one), reverse=True) except: - self.logger.info("Failed to sort list one") pass if len(sort_key_list_two) > 0: - #self.logger.info("Sort 2 %s by key: %s" % (list_two, sort_key_list_two)) try: list_two = sorted(list_two, key=lambda k: k.get(sort_key_list_two), reverse=True) except: - self.logger.info("Failed to sort list one") pass # Loops for each item in sub array and merges items together @@ -1474,16 +1425,13 @@ def merge_lists(self, list_one, list_two, set_field="", sort_key_list_one="", so base_key = "shuffle_auto_merge" try: for i in range(len(list_one)): - #self.logger.info(list_two[i]) if isinstance(list_two[i], dict): for key, value in list_two[i].items(): list_one[i][key] = value elif isinstance(list_two[i], str) and list_two[i] == "": continue elif isinstance(list_two[i], str) or isinstance(list_two[i], int) or isinstance(list_two[i], bool): - self.logger.info("IN SETTER FOR %s" % list_two[i]) if len(set_field) == 0: - self.logger.info("Define a JSON key to set for List two (Set Field)") list_one[i][base_key] = list_two[i] else: set_field = set_field.replace(" ", "_", -1) @@ -1561,13 +1509,6 @@ def xml_json_convertor(self, convertto, data): } def date_to_epoch(self, input_data, date_field, date_format): - - self.logger.info( - "Executing with {} on {} with format {}".format( - input_data, date_field, date_format - ) - ) - if isinstance(input_data, str): result = json.loads(input_data) else: @@ -1683,7 +1624,6 @@ def compare_relative_date( def run_math_operation(self, operation): - self.logger.info("Operation: %s" % operation) result = eval(operation) return result @@ -1694,8 +1634,6 @@ def escape_html(self, input_data): else: mapping = input_data - self.logger.info(f"Got mapping {json.dumps(mapping, indent=2)}") - result = markupsafe.escape(mapping) return mapping @@ -1715,7 +1653,7 @@ def check_cache_contains(self, key, value, append): try: value = json.dumps(value) except Exception as e: - self.logger.info(f"[WARNING] Error in JSON dumping (cache contains): {e}") + pass if not isinstance(value, str): value = str(value) @@ -1856,8 +1794,6 @@ def check_cache_contains(self, key, value, append): "search": value, "key": key } - - self.logger.info("Handle all values!") #return allvalues @@ -1891,6 +1827,7 @@ def change_cache_subkey(self, key, subkey, value, overwrite): value = json.dumps(value) except Exception as e: self.logger.info(f"[WARNING] Error in JSON dumping (set cache): {e}") + elif not isinstance(value, str): value = str(value) @@ -1940,9 +1877,7 @@ def get_cache_value(self, key): value = requests.post(url, json=data, verify=False) try: allvalues = value.json() - #self.logger.info("VAL1: ", allvalues) allvalues["key"] = key - #self.logger.info("VAL2: ", allvalues) if allvalues["success"] == True and len(allvalues["value"]) > 0: allvalues["found"] = True @@ -1955,7 +1890,6 @@ def get_cache_value(self, key): allvalues["value"] = parsedvalue except: - self.logger.info("Parsing of value as JSON failed") pass return json.dumps(allvalues) @@ -2026,7 +1960,6 @@ def convert_json_to_tags(self, json_object, split_value=", ", include_key=True, parsedstring = [] try: for key, value in json_object.items(): - self.logger.info("KV: %s:%s" % (key, value)) if isinstance(value, str) or isinstance(value, int) or isinstance(value, bool): if include_key == True: parsedstring.append("%s:%s" % (key, value)) @@ -2047,15 +1980,11 @@ def convert_json_to_tags(self, json_object, split_value=", ", include_key=True, return fullstring def cidr_ip_match(self, ip, networks): - self.logger.info("Executing with\nIP: {},\nNetworks: {}".format(ip, networks)) if isinstance(networks, str): try: networks = json.loads(networks) except json.decoder.JSONDecodeError as e: - self.logger.info("Failed to parse networks list as json: {}. Type: {}".format( - e, type(networks) - )) return { "success": False, "reason": "Networks is not a valid list: {}".format(networks), @@ -2079,7 +2008,7 @@ def cidr_ip_match(self, ip, networks): def get_timestamp(self, time_format): timestamp = int(time.time()) if time_format == "unix" or time_format == "epoch": - self.logger.info("Running default timestamp %s" % timestamp) + pass return timestamp @@ -2090,12 +2019,12 @@ def get_hash_sum(self, value): try: md5_value = hashlib.md5(str(value).encode('utf-8')).hexdigest() except Exception as e: - self.logger.info(f"Error in md5sum: {e}") + pass try: sha256_value = hashlib.sha256(str(value).encode('utf-8')).hexdigest() except Exception as e: - self.logger.info(f"Error in sha256: {e}") + pass parsedvalue = { "success": True, @@ -2465,7 +2394,7 @@ def parse_ioc(self, input_string, input_type="all"): try: item["is_private_ip"] = ipaddress.ip_address(item["data"]).is_private except: - self.logger.info("Error parsing %s" % item["data"]) + pass try: newarray = json.dumps(newarray) From 1a10115df2bccbcef91af4bb1eee2e5fb3696744 Mon Sep 17 00:00:00 2001 From: Frikky Date: Thu, 29 Feb 2024 13:56:36 +0100 Subject: [PATCH 03/25] Added local cache for check cache contains in case we are checking with a list. Reduces api requests drastically, but MAY cause minor inconsistencies --- shuffle-tools/1.2.0/src/app.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/shuffle-tools/1.2.0/src/app.py b/shuffle-tools/1.2.0/src/app.py index 740edbd5..12464bee 100644 --- a/shuffle-tools/1.2.0/src/app.py +++ b/shuffle-tools/1.2.0/src/app.py @@ -1649,6 +1649,20 @@ def check_cache_contains(self, key, value, append): "key": key, } + allvalues = {} + try: + for item in self.local_storage: + if item["execution_id"] == self.current_execution_id and item["key"] == key: + # Max keeping the local cache properly for 5 seconds due to workflow continuations + elapsed_time = time.time() - item["time_set"] + if elapsed_time > 5: + break + + allvalues = item["data"] + + except Exception as e: + print("[ERROR] Failed cache contains for current execution id local storage: %s" % e) + if isinstance(value, dict) or isinstance(value, list): try: value = json.dumps(value) @@ -1665,9 +1679,13 @@ def check_cache_contains(self, key, value, append): else: append = False - get_response = requests.post(url, json=data, verify=False) + if "success" not in allvalues: + get_response = requests.post(url, json=data, verify=False) + try: - allvalues = get_response.json() + if "success" not in allvalues: + allvalues = get_response.json() + try: if allvalues["value"] == None or allvalues["value"] == "null": allvalues["value"] = "[]" @@ -1686,6 +1704,7 @@ def check_cache_contains(self, key, value, append): #allvalues["key"] = key #return allvalues + return { "success": True, "found": False, @@ -1727,6 +1746,14 @@ def check_cache_contains(self, key, value, append): #return "%s %s" % (item, value) if item == value: if not append: + try: + newdata = json.loads(json.dumps(data)) + newdata["time_set"] = time.time() + newdata["data"] = allvalues + self.local_storage.append(newdata) + except Exception as e: + print("[ERROR] Failed in local storage append: %s" % e) + return { "success": True, "found": True, @@ -1798,6 +1825,7 @@ def check_cache_contains(self, key, value, append): #return allvalues except Exception as e: + print("[ERROR] Failed check cache contains: %s" % e) return { "success": False, "key": key, From 881a6054815b3b20187daa3ca8d58950721c96da Mon Sep 17 00:00:00 2001 From: Frikky Date: Mon, 11 Mar 2024 21:06:05 +0100 Subject: [PATCH 04/25] Added a branch result auto merger to make adding more branches to something dynamic --- shuffle-tools/1.2.0/api.yaml | 12 ++++++ shuffle-tools/1.2.0/src/app.py | 67 ++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/shuffle-tools/1.2.0/api.yaml b/shuffle-tools/1.2.0/api.yaml index b67bc534..b792959a 100644 --- a/shuffle-tools/1.2.0/api.yaml +++ b/shuffle-tools/1.2.0/api.yaml @@ -1094,6 +1094,18 @@ actions: - false schema: type: string + - name: merge_incoming_branches + description: 'Merges the data of incoming branches. Uses the input type to determine how to merge the data, and removes duplicates' + parameters: + - name: input_type + description: What type to use + required: false + multiline: false + example: 'list' + options: + - list + schema: + type: string - name: run_ssh_command description: 'Run a command on remote machine with SSH' parameters: diff --git a/shuffle-tools/1.2.0/src/app.py b/shuffle-tools/1.2.0/src/app.py index 12464bee..bc4e10a5 100644 --- a/shuffle-tools/1.2.0/src/app.py +++ b/shuffle-tools/1.2.0/src/app.py @@ -1845,7 +1845,6 @@ def check_cache_contains(self, key, value, append): ## subkey = "hi", value = "test3", overwrite=False ## {"subkey": "hi", "value": ["test2", "test3"]} - #def set_cache_value(self, key, value): def change_cache_subkey(self, key, subkey, value, overwrite): org_id = self.full_execution["workflow"]["execution_org"]["id"] url = "%s/api/v1/orgs/%s/set_cache" % (self.url, org_id) @@ -1925,7 +1924,6 @@ def get_cache_value(self, key): self.logger.info("Value couldn't be parsed, or json dump of value failed") return value.text - # FIXME: Add option for org only & sensitive data (not to be listed) def set_cache_value(self, key, value): org_id = self.full_execution["workflow"]["execution_org"]["id"] url = "%s/api/v1/orgs/%s/set_cache" % (self.url, org_id) @@ -2020,7 +2018,7 @@ def cidr_ip_match(self, ip, networks): try: ip_networks = list(map(ipaddress.ip_network, networks)) - ip_address = ipaddress.ip_address(ip) + ip_address = ipaddress.ip_address(ip, False) except ValueError as e: return "IP or some networks are not in valid format.\nError: {}".format(e) @@ -2577,6 +2575,69 @@ def parse_ioc_new(self, input_string, input_type="all"): return newarray + def merge_incoming_branches(self, input_type="list"): + wf = self.full_execution["workflow"] + if "branches" not in wf or not wf["branches"]: + return { + "success": False, + "reason": "No branches found" + } + + if "results" not in self.full_execution or not self.full_execution["results"]: + return { + "success": False, + "reason": "No results for previous actions not found" + } + + if not input_type: + input_type = "list" + + branches = wf["branches"] + cur_action = self.action + #print("Found %d branches" % len(branches)) + + results = [] + for branch in branches: + if branch["destination_id"] != cur_action["id"]: + continue + + # Find result for the source + source_id = branch["source_id"] + + for res in self.full_execution["results"]: + if res["action"]["id"] != source_id: + continue + + try: + parsed = json.loads(res["result"]) + results.append(parsed) + except Exception as e: + results.append(res["result"]) + + break + + if input_type == "list": + newlist = [] + for item in results: + if not isinstance(item, list): + continue + + for subitem in item: + if subitem in newlist: + continue + + newlist.append(subitem) + #newlist.append(item) + + results = newlist + else: + return { + "success": False, + "reason": "No results from source branches with type %s" % input_type + } + + return results + def list_cidr_ips(self, cidr): defaultreturn = { "success": False, From c3b3585f2b354fb87f3950b7e5771e7fb0e39d0c Mon Sep 17 00:00:00 2001 From: Frikky Date: Mon, 25 Mar 2024 12:56:37 +0100 Subject: [PATCH 05/25] Pushing email stuff --- email/1.2.0/requirements.txt | 2 +- email/1.2.0/src/app.py | 3 +-- email/1.3.0/api.yaml | 19 +++++++++++++++++++ email/1.3.0/src/app.py | 19 ++++++++++++++++++- shuffle-tools/1.2.0/src/app.py | 12 +++++++++++- 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/email/1.2.0/requirements.txt b/email/1.2.0/requirements.txt index 926027e8..7f8dacf7 100644 --- a/email/1.2.0/requirements.txt +++ b/email/1.2.0/requirements.txt @@ -1,6 +1,6 @@ requests==2.25.1 glom==20.11.0 -eml-parser==1.17.0 +eml-parser==1.17.5 msg-parser==1.2.0 mail-parser==3.15.0 extract-msg==0.30.9 diff --git a/email/1.2.0/src/app.py b/email/1.2.0/src/app.py index 4a27c9b2..23993bf8 100644 --- a/email/1.2.0/src/app.py +++ b/email/1.2.0/src/app.py @@ -391,10 +391,9 @@ def parse_email_file(self, file_id, file_extension): "reason": "Couldn't get file with ID %s" % file_id } - print("File: %s" % file_path) if file_extension.lower() == 'eml': print('working with .eml file') - ep = eml_parser.EmlParser(include_attachment_data=True, include_raw_body=True, parse_attachment=True) + ep = eml_parser.EmlParser(include_attachment_data=True, include_raw_body=True, parse_attachments=True) try: parsed_eml = ep.decode_email_bytes(file_path['data']) if str(parsed_eml["header"]["date"]) == "1970-01-01 00:00:00+00:00": diff --git a/email/1.3.0/api.yaml b/email/1.3.0/api.yaml index 40998adf..017222d7 100644 --- a/email/1.3.0/api.yaml +++ b/email/1.3.0/api.yaml @@ -233,6 +233,25 @@ actions: required: false schema: type: bool + - name: parse_eml + description: Takes an eml string and parses it to JSON + parameters: + - name: filedata + description: The EML string data + required: true + multiline: true + example: 'EML string data' + schema: + type: string + - name: extract_attachments + description: Whether to extract the attachments straight into files + required: true + options: + - true + - false + example: 'true' + schema: + type: string - name: parse_email_file description: Takes a file from shuffle and analyzes it if it's a valid .eml or .msg parameters: diff --git a/email/1.3.0/src/app.py b/email/1.3.0/src/app.py index 62b3a26d..f80a17c0 100644 --- a/email/1.3.0/src/app.py +++ b/email/1.3.0/src/app.py @@ -384,8 +384,25 @@ def merge(d1, d2): "messages": json.dumps(emails, default=default), } + def parse_eml(self, filedata, extract_attachments=False): + parsedfile = { + "success": True, + "filename": "email.eml", + "data": filedata, + } + + return self.parse_email_file(parsedfile, extract_attachments) + def parse_email_file(self, file_id, extract_attachments=False): - file_path = self.get_file(file_id) + file_path = { + "success": False, + } + + if isinstance(file_id, dict) and "data" in file_id: + file_path = file_id + else: + file_path = self.get_file(file_id) + if file_path["success"] == False: return { "success": False, diff --git a/shuffle-tools/1.2.0/src/app.py b/shuffle-tools/1.2.0/src/app.py index bc4e10a5..c68c006d 100644 --- a/shuffle-tools/1.2.0/src/app.py +++ b/shuffle-tools/1.2.0/src/app.py @@ -981,7 +981,16 @@ def list_file_category_ids(self, file_category): def get_file_value(self, filedata): filedata = self.get_file(filedata) if filedata is None: - return "File is empty?" + return { + "success": False, + "reason": "File not found", + } + + if "data" not in filedata: + return { + "success": False, + "reason": "File content not found. File might be empty or not exist", + } try: return filedata["data"].decode() @@ -998,6 +1007,7 @@ def get_file_value(self, filedata): return { "success": False, "reason": "Got the file, but the encoding can't be printed", + "size": len(filedata["data"]), } def download_remote_file(self, url, custom_filename=""): From db82f75bca0a3e944d983e458a05e642410d40dc Mon Sep 17 00:00:00 2001 From: ausef <62292266+ausef@users.noreply.github.com> Date: Thu, 11 Apr 2024 10:29:19 +0200 Subject: [PATCH 06/25] Update requirements.txt --- velociraptor/1.0.0/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/velociraptor/1.0.0/requirements.txt b/velociraptor/1.0.0/requirements.txt index ad6e959c..7698bd2c 100644 --- a/velociraptor/1.0.0/requirements.txt +++ b/velociraptor/1.0.0/requirements.txt @@ -1 +1 @@ -pyvelociraptor==0.1.6 +pyvelociraptor==0.1.8 From d2d16c84e8f9f773f31d2bb5828f0b19d2ee955e Mon Sep 17 00:00:00 2001 From: Frikky Date: Fri, 12 Apr 2024 17:45:24 +0200 Subject: [PATCH 07/25] Fixed email, ai & tools parsing app bugs --- email/1.3.0/src/app.py | 13 +++++ shuffle-ai/1.0.0/api.yaml | 30 +++++++++++ shuffle-ai/1.0.0/requirements.txt | 1 + shuffle-ai/1.0.0/src/app.py | 85 ++++++++++++++++++++++++++++--- shuffle-tools/1.2.0/src/app.py | 2 + 5 files changed, 124 insertions(+), 7 deletions(-) diff --git a/email/1.3.0/src/app.py b/email/1.3.0/src/app.py index f80a17c0..62ecfef2 100644 --- a/email/1.3.0/src/app.py +++ b/email/1.3.0/src/app.py @@ -409,6 +409,19 @@ def parse_email_file(self, file_id, extract_attachments=False): "reason": "Couldn't get file with ID %s" % file_id } + #print("PRE: ", file_path) + + # Check if data is in base64 and decode it + # If it ends with = then it may be bas64 + + if str(file_path["data"]).endswith("="): + try: + file_path["data"] = base64.b64decode(file_path["data"]) + except Exception as e: + print(f"Failed to decode base64: {e}") + + #print("POST: ", file_path) + #print("File: %s" % file_path) print('working with .eml file? %s' % file_path["filename"]) diff --git a/shuffle-ai/1.0.0/api.yaml b/shuffle-ai/1.0.0/api.yaml index ae0ae960..a247fad5 100644 --- a/shuffle-ai/1.0.0/api.yaml +++ b/shuffle-ai/1.0.0/api.yaml @@ -98,6 +98,36 @@ actions: returns: schema: type: string + - name: run_schemaless + description: Runs an automatically translated action + parameters: + - name: category + description: The category the action is in + required: true + multiline: false + schema: + type: string + - name: action + description: The action label to run + required: true + multiline: false + schema: + type: string + - name: app_name + description: The app to run the action in + required: false + multiline: false + schema: + type: string + - name: fields + description: The additional fields to add + required: false + multiline: false + schema: + type: string + returns: + schema: + type: string - name: transcribe_audio description: Returns text from audio parameters: diff --git a/shuffle-ai/1.0.0/requirements.txt b/shuffle-ai/1.0.0/requirements.txt index b1fb92b5..a783f817 100644 --- a/shuffle-ai/1.0.0/requirements.txt +++ b/shuffle-ai/1.0.0/requirements.txt @@ -1,3 +1,4 @@ pytesseract pdf2image pypdf2 +requests diff --git a/shuffle-ai/1.0.0/src/app.py b/shuffle-ai/1.0.0/src/app.py index 4a76c673..42e6e09c 100644 --- a/shuffle-ai/1.0.0/src/app.py +++ b/shuffle-ai/1.0.0/src/app.py @@ -1,8 +1,9 @@ -import pytesseract -from pdf2image import convert_from_path -import PyPDF2 import json +import PyPDF2 import tempfile +import requests +import pytesseract +from pdf2image import convert_from_path from walkoff_app_sdk.app_base import AppBase @@ -59,10 +60,6 @@ def generate_report(self, apikey, input_data, report_title, report_name="generat report_name = report_name + ".html" report_name = report_name.replace(" ", "_", -1) - - if not formatting: - formatting = "auto" - output_formatting= "Format the following text into an HTML report with relevant graphs and tables. Title of the report should be {report_title}." ret = requests.post( "https://shuffler.io/api/v1/conversation", @@ -217,5 +214,79 @@ def gpt(self, input_text): "reason": "Not implemented yet" } + def run_schemaless(self, category, action, app_name="", fields=""): + """ + action := shuffle.CategoryAction{ + Label: step.Name, + Category: step.Category, + AppName: step.AppName, + Fields: step.Fields, + + Environment: step.Environment, + + SkipWorkflow: true, + } + """ + + data = { + "label": action, + "category": category, + + "app_name": "", + "fields": [], + + "skip_workflow": True, + } + + if app_name: + data["app_name"] = app_name + + if fields: + if isinstance(fields, list): + data["fields"] = fields + + elif isinstance(fields, dict): + for key, value in fields.items(): + data["fields"].append({ + "key": key, + "value": str(value), + }) + + else: + try: + loadedfields = json.loads(fields) + for key, value in loadedfields.items(): + data["fields"].append({ + "key": key, + "value": value, + }) + + except Exception as e: + print("[ERROR] Failed to load fields as JSON: %s" % e) + return json.dumps({ + "success": False, + "reason": "Ensure Fields are valid JSON", + "type": type(fields), + "details": "%s" % e, + }) + + + baseurl = "%s/api/v1/apps/categories/run" % self.base_url + baseurl += "?execution_id=%s&authorization=%s" % (self.current_execution_id, self.authorization) + + print("[DEBUG] Running schemaless action with URL '%s', category %s and action label %s" % (baseurl, category, action)) + + headers = {} + request = requests.post( + baseurl, + json=data, + headers=headers, + ) + + try: + return request.json() + except: + return request.text + if __name__ == "__main__": Tools.run() diff --git a/shuffle-tools/1.2.0/src/app.py b/shuffle-tools/1.2.0/src/app.py index c68c006d..45160b86 100644 --- a/shuffle-tools/1.2.0/src/app.py +++ b/shuffle-tools/1.2.0/src/app.py @@ -2394,6 +2394,8 @@ def parse_ioc(self, input_string, input_type="all"): input_type = input_type.split(",") for item in input_type: item = item.strip() + if not item.endswith("s"): + item = "%ss" % item ioc_types = input_type From 655302ff7d80f341892fb0ccbdb5aeae9a95186f Mon Sep 17 00:00:00 2001 From: Frikky Date: Wed, 17 Apr 2024 17:51:33 +0200 Subject: [PATCH 08/25] Fixed timeout bug in PATCH HTTP action --- http/1.4.0/src/app.py | 5 +++++ shuffle-tools/1.2.0/api.yaml | 2 ++ 2 files changed, 7 insertions(+) diff --git a/http/1.4.0/src/app.py b/http/1.4.0/src/app.py index ff2ec91b..865d223e 100755 --- a/http/1.4.0/src/app.py +++ b/http/1.4.0/src/app.py @@ -304,6 +304,11 @@ def PATCH(self, url, headers="", body="", username="", password="", verify=True, else: auth = requests.auth.HTTPBasicAuth(username, password) + if not timeout: + timeout = 5 + if timeout: + timeout = int(timeout) + if to_file == "true": to_file = True else: diff --git a/shuffle-tools/1.2.0/api.yaml b/shuffle-tools/1.2.0/api.yaml index b792959a..a31a363d 100644 --- a/shuffle-tools/1.2.0/api.yaml +++ b/shuffle-tools/1.2.0/api.yaml @@ -282,6 +282,8 @@ actions: required: false multiline: false example: "domains,urls,email_addresses,ipv4s,ipv4_cidrs,ipv6s,md5s,sha256s,sha1s,cves" + value: "domains,urls,ipv4s,md5s,sha1s" + multiselect: true schema: type: string returns: From d46b22098be0b487cc1a136a9f65beae54b4ccbf Mon Sep 17 00:00:00 2001 From: dhaval055 Date: Fri, 19 Apr 2024 10:23:20 +0000 Subject: [PATCH 09/25] Updated send_email_smtp action to support cc --- email/1.3.0/api.yaml | 7 +++++++ email/1.3.0/src/app.py | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/email/1.3.0/api.yaml b/email/1.3.0/api.yaml index 017222d7..3a822c6a 100644 --- a/email/1.3.0/api.yaml +++ b/email/1.3.0/api.yaml @@ -83,6 +83,13 @@ actions: required: true schema: type: string + - name: cc_emails + description: cc_emails + multiline: false + example: "test@gmail.com" + required: false + schema: + type: string - name: subject description: The subject of the email multiline: false diff --git a/email/1.3.0/src/app.py b/email/1.3.0/src/app.py index 62ecfef2..4d92dbee 100644 --- a/email/1.3.0/src/app.py +++ b/email/1.3.0/src/app.py @@ -75,7 +75,7 @@ def send_email_shuffle(self, apikey, recipients, subject, body): return requests.post(url, headers=headers, json=data).text def send_email_smtp( - self, smtp_host, recipient, subject, body, smtp_port, attachments="", username="", password="", ssl_verify="True", body_type="html" + self, smtp_host, recipient, cc_emails ,subject, body, smtp_port, attachments="", username="", password="", ssl_verify="True", body_type="html", ): if type(smtp_port) == str: try: @@ -112,6 +112,10 @@ def send_email_smtp( msg["From"] = username msg["To"] = recipient msg["Subject"] = subject + + if cc_emails: + msg["cc"] = cc_emails + msg.attach(MIMEText(body, body_type)) # Read the attachments From b03e318514c008164ff8f66e3f1005f0551c5627 Mon Sep 17 00:00:00 2001 From: dhaval055 Date: Fri, 19 Apr 2024 10:57:38 +0000 Subject: [PATCH 10/25] fixed success message --- email/1.3.0/api.yaml | 2 +- email/1.3.0/src/app.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/email/1.3.0/api.yaml b/email/1.3.0/api.yaml index 3a822c6a..12679e9d 100644 --- a/email/1.3.0/api.yaml +++ b/email/1.3.0/api.yaml @@ -86,7 +86,7 @@ actions: - name: cc_emails description: cc_emails multiline: false - example: "test@gmail.com" + example: "frikky@shuffler.io,frikky@shuffler.io" required: false schema: type: string diff --git a/email/1.3.0/src/app.py b/email/1.3.0/src/app.py index 4d92dbee..0da967ce 100644 --- a/email/1.3.0/src/app.py +++ b/email/1.3.0/src/app.py @@ -114,7 +114,7 @@ def send_email_smtp( msg["Subject"] = subject if cc_emails: - msg["cc"] = cc_emails + msg["Cc"] = cc_emails msg.attach(MIMEText(body, body_type)) @@ -165,7 +165,7 @@ def send_email_smtp( self.logger.info("Successfully sent email with subject %s to %s" % (subject, recipient)) return { "success": True, - "reason": "Email sent to %s!" % recipient, + "reason": "Email sent to %s, %s!" %(recipient,cc_emails) if cc_emails else "Email sent to %s!" % recipient, "attachments": attachment_count } From 9a75d9536f790ec17d09a6e4d857015ca8c387a9 Mon Sep 17 00:00:00 2001 From: dhaval055 Date: Mon, 22 Apr 2024 09:32:42 +0000 Subject: [PATCH 11/25] made cc_emails field optional in Python --- email/1.3.0/src/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/email/1.3.0/src/app.py b/email/1.3.0/src/app.py index 0da967ce..0b7feb45 100644 --- a/email/1.3.0/src/app.py +++ b/email/1.3.0/src/app.py @@ -75,7 +75,7 @@ def send_email_shuffle(self, apikey, recipients, subject, body): return requests.post(url, headers=headers, json=data).text def send_email_smtp( - self, smtp_host, recipient, cc_emails ,subject, body, smtp_port, attachments="", username="", password="", ssl_verify="True", body_type="html", + self, smtp_host, recipient, subject, body, smtp_port, attachments="", username="", password="", ssl_verify="True", body_type="html", cc_emails="" ): if type(smtp_port) == str: try: @@ -113,7 +113,7 @@ def send_email_smtp( msg["To"] = recipient msg["Subject"] = subject - if cc_emails: + if cc_emails != None and len(cc_emails) > 0: msg["Cc"] = cc_emails msg.attach(MIMEText(body, body_type)) From a4c794b4d1bf301fb3b842fbaec984d184e7a4a0 Mon Sep 17 00:00:00 2001 From: BenM Date: Wed, 24 Apr 2024 13:28:18 +0100 Subject: [PATCH 12/25] Add new functions to microsoft-identity-and-access --- microsoft-identity-and-access/1.0.0/api.yaml | 106 +++++++++++++ .../1.0.0/src/app.py | 144 ++++++++++++++++++ 2 files changed, 250 insertions(+) diff --git a/microsoft-identity-and-access/1.0.0/api.yaml b/microsoft-identity-and-access/1.0.0/api.yaml index 9a3e015d..b891edc1 100644 --- a/microsoft-identity-and-access/1.0.0/api.yaml +++ b/microsoft-identity-and-access/1.0.0/api.yaml @@ -326,5 +326,111 @@ actions: required: true schema: type: string + - name: disable_user_account + description: Disable user account + parameters: + - name: user_email_or_id + description: User Email or Object ID + multiline: false + example: "Test.User@example.com" + required: true + schema: + type: string + - name: update_user_job_title + description: Updates user Job Title field + parameters: + - name: user_email_or_id + description: User Email or Object ID + multiline: false + example: "Test.user@example.com" + required: true + schema: + type: string + - name: user_job_title + description: Job Title to update for user + multiline: false + example: "DevOps Engineer" + required: true + schema: + type: string + - name: update_user_department + description: Updates user Department field + parameters: + - name: user_email_or_id + description: User Email or Object ID + multiline: false + example: "Test.user@example.com" + required: true + schema: + type: string + - name: user_department + description: Department to update for user + multiline: false + example: "Finance Department" + required: true + schema: + type: string + - name: update_user_employee_type + description: Updates user Employee Type field + parameters: + - name: user_email_or_id + description: User Email or Object ID + multiline: false + example: "Test.user@example.com" + required: true + schema: + type: string + - name: user_employee_type + description: Employee Type to update for user + multiline: false + example: "Contractor" + required: true + schema: + type: string + - name: update_user_leave_date + description: Updates user Leave Date field + parameters: + - name: user_email_or_id + description: User Email or Object ID + multiline: false + example: "Test.user@example.com" + required: true + schema: + type: string + - name: user_leave_date + description: User Leave Date + multiline: false + example: "2022-09-30T23:59:59Z" + required: true + schema: + type: string + - name: get_user_direct_groups + description: Retrieves Static Groups User is Member Of + parameters: + - name: user_email_or_id + description: User Email or Object ID + multiline: false + example: "Test.user@example.com" + required: true + schema: + type: string + - name: remove_user_from_group + description: Removes User from Specified Group + parameters: + - name: user_id + description: Object ID of User + multiline: false + example: eb6fa72b-f4f0-4ce0-94d2-dd16b4a22686 + required: true + schema: + type: string + - name: group_id + description: Object ID of Group + multiline: false + example: 2a712b67-91af-429f-9603-a5bfhgu7b151 + required: true + schema: + type: string + large_image: data:image/png;base64,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 diff --git a/microsoft-identity-and-access/1.0.0/src/app.py b/microsoft-identity-and-access/1.0.0/src/app.py index 07a791ad..19f7729f 100644 --- a/microsoft-identity-and-access/1.0.0/src/app.py +++ b/microsoft-identity-and-access/1.0.0/src/app.py @@ -498,5 +498,149 @@ def reset_user_password(self, tenant_id, client_id, client_secret, user_email_or return {"success": False, "reason": "Bad status code %d - expecting 200." % ret.status_code, "error_response":ret.text} + def disable_user_account(self, tenant_id, client_id, client_secret, user_email_or_id): + graph_url = "https://graph.microsoft.com" + session = self.authenticate(tenant_id, client_id, client_secret, graph_url) + + graph_url = f"https://graph.microsoft.com/beta/users/{user_email_or_id}" + + headers = { + "Content-type": "application/json" + } + request_body = { + "accountEnabled": "False" + } + + ret = session.patch(graph_url, json=request_body,headers=headers) + print(ret.status_code) + print(ret.text) + if ret.status_code < 300: + data = ret.json() + return data + + return {"success": False, "reason": "Bad status code %d - expecting 200." % ret.status_code, "error_response":ret.text} + + def update_user_job_title(self, tenant_id, client_id, client_secret, user_email_or_id, user_job_title): + graph_url = "https://graph.microsoft.com" + session = self.authenticate(tenant_id, client_id, client_secret, graph_url) + + graph_url = f"https://graph.microsoft.com/beta/users/{user_email_or_id}" + + headers = { + "Content-type": "application/json" + } + request_body = { + "jobTitle": user_job_title + } + + ret = session.patch(graph_url, json=request_body,headers=headers) + print(ret.status_code) + print(ret.text) + if ret.status_code < 300: + data = ret.json() + return data + + return {"success": False, "reason": "Bad status code %d - expecting 200." % ret.status_code, "error_response":ret.text} + + def update_user_department(self, tenant_id, client_id, client_secret, user_email_or_id, user_department): + graph_url = "https://graph.microsoft.com" + session = self.authenticate(tenant_id, client_id, client_secret, graph_url) + + graph_url = f"https://graph.microsoft.com/beta/users/{user_email_or_id}" + + headers = { + "Content-type": "application/json" + } + request_body = { + "department": user_department + } + + ret = session.patch(graph_url, json=request_body,headers=headers) + print(ret.status_code) + print(ret.text) + if ret.status_code < 300: + data = ret.json() + return data + + return {"success": False, "reason": "Bad status code %d - expecting 200." % ret.status_code, "error_response":ret.text} + + def update_user_employee_type(self, tenant_id, client_id, client_secret, user_email_or_id, user_employee_type): + graph_url = "https://graph.microsoft.com" + session = self.authenticate(tenant_id, client_id, client_secret, graph_url) + + graph_url = f"https://graph.microsoft.com/beta/users/{user_email_or_id}" + + headers = { + "Content-type": "application/json" + } + request_body = { + "employeeType": user_employee_type + } + + ret = session.patch(graph_url, json=request_body,headers=headers) + print(ret.status_code) + print(ret.text) + if ret.status_code < 300: + data = ret.json() + return data + + return {"success": False, "reason": "Bad status code %d - expecting 200." % ret.status_code, "error_response":ret.text} + + def update_user_leave_date(self, tenant_id, client_id, client_secret, user_email_or_id, user_leave_date): + graph_url = "https://graph.microsoft.com" + session = self.authenticate(tenant_id, client_id, client_secret, graph_url) + + graph_url = f"https://graph.microsoft.com/beta/users/{user_email_or_id}" + + headers = { + "Content-type": "application/json" + } + request_body = { + "employeeLeaveDateTime": user_leave_date + } + + ret = session.patch(graph_url, json=request_body,headers=headers) + print(ret.status_code) + print(ret.text) + if ret.status_code < 300: + data = ret.json() + return data + + return {"success": False, "reason": "Bad status code %d - expecting 200." % ret.status_code, "error_response":ret.text} + + def get_user_direct_groups(self, tenant_id, client_id, client_secret, user_email_or_id): + graph_url = "https://graph.microsoft.com" + session = self.authenticate(tenant_id, client_id, client_secret, graph_url) + + graph_url = f"https://graph.microsoft.com/beta/users/{user_email_or_id}/memberOf?$filter=NOT(groupTypes/any(c:c eq 'DynamicMembership'))&$count=true" + + headers = { + "ConsistencyType": "eventual" + } + + ret = session.get(graph_url,headers=headers) + print(ret.status_code) + print(ret.text) + if ret.status_code < 300: + data = ret.json() + return data + + return {"success": False, "reason": "Bad status code %d - expecting 200." % ret.status_code, "error_response":ret.text} + + def remove_user_from_group(self, tenant_id, client_id, client_secret, user_id, group_id): + graph_url = "https://graph.microsoft.com" + session = self.authenticate(tenant_id, client_id, client_secret, graph_url) + + graph_url = f"https://graph.microsoft.com/beta/groups/{group_id}/members/{user_id}/$ref" + + ret = session.delete(graph_url) + print(ret.status_code) + print(ret.text) + if ret.status_code < 300: + data = ret.json() + return data + + return {"success": False, "reason": "Bad status code %d - expecting 200." % ret.status_code, "error_response":ret.text} + if __name__ == "__main__": MsIdentityAccess.run() From 8112147ef9231a6ad47199af748db2b092e928f1 Mon Sep 17 00:00:00 2001 From: Frikky Date: Sun, 5 May 2024 18:42:40 +0200 Subject: [PATCH 13/25] Added dict merging to branches for shuffle tools --- shuffle-tools/1.2.0/api.yaml | 3 ++- shuffle-tools/1.2.0/src/app.py | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/shuffle-tools/1.2.0/api.yaml b/shuffle-tools/1.2.0/api.yaml index a31a363d..81e4a7e7 100644 --- a/shuffle-tools/1.2.0/api.yaml +++ b/shuffle-tools/1.2.0/api.yaml @@ -282,7 +282,7 @@ actions: required: false multiline: false example: "domains,urls,email_addresses,ipv4s,ipv4_cidrs,ipv6s,md5s,sha256s,sha1s,cves" - value: "domains,urls,ipv4s,md5s,sha1s" + value: "domains,urls,ipv4s,md5s,sha1s,email_addresses" multiselect: true schema: type: string @@ -1106,6 +1106,7 @@ actions: example: 'list' options: - list + - dict schema: type: string - name: run_ssh_command diff --git a/shuffle-tools/1.2.0/src/app.py b/shuffle-tools/1.2.0/src/app.py index 45160b86..04c854d3 100644 --- a/shuffle-tools/1.2.0/src/app.py +++ b/shuffle-tools/1.2.0/src/app.py @@ -2642,6 +2642,15 @@ def merge_incoming_branches(self, input_type="list"): #newlist.append(item) results = newlist + elif input_type == "dict": + new_dict = {} + for item in results: + if not isinstance(item, dict): + continue + + new_dict = self.merge_lists(new_dict, item) + + results = json.dumps(new_dict) else: return { "success": False, From 941d5c723dfba836544bc9ea40d955ad8280c863 Mon Sep 17 00:00:00 2001 From: Frikky Date: Wed, 8 May 2024 23:08:50 +0200 Subject: [PATCH 14/25] Rebuild with new SDK --- shuffle-tools/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/shuffle-tools/README.md b/shuffle-tools/README.md index 0a3518ac..febda00c 100644 --- a/shuffle-tools/README.md +++ b/shuffle-tools/README.md @@ -1,7 +1,6 @@ # Shuffle tools App documentation Shuffle tools is a utility app that simplifies your understanding of what happens in a node and also allows you to test on the fly. - ## Actions The Shuffle-tools app comes with a multitude of different actions, here we will check a few out and give a brief description. From 08133f32f8c62fee13091dc38c1de3d00e8ad06f Mon Sep 17 00:00:00 2001 From: Frikky Date: Thu, 9 May 2024 13:52:22 +0200 Subject: [PATCH 15/25] Fixed routes to us /api/v1/ for mail/sms --- email/1.3.0/api.yaml | 27 +++++++++++++++++++++++++++ email/1.3.0/src/app.py | 16 ++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/email/1.3.0/api.yaml b/email/1.3.0/api.yaml index 12679e9d..23fdff8f 100644 --- a/email/1.3.0/api.yaml +++ b/email/1.3.0/api.yaml @@ -303,4 +303,31 @@ actions: returns: schema: type: string + - name: send_sms_shuffle + description: Send an SMS from Shuffle + parameters: + - name: apikey + description: Your https://shuffler.io organization apikey + multiline: false + example: "https://shuffler.io apikey" + required: true + schema: + type: string + - name: phone_numbers + description: The receivers of the SMS + multiline: false + example: "+4741323535,+8151023022" + required: true + schema: + type: string + - name: body + description: The SMS to add to the numbers + multiline: true + example: "This is an alert from Shuffle :)" + required: true + schema: + type: string + returns: + schema: + type: string large_image: data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/hAytodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Nzg4QTJBMjVEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Nzg4QTJBMjZEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3ODhBMkEyM0QwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3ODhBMkEyNEQwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoMDwwKCw4LCQkNEQ0ODxAQERAKDBITEhATDxAQEP/AAAsIAGQAZAEBEQD/xAAeAAABAwUBAQAAAAAAAAAAAAAAAQgJAgQFBwoGA//EAEoQAAECBAMEBwMDEQgDAAAAAAECAwAEBREGBxIIITFRCRMiMkFSYRRicSNCQxUWGBk2U1dYc3WBlJWzwdLTJDNjZXKDkeEmgqH/2gAIAQEAAD8Ak8JKipSlBwuDSpSeDw8qeRguQQrUAQNAV4JH3s+sA7OnT8n1fcv9Bfzc7wWAAToIAOsJ8Uq++H0gI1XSUlYWdSkji6fMnkBAdS9SidesWUpI3OjyjkYXtXCgbEDSFW3JT5D6+sIOzp0/J9X3NX0F/NzvBYABOggA6wnxSr74fSAjVdJSVhZ1KSOLp8yeQEBJUSVKCysaVKHB0eVPIwXIIVqAIGgK8Ej72fWFS640kNtzjcukcGli6k/GENwVagAQO0EcEjmj1g33AATe1wD3SnmffgG/Tp337mv535T+EaB2k9tzInZilVyuMq+up4iUjWxh+mFLs8s23dbv0stnmsgnwBiNPOHpddozHExMSmWsrSsA0tZIaMs0JudCfV50FKf/AFQPjDYsT7S+0LjJ8zGJc68aTqiSrSqtPoQD6ISoJH6BGFkM5c3qW8Jim5qYvlXArUFNVuZSb89y43Tlv0ju15ltMtKZzUmsRSbZGuSxC0mebcHIrV8r/wALEP02c+l2ywzAmZXDGeND+saqvEIRVWXFP0x1Z3AOE9thPx1JHioQ/un1Kn1eQZqtLn2ZuSmkJdamZVwOIWlQuktKTuUg8xFybgnUACB2gjgkc0e9BvuAAm9rgHulPM+/CpDik3bal1p8FPd8/GEtp7Ojq+r36OPUe96wWv2dF79vR5/8T/qI/ukM6RMZMGcyWyUqDMzjZ5vRWKwmy26UlQ3IQOBmLHx3IFibncIeqvWKtiCqTVbrtSmahUJ11T0zNTLqnHXnFG5UpSiSSeZi0ggggh2GxRt8Y92X69K4cr83N1zLuadCZumLXrcp4Ue0/KXPZPiW+6r0O+JxsF4zwvmFhSl42wXV5eo0Sqy6ZySmWFakJbUO/wDHiCk7wQQd4jN2v2dF79vR5v8AE/6g6rrflPYfar/S69Or9EIAAE6QoAHshfFJ5r9Ibpt3bTrOzBkbPYhpb7f11V5aqZh9le8iZUm65i3i20ntcirQPGIA6nU6jWqlNVirzr05PTzy5iZmHllTjrqyVKWoneSSSSYtoIIIIIIkI6J/axmsA4+Rs84yqZ+tvFb5VQ1vL7EjVCNyN/Bt4C1vOEn5xiYndYghVr3IHeKuY9yKVJbJu63MrV4qY7h+EVA6rEKKwvclSuLp8quQiDnpWM5ZnMrafn8HS04pykZfy6KMw2D2BNEByZUPXWQj/aEM0h3uwvk5PYmoeLM4HcnqHmth/CU1KytdwrNy5M+uUdQtZmZBYIu83oN2z3wbcbWk7ym2Zej6zuwXJ49y5yXwbUqXNgpUPZlpelnh32XmyrU24k7ik7/0WMey+wI2OPxesJfqyv5oPsCNjj8XrCX6sr+aD7AjY4/F6wl+rq/mhs2b2SGzXjPGc3s9bKuzngiq4zZGjEWJ3pRTlKwkyrcVOKCrOzVr6GRex73AiIe65TvqRWqhSet632Kadl9enTq0LKb28L24R86ZUp6jVKUrFMmVy85IvtzMu8g2U24hQUlQPMEAx0h7O+abGdWR+DM0mnAF16lMuzRTxamgNDzQHIOJWI2IpxDZ0Lm3ZdQ4tti6U/CEdeDaHJhxYWAklahwdAF9KeRjmXzRxJMYxzLxXiyacUt2sVqdnlFRuflHlq/jHmIlv6D37is1T/mlM/cvQ5zNnZ7xtl/jWc2htlFUtIYrfs7iXCLy+rpeLGk7zcDczN2vpdFrnvcSTsrIPaHwNtBYcmKlhsv02t0h4ydfw7UE9XUKPOJJC2Xmzv3KBAWNyrbvEDaDjjbTa3XVpQhAKlKUbBIHEk+ENLxdnDj/AGr8T1LJ7Zgra6NgymPmSxhmU0LhB+kkaV4OPkGynu6gG4PAlwGUuT2AMjsDy2BMuqGin06XBcdWTrfm3j3333D2nHFHeVH/AOCwjmnxr92Ve/Oc1+9VGGibDogMVP1vZWmKI+4b4dxJOybS1G4S06ht7QPipxf/ADD4kurbGhE21LpHBtwXUn4xbVNpb9OnGAAFrl3EkI4JukgFHrHMFWpdyUrE/KvAhxmZdbUDxBCyDFnEuHQej/wjNU/5rTP3L0PQ2hs1cR4f+pOUWU/VTGZeOtbFK1jW3SZNO6YqkwPBtlJ7IPfcKUi++NdYh2H5LB1CoWLtnXFD2Fs1sLS6rV+ZUXG8SqWouPtVVP0yXnCo6+8gqFtwAGAbkdpzbFcTgXNLB1Qyay9pBEri1iXm9VQxPNo/vJeVdT/dyJ3XcG9YNgTvt6TFODKZsY4nlc2MsaEmRypn25em45oMi2eqpiUANsVllA8gsiYtvUiyzcpJhz8rOylSkGqjITTUzKzTKXmHmlhSHG1C6VJI3EEEEGOXnGv3ZV785zX71UYaJjehfk3mdn/GM4oHRM4sWEBfcsiUZ1Eeu+JBUhxSbttS60+Cnu+fjCAaDbR1fV9rRx6n3vW8c5+2Bl1MZV7TGYmDXmlIaZrkxNyhIsFy0wrr2lD00OJjT0SfdE5mphvJnIrOXHmJutdalatSmZSSlxqmKhNuNOpYlWU8VOOLISAOdzuBiQLZ5yrxJQTVs382Q0/mVjrQ9VAk6m6RJp3y9Llz4NtA9ojvuFSjfdG54N8fGekZOpyUxTajKtTMrNNLYfYdSFIdbULKSpJ3EEEgiG4ZXz07s0Zis7OuJpp1zAmJFvP5cVSYWSJVYut2hurPzkC62Ce83dHFFogGxr92Ve/Oc1+9VGGiezo0MupnLzY/wezPy5bm8RqmMROsqFiUvr+SWf8AaQ2besOl6rrflPYfar/S69Or9EIAAE6QoAHshfFJ5r9Ii76Y7Z4mJgUHaSw7IqWhlCKHiLQN6RcmWmP9Nypsn8mIizj3GWGdOY2T9Xka1gSuJlH6bO/VKWbflm5hlubDam0v9U4lSC4lClBKiLp1G1iY3v8AbSdtr8LTP7Ekf6UL9tJ22vwss/sOR/pQfbSdtr8LLP7Dkf6UH20nba/C0z+w5H+lHmMxOkB2qc1MOKwrjjMNmfkPaGZxrTSZRl1iYZWFtPNOobC21pULhSSDx8DDe5uamJ6aenZt1Tr8w4p11auKlqNyT8SY2ZszZH1raIzqw1ldSG1hqozSXKlMAHTKyLZCn3VHwsi4HvKSPGOjSj0im0CjyVBpMqJen06XalZZhG7Q22kJQE+4AAIulJbJu63MrV4qY7h+EVA6rEKKwvclSuLp8quQjB45wVhrMbB9YwNjGnIn6JW5VyQnWVjihYtoTysbEKHAgGOf7a72U8abKeZszhStMOzVAnVreoNXCfk5uXvuSojcHUAgLTz3jcRGi4IIIIIuqVSqnXKnK0ajSD89PzzyJeWlpdsrcecUbJQlI3kkkAAROd0eGxqjZiy7XiLF8sy5j/FjaFVEiyhJMDeiSB9D2lkbiqw4JEO58CrUQAdJV4pPkHu+sIpxDZ0Lm3ZdQ4tti6U/CFJKiVKUFle5Sk8HR5U8jBcghWoAgaArwSPIfe9Y8PnJkvl1nzgScy7zMoDVQpMyLtlXZekHfmvNucULHgR8DcEiIZtqro1s58gZucxFg6QmsbYJQVOonpFkqnJNrw9pYTcgAfSJuk8Tp4Qz9SSklKgQQbEHwgggjYeTOz9m7n/iFGHMq8Fz1YdCgJiZSjRKSiT8955XYQB6m58AYmN2LejtwJsyoYxri52XxVmC43unA3/ZpAEb0ygVvv4F09ojgEgm7wSSq5Kgsr3KUODo8qeRguQQrUAQNAV4JHkPvesKl1bY0Im2pdI4NuC6k/GENwVagAQO0EcEjmj1g33AATe1wD3SnmffgG/Tp337mv535T+EG4i4KiCbAnvE8j7kaHzf2Hdl/O1+YqONcraexU3jd6p0i8jNFfPU1ZLnxWlUNjxL0LOTs6+tzC+bWLKSkHUWpmXl5xKR4BJAbJjD0/oTcEoeH1Uz5rbzfe0sUZlolH+pTigFelo3Nlr0UuyZgSYZn6vQ6zjKaQQpr6uz3yBI462WQhNvRVxDscMYVwvgujMYfwfh+n0Wly/ZZlJCVQw2k8tCABp9YypsL6iQAe0U8Unkj3YDcE6gAQO0EcEjmj3oN9wAE3tcA90p5n34VIcUm7bUutPgp7vn4wOpS05MNtiyZdAW0PKo+MASkuIbIulbPXKHNfOEa+V9m6zf7Vq633rcIpSoqbQ6T2lvdQo80coVxRbRMLRuVLuBts+VJ4iKnEhtb6ECwl0BxseVR8YAlJcQ2RdK2euUOa+cI18r7N1m/wBq1db71uEUpUVNodJ7S3uoUeaOUK4otomFo3Kl3A22fKk8RFTiQ2t9CBYS6A42PKo+MASkuIbIulbPXKHNfOPtKSkvNy6JiYaC3F71KJO+P//Z diff --git a/email/1.3.0/src/app.py b/email/1.3.0/src/app.py index 0b7feb45..ed3992cd 100644 --- a/email/1.3.0/src/app.py +++ b/email/1.3.0/src/app.py @@ -633,6 +633,22 @@ def analyze_headers(self, headers): # Should be a dictionary return analyzed_headers + # This is an SMS function of Shuffle + def send_sms_shuffle(self, apikey, phone_numbers, body): + phone_numbers = self.parse_list_internal(phone_numbers) + + targets = [phone_numbers] + if ", " in phone_numbers: + targets = phone_numbers.split(", ") + elif "," in phone_numbers: + targets = phone_numbers.split(",") + + data = {"numbers": targets, "body": body} + + url = "https://shuffler.io/api/v1/functions/sendsms" + headers = {"Authorization": "Bearer %s" % apikey} + return requests.post(url, headers=headers, json=data, verify=False).text + # Run the actual thing after we've checked params def run(request): From 36333b01ef79a03dce2d5a0482e8d436aa4823b5 Mon Sep 17 00:00:00 2001 From: Frikky Date: Thu, 9 May 2024 20:29:56 +0200 Subject: [PATCH 16/25] Fixed base64 decoding to have more failure handlers --- shuffle-tools/1.2.0/src/app.py | 68 +++++++++++++++++----------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/shuffle-tools/1.2.0/src/app.py b/shuffle-tools/1.2.0/src/app.py index 04c854d3..a312551d 100644 --- a/shuffle-tools/1.2.0/src/app.py +++ b/shuffle-tools/1.2.0/src/app.py @@ -94,48 +94,46 @@ def base64_conversion(self, string, operation): return value elif operation == "decode": + decoded_bytes = "" + + # For loop this. It's stupid. try: decoded_bytes = base64.b64decode(string) - try: - decoded_bytes = str(decoded_bytes, "utf-8") - except: - pass + except Exception as e: + if "incorrect padding" in str(e).lower(): + try: + decoded_bytes = base64.b64decode(string + "=") + except Exception as e: + if "incorrect padding" in str(e).lower(): + try: + decoded_bytes = base64.b64decode(string + "==") + except Exception as e: + if "incorrect padding" in str(e).lower(): + try: + decoded_bytes = base64.b64decode(string + "===") + except Exception as e: + if "incorrect padding" in str(e).lower(): + return "Invalid Base64" - # Check if json - try: - decoded_bytes = json.loads(decoded_bytes) - except: - pass - return decoded_bytes - except Exception as e: - #return string.decode("utf-16") + decoded_bytes = base64.b64decode(string) + try: + decoded_bytes = str(decoded_bytes, "utf-8") + except: + pass - return { - "success": False, - "reason": f"Error decoding the base64: {e}", - } - #newvar = binascii.a2b_base64(string) - #try: - # if str(newvar).startswith("b'") and str(newvar).endswith("'"): - # newvar = newvar[2:-1] - #except Exception as e: - #return newvar - - #try: - # return newvar - #except: - # pass + # Check if json + try: + decoded_bytes = json.loads(decoded_bytes) + except: + pass - return { - "success": False, - "reason": "Error decoding the base64", - } + return decoded_bytes - return json.dumps({ + return { "success": False, - "reason": "No base64 to be converted", - }) + "reason": "Invalid operation", + } def parse_list_internal(self, input_list): if isinstance(input_list, list): @@ -565,7 +563,7 @@ def check_wildcard(self, wildcardstring, matching_string): if wildcardstring in str(matching_string).lower(): return True else: - wildcardstring = wildcardstring.replace(".", "\.") + wildcardstring = wildcardstring.replace(".", "\\.") wildcardstring = wildcardstring.replace("*", ".*") if re.match(wildcardstring, str(matching_string).lower()): From 284ce193f59ea2f167c27eb1127bccf3d984f4a7 Mon Sep 17 00:00:00 2001 From: Frikky Date: Mon, 13 May 2024 04:17:55 +0200 Subject: [PATCH 17/25] Bumped shuffle AI app --- email/1.3.0/src/app.py | 21 ++++++++++++--- shuffle-ai/1.0.0/upload.sh | 4 +-- shuffle-tools/1.2.0/src/app.py | 47 ++++++++++++++++++++++------------ 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/email/1.3.0/src/app.py b/email/1.3.0/src/app.py index ed3992cd..51406b65 100644 --- a/email/1.3.0/src/app.py +++ b/email/1.3.0/src/app.py @@ -395,6 +395,10 @@ def parse_eml(self, filedata, extract_attachments=False): "data": filedata, } + # Encode the data as utf-8 if it's not base64 + if not str(parsedfile["data"]).endswith("="): + parsedfile["data"] = parsedfile["data"].encode("utf-8") + return self.parse_email_file(parsedfile, extract_attachments) def parse_email_file(self, file_id, extract_attachments=False): @@ -413,8 +417,6 @@ def parse_email_file(self, file_id, extract_attachments=False): "reason": "Couldn't get file with ID %s" % file_id } - #print("PRE: ", file_path) - # Check if data is in base64 and decode it # If it ends with = then it may be bas64 @@ -434,6 +436,16 @@ def parse_email_file(self, file_id, extract_attachments=False): else: extract_attachments = False + # Replace raw newlines \\r\\n with actual newlines + # The data is a byte string, so we need to decode it to utf-8 + try: + print("Pre size: %d" % len(file_path["data"])) + file_path["data"] = file_path["data"].decode("utf-8").replace("\\r\\n", "\n").encode("utf-8") + print("Post size: %d" % len(file_path["data"])) + except Exception as e: + print(f"Failed to decode file: {e}") + pass + # Makes msg into eml if ".msg" in file_path["filename"] or "." not in file_path["filename"]: print(f"[DEBUG] Working with .msg file {file_path['filename']}. Filesize: {len(file_path['data'])}") @@ -448,6 +460,7 @@ def parse_email_file(self, file_id, extract_attachments=False): if ".msg" in file_path["filename"]: return {"success":False, "reason":f"Exception occured during msg parsing: {e}"} + ep = eml_parser.EmlParser( include_attachment_data=True, include_raw_body=True @@ -456,8 +469,8 @@ def parse_email_file(self, file_id, extract_attachments=False): try: print("Pre email") parsed_eml = ep.decode_email_bytes(file_path['data']) - if str(parsed_eml["header"]["date"]) == "1970-01-01 00:00:00+00:00" and len(parsed_eml["header"]["subject"]) == 0: - return {"success":False,"reason":"Not a valid EML/MSG file, or the file have a timestamp or subject defined (required).", "date": str(parsed_eml["header"]["date"]), "subject": str(parsed_eml["header"]["subject"])} + #if str(parsed_eml["header"]["date"]) == "1970-01-01 00:00:00+00:00" and len(parsed_eml["header"]["subject"]) == 0: + # return {"success":False,"reason":"Not a valid EML/MSG file, or the file have a timestamp or subject defined (required).", "date": str(parsed_eml["header"]["date"]), "subject": str(parsed_eml["header"]["subject"])} # Put attachments in the shuffle file system print("Pre attachment") diff --git a/shuffle-ai/1.0.0/upload.sh b/shuffle-ai/1.0.0/upload.sh index 33f84bac..6dbdff4d 100755 --- a/shuffle-ai/1.0.0/upload.sh +++ b/shuffle-ai/1.0.0/upload.sh @@ -1,6 +1,6 @@ gcloud run deploy shuffle-ai-1-0-0 \ --region=europe-west2 \ - --max-instances=3 \ + --max-instances=5 \ --set-env-vars=SHUFFLE_APP_EXPOSED_PORT=8080,SHUFFLE_SWARM_CONFIG=run,SHUFFLE_LOGS_DISABLED=true --source=./ \ - --timeout=1800s + --timeout=300s diff --git a/shuffle-tools/1.2.0/src/app.py b/shuffle-tools/1.2.0/src/app.py index a312551d..c0c8d6b5 100644 --- a/shuffle-tools/1.2.0/src/app.py +++ b/shuffle-tools/1.2.0/src/app.py @@ -94,29 +94,44 @@ def base64_conversion(self, string, operation): return value elif operation == "decode": - decoded_bytes = "" + + if "-" in string: + string = string.replace("-", "+", -1) + + if "_" in string: + string = string.replace("_", "/", -1) + + # Fix padding + if len(string) % 4 != 0: + string += "=" * (4 - len(string) % 4) + # For loop this. It's stupid. + decoded_bytes = "" try: decoded_bytes = base64.b64decode(string) except Exception as e: - if "incorrect padding" in str(e).lower(): - try: - decoded_bytes = base64.b64decode(string + "=") - except Exception as e: - if "incorrect padding" in str(e).lower(): - try: - decoded_bytes = base64.b64decode(string + "==") - except Exception as e: - if "incorrect padding" in str(e).lower(): - try: - decoded_bytes = base64.b64decode(string + "===") - except Exception as e: - if "incorrect padding" in str(e).lower(): - return "Invalid Base64" + return json.dumps({ + "success": False, + "reason": "Invalid Base64 - %s" % e, + }) + + #if "incorrect padding" in str(e).lower(): + # try: + # decoded_bytes = base64.b64decode(string + "=") + # except Exception as e: + # if "incorrect padding" in str(e).lower(): + # try: + # decoded_bytes = base64.b64decode(string + "==") + # except Exception as e: + # if "incorrect padding" in str(e).lower(): + # try: + # decoded_bytes = base64.b64decode(string + "===") + # except Exception as e: + # if "incorrect padding" in str(e).lower(): + # return "Invalid Base64" - decoded_bytes = base64.b64decode(string) try: decoded_bytes = str(decoded_bytes, "utf-8") except: From acc90cd1fb4d233ef02445b54356973ee3657c9f Mon Sep 17 00:00:00 2001 From: Frikky Date: Sat, 18 May 2024 12:57:06 +0200 Subject: [PATCH 18/25] Updated readme to rebuild all --- shuffle-tools/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/shuffle-tools/README.md b/shuffle-tools/README.md index febda00c..0a3518ac 100644 --- a/shuffle-tools/README.md +++ b/shuffle-tools/README.md @@ -1,6 +1,7 @@ # Shuffle tools App documentation Shuffle tools is a utility app that simplifies your understanding of what happens in a node and also allows you to test on the fly. + ## Actions The Shuffle-tools app comes with a multitude of different actions, here we will check a few out and give a brief description. From 5f0d61463f6432d5b004070b6966863a6760f4a3 Mon Sep 17 00:00:00 2001 From: Aditya <60684641+0x0elliot@users.noreply.github.com> Date: Wed, 22 May 2024 01:43:54 +0530 Subject: [PATCH 19/25] feat[auth-overrides]: added in the support --- shuffle-subflow/1.1.0/src/app.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/shuffle-subflow/1.1.0/src/app.py b/shuffle-subflow/1.1.0/src/app.py index 5c4b4d8a..3ce1692e 100644 --- a/shuffle-subflow/1.1.0/src/app.py +++ b/shuffle-subflow/1.1.0/src/app.py @@ -26,7 +26,7 @@ def run_userinput(self, user_apikey, sms="", email="", subflow="", information=" headers = { "Authorization": "Bearer %s" % user_apikey, - "User-Agent": "Shuffle Userinput 1.1.0" + "User-Agent": "Shuffle Userinput 1.1.0", } result = { @@ -143,7 +143,7 @@ def run_userinput(self, user_apikey, sms="", email="", subflow="", information=" return json.dumps(result) - def run_subflow(self, user_apikey, workflow, argument, source_workflow="", source_execution="", source_node="", source_auth="", startnode="", backend_url="", check_result=""): + def run_subflow(self, user_apikey, workflow, argument, source_workflow="", source_execution="", source_node="", source_auth="", startnode="", backend_url="", check_result="", auth_override=""): #print("STARTNODE: %s" % startnode) url = "%s/api/v1/workflows/%s/execute" % (self.url, workflow) if len(self.base_url) > 0: @@ -190,6 +190,10 @@ def run_subflow(self, user_apikey, workflow, argument, source_workflow="", sourc "User-Agent": "Shuffle Subflow 1.1.0" } + if len(auth_override) > 0: + print("Overriding auth with: %s" % auth_override) + headers["appauth"] = auth_override + if len(str(argument)) == 0: ret = requests.post(url, headers=headers, params=params, verify=False, proxies=self.proxy_config) else: From ed793211dd6edabd54c227df52a0b2878f8cee3e Mon Sep 17 00:00:00 2001 From: Aditya <60684641+0x0elliot@users.noreply.github.com> Date: Wed, 22 May 2024 01:45:43 +0530 Subject: [PATCH 20/25] feat[auth-overrides]: added in the support --- shuffle-subflow/1.0.0/src/app.py | 5 ++++- shuffle-subflow/1.1.0/src/app.py | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/shuffle-subflow/1.0.0/src/app.py b/shuffle-subflow/1.0.0/src/app.py index 82253179..c6f25c41 100644 --- a/shuffle-subflow/1.0.0/src/app.py +++ b/shuffle-subflow/1.0.0/src/app.py @@ -140,7 +140,7 @@ def run_userinput(self, user_apikey, sms="", email="", subflow="", information=" return json.dumps(result) - def run_subflow(self, user_apikey, workflow, argument, source_workflow="", source_execution="", source_node="", source_auth="", startnode="", backend_url=""): + def run_subflow(self, user_apikey, workflow, argument, source_workflow="", source_execution="", source_node="", source_auth="", startnode="", backend_url="", auth_override=""): #print("STARTNODE: %s" % startnode) url = "%s/api/v1/workflows/%s/execute" % (self.url, workflow) if len(self.base_url) > 0: @@ -187,6 +187,9 @@ def run_subflow(self, user_apikey, workflow, argument, source_workflow="", sourc "User-Agent": "Shuffle Subflow 1.0.0" } + if len(auth_override) > 0: + headers["appauth"] = auth_override + if len(str(argument)) == 0: ret = requests.post(url, headers=headers, params=params) else: diff --git a/shuffle-subflow/1.1.0/src/app.py b/shuffle-subflow/1.1.0/src/app.py index 3ce1692e..4eead3fb 100644 --- a/shuffle-subflow/1.1.0/src/app.py +++ b/shuffle-subflow/1.1.0/src/app.py @@ -191,7 +191,6 @@ def run_subflow(self, user_apikey, workflow, argument, source_workflow="", sourc } if len(auth_override) > 0: - print("Overriding auth with: %s" % auth_override) headers["appauth"] = auth_override if len(str(argument)) == 0: From acb32e447266d6fe6d3ee7e847aa7d33146db9ef Mon Sep 17 00:00:00 2001 From: Frikky Date: Sat, 25 May 2024 21:10:46 +0200 Subject: [PATCH 21/25] Added more headers to be analyzed without lists --- email/1.3.0/src/app.py | 19 ++++++++++++++++++- shuffle-ai/1.0.0/src/app.py | 15 +++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/email/1.3.0/src/app.py b/email/1.3.0/src/app.py index 51406b65..4af23947 100644 --- a/email/1.3.0/src/app.py +++ b/email/1.3.0/src/app.py @@ -556,12 +556,16 @@ def analyze_headers(self, headers): analyzed_headers = { "success": True, + "sender": "", + "receiver": "", + "subject": "", + "date": "", "details": { "spf": "", "dkim": "", "dmarc": "", "spoofed": "", - } + }, } for item in headers: @@ -569,6 +573,19 @@ def analyze_headers(self, headers): item["key"] = item["name"] item["key"] = item["key"].lower() + + # Handle sender/receiver + if item["key"] == "from" or item["key"] == "sender" or item["key"] == "delivered-to": + analyzed_headers["sender"] = item["value"] + + if item["key"] == "to" or item["key"] == "receiver" or item["key"] == "delivered-to": + analyzed_headers["receiver"] = item["value"] + + if item["key"] == "subject" or item["key"] == "title": + analyzed_headers["subject"] = item["value"] + + if item["key"] == "date": + analyzed_headers["date"] = item["value"] if "spf" in item["key"]: analyzed_headers["details"]["spf"] = spf diff --git a/shuffle-ai/1.0.0/src/app.py b/shuffle-ai/1.0.0/src/app.py index 42e6e09c..9bcff4a2 100644 --- a/shuffle-ai/1.0.0/src/app.py +++ b/shuffle-ai/1.0.0/src/app.py @@ -215,6 +215,8 @@ def gpt(self, input_text): } def run_schemaless(self, category, action, app_name="", fields=""): + self.logger.info("[DEBUG] Running schemaless action with category '%s' and action label '%s'" % (category, action)) + """ action := shuffle.CategoryAction{ Label: step.Name, @@ -253,6 +255,12 @@ def run_schemaless(self, category, action, app_name="", fields=""): }) else: + fields = str(fields).strip() + if not fields.startswith("{") and not fields.startswith("["): + fields = json.dumps({ + "data": fields, + }) + try: loadedfields = json.loads(fields) for key, value in loadedfields.items(): @@ -262,11 +270,10 @@ def run_schemaless(self, category, action, app_name="", fields=""): }) except Exception as e: - print("[ERROR] Failed to load fields as JSON: %s" % e) + self.logger.info("[ERROR] Failed to load fields as JSON: %s" % e) return json.dumps({ "success": False, - "reason": "Ensure Fields are valid JSON", - "type": type(fields), + "reason": "Ensure 'Fields' are valid JSON", "details": "%s" % e, }) @@ -274,7 +281,7 @@ def run_schemaless(self, category, action, app_name="", fields=""): baseurl = "%s/api/v1/apps/categories/run" % self.base_url baseurl += "?execution_id=%s&authorization=%s" % (self.current_execution_id, self.authorization) - print("[DEBUG] Running schemaless action with URL '%s', category %s and action label %s" % (baseurl, category, action)) + self.logger.info("[DEBUG] Running schemaless action with URL '%s', category %s and action label %s" % (baseurl, category, action)) headers = {} request = requests.post( From 6190f892ba29e82807cb158de2b2ce82c37c8caf Mon Sep 17 00:00:00 2001 From: Aditya <60684641+0x0elliot@users.noreply.github.com> Date: Mon, 27 May 2024 00:54:59 +0530 Subject: [PATCH 22/25] fix[debugging-email-app]: trying to make some adjustments to make email app work --- email/1.1.0/src/app.py | 2 +- email/1.2.0/src/app.py | 2 +- email/1.3.0/src/app.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/email/1.1.0/src/app.py b/email/1.1.0/src/app.py index 8b2868c8..6dbac7db 100644 --- a/email/1.1.0/src/app.py +++ b/email/1.1.0/src/app.py @@ -64,7 +64,7 @@ def send_email_shuffle(self, apikey, recipients, subject, body): elif "," in recipients: targets = recipients.split(",") - data = {"targets": targets, "body": body, "subject": subject, "type": "alert"} + data = {"targets": targets, "body": body, "subject": subject, "type": "alert", "email_app": True} url = "https://shuffler.io/functions/sendmail" headers = {"Authorization": "Bearer %s" % apikey} diff --git a/email/1.2.0/src/app.py b/email/1.2.0/src/app.py index 23993bf8..700c9890 100644 --- a/email/1.2.0/src/app.py +++ b/email/1.2.0/src/app.py @@ -67,7 +67,7 @@ def send_email_shuffle(self, apikey, recipients, subject, body): elif "," in recipients: targets = recipients.split(",") - data = {"targets": targets, "body": body, "subject": subject, "type": "alert"} + data = {"targets": targets, "body": body, "subject": subject, "type": "alert", "email_app": True} url = "https://shuffler.io/functions/sendmail" headers = {"Authorization": "Bearer %s" % apikey} diff --git a/email/1.3.0/src/app.py b/email/1.3.0/src/app.py index 51406b65..dbdc4d75 100644 --- a/email/1.3.0/src/app.py +++ b/email/1.3.0/src/app.py @@ -68,7 +68,7 @@ def send_email_shuffle(self, apikey, recipients, subject, body): elif "," in recipients: targets = recipients.split(",") - data = {"targets": targets, "body": body, "subject": subject, "type": "alert"} + data = {"targets": targets, "body": body, "subject": subject, "type": "alert", "email_app": True} url = "https://shuffler.io/functions/sendmail" headers = {"Authorization": "Bearer %s" % apikey} From 49ac10ef72305e2beb56281929c892307beef318 Mon Sep 17 00:00:00 2001 From: Frikky Date: Sun, 26 May 2024 22:13:38 +0200 Subject: [PATCH 23/25] Added analysis fixes to email app --- email/1.3.0/src/app.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/email/1.3.0/src/app.py b/email/1.3.0/src/app.py index 4af23947..ba5d67bc 100644 --- a/email/1.3.0/src/app.py +++ b/email/1.3.0/src/app.py @@ -518,6 +518,8 @@ def parse_email_headers(self, email_headers): # Basic function to check headers in an email # Can be dumped in in pretty much any format def analyze_headers(self, headers): + self.logger.info("Input headers: %s" % headers) + # Raw if isinstance(headers, str): headers = self.parse_email_headers(headers) @@ -531,6 +533,11 @@ def analyze_headers(self, headers): headers = headers["header"] if "header" in headers: headers = headers["header"] + + if "headers" in headers: + headers = headers["headers"] + if "headers" in headers: + headers = headers["headers"] if not isinstance(headers, list): newheaders = [] @@ -548,6 +555,7 @@ def analyze_headers(self, headers): headers = newheaders + #self.logger.info("Parsed headers: %s" % headers) spf = False dkim = False From 9495c5ca91eb142c3eeb268ea1cf6e1ba313e9c3 Mon Sep 17 00:00:00 2001 From: Aditya <60684641+0x0elliot@users.noreply.github.com> Date: Tue, 28 May 2024 23:06:07 +0530 Subject: [PATCH 24/25] fix[email-app-sms]: SMS bug fixing --- email/1.3.0/src/app.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/email/1.3.0/src/app.py b/email/1.3.0/src/app.py index 5e7dc58a..f3b414f6 100644 --- a/email/1.3.0/src/app.py +++ b/email/1.3.0/src/app.py @@ -665,13 +665,8 @@ def analyze_headers(self, headers): # This is an SMS function of Shuffle def send_sms_shuffle(self, apikey, phone_numbers, body): - phone_numbers = self.parse_list_internal(phone_numbers) - - targets = [phone_numbers] - if ", " in phone_numbers: - targets = phone_numbers.split(", ") - elif "," in phone_numbers: - targets = phone_numbers.split(",") + phone_numbers = phone_numbers.replace(" ", "") + targets = phone_numbers.split(",") data = {"numbers": targets, "body": body} From 0ddf2faff515a502ba37dd27b9d5d1ecaf74dc73 Mon Sep 17 00:00:00 2001 From: Frikky Date: Thu, 6 Jun 2024 19:21:52 +0200 Subject: [PATCH 25/25] Fixed url vs urls in parse ioc --- shuffle-tools/1.2.0/src/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shuffle-tools/1.2.0/src/app.py b/shuffle-tools/1.2.0/src/app.py index c0c8d6b5..821d3830 100644 --- a/shuffle-tools/1.2.0/src/app.py +++ b/shuffle-tools/1.2.0/src/app.py @@ -2405,11 +2405,13 @@ def parse_ioc(self, input_string, input_type="all"): input_type = "all" else: input_type = input_type.split(",") - for item in input_type: + for i in range(len(input_type)): item = item.strip() if not item.endswith("s"): item = "%ss" % item + input_type[i] = item + ioc_types = input_type iocs = find_iocs(str(input_string), included_ioc_types=ioc_types)