diff --git a/Packs/Campaign/TestPlaybooks/Detect_and_Manage_Phishing_Campaigns_-_Test.yml b/Packs/Campaign/TestPlaybooks/Detect_and_Manage_Phishing_Campaigns_-_Test.yml index 22bd8011d0b0..6fed03ce96cd 100644 --- a/Packs/Campaign/TestPlaybooks/Detect_and_Manage_Phishing_Campaigns_-_Test.yml +++ b/Packs/Campaign/TestPlaybooks/Detect_and_Manage_Phishing_Campaigns_-_Test.yml @@ -5,7 +5,7 @@ description: |- Tests the scenario where similar phishing incidents should create a phishing campaign incident. It does that by creating fake phishing incidents which should run the Phishing - Generic v3 playbook, and in the process run the Detect & Manage Phishing Campaigns playbook. - The campaign incident should have 9 incidents when all incidents have finished running. + The campaign incident should have 9 incidents when all incidents have finished running. starttaskid: "0" tasks: "0": diff --git a/Packs/rasterize/Integrations/rasterize/rasterize.py b/Packs/rasterize/Integrations/rasterize/rasterize.py index f9058605deab..1a12deb60f1e 100644 --- a/Packs/rasterize/Integrations/rasterize/rasterize.py +++ b/Packs/rasterize/Integrations/rasterize/rasterize.py @@ -12,6 +12,7 @@ import time import traceback import websocket +import json from concurrent.futures import ThreadPoolExecutor from enum import Enum from threading import Event @@ -20,6 +21,7 @@ from pdf2image import convert_from_path from PyPDF2 import PdfReader + # region constants and configurations pypdf_logger = logging.getLogger("PyPDF2") @@ -60,6 +62,12 @@ DEFAULT_RETRY_WAIT_IN_SECONDS = 2 PAGES_LIMITATION = 20 +# chrome instance data keys +INSTANCE_ID = "instance_id" +CHROME_INSTANCE_OPTIONS = "chrome_options" +RASTERIZETION_COUNT = "rasteriztion_count" + + try: env_max_rasterizations_count = os.getenv('MAX_RASTERIZATIONS_COUNT', '500') MAX_RASTERIZATIONS_COUNT = int(env_max_rasterizations_count) @@ -96,8 +104,7 @@ LOCAL_CHROME_HOST = "127.0.0.1" CHROME_LOG_FILE_PATH = "/var/chrome_headless.log" -CHROME_INSTANCES_FILE_PATH = '/var/chrome_instances.tsv' -RASTERIZATIONS_COUNTER_FILE_PATH = '/var/rasterizations_counter.txt' +CHROME_INSTANCES_FILE_PATH = '/var/chrome_instances.json' class RasterizeType(Enum): @@ -282,26 +289,84 @@ def get_chrome_browser(port: str) -> pychrome.Browser | None: return None -def read_file(filename): +def read_json_file(json_file_path: str = CHROME_INSTANCES_FILE_PATH) -> dict[str, Any]: + """ + Read the content from a JSON file and return it as a Python dictionary or list. + :param file_path: Path to the JSON file. + :return: The JSON content as a Python dictionary or list, or None if the file does not exist or is empty. + """ + if not os.path.exists(json_file_path): + demisto.info(f"File '{json_file_path}' does not exist.") + return {} try: - with open(filename) as file: - ret_value = file.read() - demisto.info(f"File '{filename}' contents: {ret_value}.") - return ret_value - except FileNotFoundError: - demisto.info(f"File '{filename}' does not exist") - return None + with open(json_file_path) as file: + # Read and parse the JSON data + data = json.load(file) + return data + except json.JSONDecodeError: + demisto.debug(f"Error decoding JSON from the file '{json_file_path}'.") + return {} + + +def increase_counter_chrome_instances_file(chrome_port: str = ''): + """ + he function will increase the counter of the port "chrome_port"ץ + If the file "CHROME_INSTANCES_FILE_PATH" exists the function will increase the counter of the port "chrome_port." + + :param chrome_port: Port for Chrome instance. + """ + existing_data = read_json_file() + + if chrome_port in existing_data: + existing_data[chrome_port][RASTERIZETION_COUNT] = existing_data[chrome_port].get(RASTERIZETION_COUNT, 0) + 1 + write_chrome_instances_file(existing_data) + else: + demisto.info(f"Chrome port '{chrome_port}' not found.") + + +def terminate_port_chrome_instances_file(chrome_port: str = ''): + """ + he function will increase the counter of the port "chrome_port"ץ + If the file "CHROME_INSTANCES_FILE_PATH" exists the function will increase the counter of the port "chrome_port." + + :param chrome_port: Port for Chrome instance. + """ + existing_data = read_json_file() + + if chrome_port in existing_data: + del existing_data[chrome_port] + write_chrome_instances_file(existing_data) + else: + demisto.info(f"Chrome port '{chrome_port}' not found.") + + +def add_new_chrome_instance(new_chrome_instance_content: Optional[Dict] = None) -> None: + """Add new Chrome instance content to the JSON file. + + :param new_chrome_instance_content: Data to write to the file. If None, an empty file is created. + + """ + existing_data = read_json_file() + + if new_chrome_instance_content: + existing_data.update(new_chrome_instance_content) + + write_chrome_instances_file(existing_data) + + +def write_chrome_instances_file(new_chrome_content: Optional[Dict] = {} + ): + """ + Add new Chrome instance content to the JSON file. + :param new_chrome_content: Data to write to the file. If None, an empty file is created. -def write_file(filename, contents, overwrite=False): - demisto.info(f"Saving File '{filename}' with {contents}.") - mode = 'w' if overwrite else 'a' + """ try: - with open(filename, mode, encoding='utf-8') as file: - file.write(("\n" if not overwrite else "") + contents) - demisto.info(f"File '{filename}' saved successfully with {contents}.") + with open(CHROME_INSTANCES_FILE_PATH, 'w') as file: + json.dump(new_chrome_content, file, indent=4) except Exception as e: - demisto.info(f"An error occurred while writing to the file '{filename}': {e}") + demisto.debug(f"An error occurred while writing to the file: {e}") def opt_name(opt): @@ -337,14 +402,14 @@ def get_chrome_options(default_options, user_options): return options -def start_chrome_headless(chrome_port, instance_id, chrome_options, chrome_binary=CHROME_EXE, user_options=""): +def start_chrome_headless(chrome_port, instance_id, chrome_options, chrome_binary=CHROME_EXE): try: logfile = open(CHROME_LOG_FILE_PATH, 'ab') default_chrome_options = CHROME_OPTIONS default_chrome_options.append(f"--remote-debugging-port={chrome_port}") subprocess_options = [chrome_binary] - user_chrome_options = demisto.params().get('chrome_options', "") + user_chrome_options = demisto.params().get(CHROME_INSTANCE_OPTIONS, "") subprocess_options.extend(get_chrome_options(default_chrome_options, user_chrome_options)) demisto.debug(f"Starting Chrome with {subprocess_options=}") @@ -352,13 +417,19 @@ def start_chrome_headless(chrome_port, instance_id, chrome_options, chrome_binar demisto.debug(f'Chrome started on port {chrome_port}, pid: {process.pid},returncode: {process.returncode}') if process: - demisto.debug(f'New Chrome session active on Port {chrome_port}') + demisto.debug(f'New Chrome session active on {chrome_port=}: {chrome_options=} {chrome_options=}') # Allow Chrome to initialize time.sleep(DEFAULT_RETRY_WAIT_IN_SECONDS) # pylint: disable=E9003 browser = get_chrome_browser(chrome_port) if browser: - new_row = f"{chrome_port}\t{instance_id}\t{chrome_options}" - write_file(CHROME_INSTANCES_FILE_PATH, new_row) + new_chrome_instance = { + chrome_port: { + INSTANCE_ID: instance_id, + CHROME_INSTANCE_OPTIONS: chrome_options, + RASTERIZETION_COUNT: 0 + } + } + add_new_chrome_instance(new_chrome_instance_content=new_chrome_instance) else: process.kill() return None, None @@ -418,7 +489,7 @@ def terminate_chrome(chrome_port: str = '', killall: bool = False) -> None: # p process.kill() except Exception as e: demisto.info(f"Exception when trying to kill chrome with {pid=}, {e}") - + terminate_port_chrome_instances_file(chrome_port=chrome_port) demisto.debug('terminate_chrome, Finish') @@ -445,60 +516,30 @@ def chrome_manager() -> tuple[Any | None, str | None]: # This way, when fetching the content from the file, if there was no instance_id or chrome_options before, # it can compare between the fetched 'None' string and the 'None' that assigned. instance_id = demisto.callingContext.get('context', {}).get('IntegrationInstanceID', 'None') or 'None' - chrome_options = demisto.params().get('chrome_options') or 'None' - chrome_instances_contents = read_file(CHROME_INSTANCES_FILE_PATH) - instance_id_to_chrome_options, instance_id_to_port, instances_id, chromes_options = \ - get_chrome_instances_contents_dictionaries(chrome_instances_contents) - - if not chrome_instances_contents or instance_id not in instances_id: + chrome_options = demisto.params().get('chrome_options', 'None') + chrome_instances_contents = read_json_file(CHROME_INSTANCES_FILE_PATH) + instance_id_dict = { + value[INSTANCE_ID]: { + 'chrome_port': key, + CHROME_INSTANCE_OPTIONS: value[CHROME_INSTANCE_OPTIONS] + } + for key, value in chrome_instances_contents.items() + } + if not chrome_instances_contents or instance_id not in instance_id_dict.keys(): return generate_new_chrome_instance(instance_id, chrome_options) - elif chrome_options != instance_id_to_chrome_options.get(instance_id): - chrome_port = instance_id_to_port.get(instance_id, '') - delete_row_with_old_chrome_configurations_from_chrome_instances_file(chrome_instances_contents, instance_id, chrome_port) + elif chrome_options != instance_id_dict.get(instance_id, {}).get(CHROME_INSTANCE_OPTIONS, ''): + # If the current Chrome options differ from the saved options for this instance ID, + # it terminates the existing Chrome instance and generates a new one with the new options. + chrome_port = instance_id_dict.get(instance_id, {}).get('chrome_port', '') terminate_chrome(chrome_port=chrome_port) return generate_new_chrome_instance(instance_id, chrome_options) - chrome_port = instance_id_to_port.get(instance_id, '') + chrome_port = instance_id_dict.get(instance_id, {}).get('chrome_port', '') browser = get_chrome_browser(chrome_port) return browser, chrome_port -def get_chrome_instances_contents_dictionaries(chrome_instances_contents: str) -> tuple[ - Dict[str, str], Dict[str, str], List[str], List[str]]: - """ - Parses the chrome instances content to extract and return two dictionaries and two lists. - - Args: - chrome_instances_contents: The file content to be parsed. - - Returns: - tuple: A tuple containing: - - instance_id_to_chrome_options (dict): A dictionary mapping instance ID to Chrome options. - - instance_id_to_port (dict): A dictionary mapping instance ID to port. - - instances_id (list): A list of instances ID extracted from instance_id_to_port keys. - - chromes_options (list): A list of Chrome options extracted from instance_id_to_chrome_options values. - - The purpose of this method is to transform the file content into dictionaries and lists - for easier access and manipulation of the data. - """ - instance_id_to_chrome_options = {} - instance_id_to_port = {} - - if chrome_instances_contents: - splitted_chrome_instances_contents = chrome_instances_contents.strip().splitlines() - for line in splitted_chrome_instances_contents: - port, instance_id, chrome_options = line.strip().split('\t') - instance_id_to_chrome_options[instance_id] = chrome_options - instance_id_to_port[instance_id] = port - - instances_id = list(instance_id_to_port.keys()) - chromes_options = list(instance_id_to_chrome_options.values()) - if instances_id and not chromes_options: - chromes_options.append('None') - return instance_id_to_chrome_options, instance_id_to_port, instances_id, chromes_options - - def generate_new_chrome_instance(instance_id: str, chrome_options: str) -> tuple[Any | None, str | None]: chrome_port = generate_chrome_port() return start_chrome_headless(chrome_port, instance_id, chrome_options) @@ -524,41 +565,7 @@ def generate_chrome_port() -> str | None: return None -def delete_row_with_old_chrome_configurations_from_chrome_instances_file(chrome_instances_contents: str, instance_id: str, - chrome_port: str) -> None: - """ - Removes a specific row from the given content based on the instance ID and port, - and updates the file with the new content. - - Args: - chrome_instances_contents (str): The file content to be searched and modified. - instance_id (str): The instance ID to search for in the content. - chrome_port (str): The port to search for in the content. - - Returns: - None - - This function searches for a row in the content that includes the specified instance ID and port. - Once the row is found, it is deleted from the content. The updated content is then written - back to the file using the write_file function. - """ - index_to_delete = -1 - - splitted_chrome_instances_contents = chrome_instances_contents.strip().splitlines() - for index, line in enumerate(splitted_chrome_instances_contents): - port_from_chrome_instances_file, instance_id_from_chrome_instances_file, chrome_options_from_chrome_instances_file = \ - line.strip().split('\t') - if port_from_chrome_instances_file == chrome_port and instance_id_from_chrome_instances_file == instance_id: - index_to_delete = index - break - - if index_to_delete >= 0: - del splitted_chrome_instances_contents[index_to_delete] - chrome_instances_contents = '\n'.join(splitted_chrome_instances_contents) - write_file(CHROME_INSTANCES_FILE_PATH, chrome_instances_contents, overwrite=True) - - -def setup_tab_event(browser: pychrome.Browser, tab: pychrome.Tab) -> tuple[PychromeEventHandler, Event]: +def setup_tab_event(browser: pychrome.Browser, tab: pychrome.Tab) -> tuple[PychromeEventHandler, Event]: # pragma: no cover tab_ready_event = Event() tab_event_handler = PychromeEventHandler(browser, tab, tab_ready_event) @@ -791,14 +798,15 @@ def perform_rasterize(path: str | list[str], :param width: window width :param height: window height """ - demisto.debug(f"rasterize, {path=}, {rasterize_type=}") + demisto.debug(f"perform_rasterize, {path=}, {rasterize_type=}") browser, chrome_port = chrome_manager() + if browser: support_multithreading() with ThreadPoolExecutor(max_workers=MAX_CHROME_TABS_COUNT) as executor: demisto.debug(f'path type is: {type(path)}') paths = [path] if isinstance(path, str) else path - demisto.debug(f"rasterize, {paths=}, {rasterize_type=}") + demisto.debug(f"perform_rasterize, {paths=}, {rasterize_type=}") rasterization_threads = [] rasterization_results = [] for current_path in paths: @@ -818,23 +826,22 @@ def perform_rasterize(path: str | list[str], # Wait for all tasks to complete executor.shutdown(wait=True) demisto.info( - f"Finished {len(rasterization_threads)} rasterize operations, active tabs len: {len(browser.list_tab())}") - - previous_rasterizations_counter_from_file = read_file(RASTERIZATIONS_COUNTER_FILE_PATH) - if previous_rasterizations_counter_from_file: - total_rasterizations_count = int(previous_rasterizations_counter_from_file) + len(rasterization_threads) - else: - total_rasterizations_count = len(rasterization_threads) - demisto.debug(f"Should Chrome be terminated?, {total_rasterizations_count=}," - f" {MAX_RASTERIZATIONS_COUNT=}, {len(browser.list_tab())=}") - if total_rasterizations_count > MAX_RASTERIZATIONS_COUNT: - demisto.info(f"Terminating Chrome after {total_rasterizations_count} rasterizations") - terminate_chrome(killall=True) - write_file(CHROME_INSTANCES_FILE_PATH, "", overwrite=True) - demisto.info(f"Terminated Chrome after {total_rasterizations_count} rasterizations") - write_file(RASTERIZATIONS_COUNTER_FILE_PATH, "0", overwrite=True) + f"perform_rasterize Finished {len(rasterization_threads)} rasterize operations," + f"active tabs len: {len(browser.list_tab())}") + + chrome_instances_file_content: dict = read_json_file() # CR fix name + rasterizations_count = chrome_instances_file_content.get(chrome_port, {}).get(RASTERIZETION_COUNT, 0) + len( + rasterization_threads) + + demisto.debug(f"perform_rasterize checking if the chrome in port:{chrome_port} should be deleted:" + f"{rasterizations_count=}, {MAX_RASTERIZATIONS_COUNT=}, {len(browser.list_tab())=}") + if not chrome_port: + demisto.debug("perform_rasterize: the chrome port was not found") + elif rasterizations_count >= MAX_RASTERIZATIONS_COUNT: + demisto.info(f"perform_rasterize: terminating Chrome after {rasterizations_count=} rasterizations") + terminate_chrome(chrome_port=chrome_port) else: - write_file(RASTERIZATIONS_COUNTER_FILE_PATH, str(total_rasterizations_count), overwrite=True) + increase_counter_chrome_instances_file(chrome_port=chrome_port) # Get the results for current_thread in rasterization_threads: diff --git a/Packs/rasterize/Integrations/rasterize/rasterize_test.py b/Packs/rasterize/Integrations/rasterize/rasterize_test.py index 72169de4edf8..c5c0671b2418 100644 --- a/Packs/rasterize/Integrations/rasterize/rasterize_test.py +++ b/Packs/rasterize/Integrations/rasterize/rasterize_test.py @@ -11,6 +11,7 @@ import threading import pytest import requests +import json # disable warning from urllib3. these are emitted when python driver can't connect to chrome yet logging.getLogger("urllib3").setLevel(logging.ERROR) @@ -18,15 +19,15 @@ RETURN_ERROR_TARGET = 'rasterize.return_error' -def util_read_tsv(filename): - with open(filename) as file: +def util_read_tsv(file_path): + with open(file_path) as file: ret_value = file.read() return ret_value -def util_generate_mock_info_file(info): - from rasterize import write_file - write_file("test_data/info.tsv", info, overwrite=True) +def util_load_json(path): + with open(path, encoding='utf-8') as f: + return json.loads(f.read()) def test_rasterize_email_image(caplog, capfd, mocker): @@ -402,8 +403,7 @@ def test_chrome_manager_case_chrome_instances_file_is_empty(mocker): mocker.patch.object(demisto, 'callingContext', mock_context) mocker.patch.object(demisto, 'params', return_value=params) - mocker.patch.object(rasterize, 'read_file', return_value=None) - mocker.patch.object(rasterize, 'get_chrome_instances_contents_dictionaries', return_value=[{}, {}, {}, {}]) + mocker.patch.object(rasterize, 'read_json_file', return_value={}) generate_new_chrome_instance_mocker = mocker.patch.object(rasterize, 'generate_new_chrome_instance', return_value=["browser_object", "chrome_port"]) terminate_chrome_mocker = mocker.patch.object(rasterize, 'terminate_chrome', return_value=None) @@ -422,11 +422,10 @@ def test_chrome_manager_case_chromes_options_exist_and_instance_id_not_linked(mo When chrome instances file is not empty and instance id is not linked to the chrome options Then make sure code running into case 2 and calling generate_new_chrome_instance which return browser and chrome port. """ - from rasterize import chrome_manager - from rasterize import get_chrome_instances_contents_dictionaries + from rasterize import chrome_manager, read_json_file instance_id = "instance_id_that_does_not_exist" - chrome_options = "chrome_options2" # exist + chrome_options = "chrome_options2" mock_context = { 'context': { @@ -438,15 +437,10 @@ def test_chrome_manager_case_chromes_options_exist_and_instance_id_not_linked(mo 'chrome_options': chrome_options } - mock_file_content = util_read_tsv("test_data/info.tsv") - mock_file_content_edited = mock_file_content.replace('\\t', '\t') - instance_id_to_chrome_options, instance_id_to_port, instances_id, chromes_options = \ - get_chrome_instances_contents_dictionaries(mock_file_content_edited) + mock_file_content = read_json_file("test_data/chrome_instances.json") mocker.patch.object(demisto, 'callingContext', mock_context) mocker.patch.object(demisto, 'params', return_value=params) - mocker.patch.object(rasterize, 'read_file', return_value=mock_file_content_edited) - mocker.patch.object(rasterize, 'get_chrome_instances_contents_dictionaries', - return_value=[instance_id_to_chrome_options, instance_id_to_port, instances_id, chromes_options]) + mocker.patch.object(rasterize, 'read_json_file', return_value=mock_file_content) generate_new_chrome_instance_mocker = mocker.patch.object(rasterize, 'generate_new_chrome_instance', return_value=["browser_object", "chrome_port"]) terminate_chrome_mocker = mocker.patch.object(rasterize, 'terminate_chrome', return_value=None) @@ -465,7 +459,7 @@ def test_chrome_manager_case_new_chrome_options_and_instance_id(mocker): When chrome instances file is not empty Then make sure code running into case 3 and calling generate_new_chrome_instance which return browser and chrome port. """ - from rasterize import chrome_manager + from rasterize import chrome_manager, read_json_file instance_id = "instance_id_that_does_not_exist" chrome_options = "chrome_options_that_does_not_exist" @@ -480,15 +474,11 @@ def test_chrome_manager_case_new_chrome_options_and_instance_id(mocker): 'chrome_options': chrome_options } - mock_file_content = util_read_tsv("test_data/info.tsv") - mock_file_content_edited = mock_file_content.replace('\\t', '\t') - instance_id_to_chrome_options, instance_id_to_port, instances_id, chromes_options = \ - get_chrome_instances_contents_dictionaries(mock_file_content_edited) + mock_file_content = read_json_file("test_data/chrome_instances.json") + mocker.patch.object(demisto, 'callingContext', mock_context) mocker.patch.object(demisto, 'params', return_value=params) - mocker.patch.object(rasterize, 'read_file', return_value=mock_file_content_edited) - mocker.patch.object(rasterize, 'get_chrome_instances_contents_dictionaries', - return_value=[instance_id_to_chrome_options, instance_id_to_port, instances_id, chromes_options]) + mocker.patch.object(rasterize, 'read_json_file', return_value=mock_file_content) generate_new_chrome_instance_mocker = mocker.patch.object(rasterize, 'generate_new_chrome_instance', return_value=["browser_object", "chrome_port"]) terminate_chrome_mocker = mocker.patch.object(rasterize, 'terminate_chrome', return_value=None) @@ -508,7 +498,7 @@ def test_chrome_manager_case_instance_id_exist_but_new_chrome_options(mocker): Then make sure code running into case 4, terminating old chrome port, generating new one, and update the chrome instances file. """ - from rasterize import chrome_manager + from rasterize import chrome_manager, read_json_file instance_id = "22222222-2222-2222-2222-222222222222" # exist chrome_options = "chrome_options_that_does_not_exist" @@ -523,18 +513,14 @@ def test_chrome_manager_case_instance_id_exist_but_new_chrome_options(mocker): 'chrome_options': chrome_options } - mock_file_content = util_read_tsv("test_data/info.tsv") - mock_file_content_edited = mock_file_content.replace('\\t', '\t') - instance_id_to_chrome_options, instance_id_to_port, instances_id, chromes_options = \ - get_chrome_instances_contents_dictionaries(mock_file_content_edited) + mock_file_content = read_json_file("test_data/chrome_instances.json") + mocker.patch.object(demisto, 'callingContext', mock_context) mocker.patch.object(demisto, 'params', return_value=params) - mocker.patch.object(rasterize, 'read_file', return_value=mock_file_content_edited) - mocker.patch.object(rasterize, 'get_chrome_instances_contents_dictionaries', - return_value=[instance_id_to_chrome_options, instance_id_to_port, instances_id, chromes_options]) + mocker.patch.object(rasterize, 'read_json_file', return_value=mock_file_content) + mocker.patch.object(rasterize, 'get_chrome_browser', return_value=None) terminate_chrome_mocker = mocker.patch.object(rasterize, 'terminate_chrome', return_value=None) - mocker.patch.object(rasterize, 'delete_row_with_old_chrome_configurations_from_chrome_instances_file', return_value=None) generate_new_chrome_instance_mocker = mocker.patch.object(rasterize, 'generate_new_chrome_instance', return_value=["browser_object", "chrome_port"]) browser, chrome_port = chrome_manager() @@ -552,7 +538,7 @@ def test_chrome_manager_case_instance_id_and_chrome_options_exist_and_linked(moc When chrome instances file is not empty, and instance id and chrome options linked. Then make sure code running into case 5 and using the browser that already in used. """ - from rasterize import chrome_manager + from rasterize import chrome_manager, read_json_file instance_id = "22222222-2222-2222-2222-222222222222" # exist chrome_options = "chrome_options2" @@ -567,21 +553,17 @@ def test_chrome_manager_case_instance_id_and_chrome_options_exist_and_linked(moc 'chrome_options': chrome_options } - mock_file_content = util_read_tsv("test_data/info.tsv") - mock_file_content_edited = mock_file_content.replace('\\t', '\t') - instance_id_to_chrome_options, instance_id_to_port, instances_id, chromes_options = \ - get_chrome_instances_contents_dictionaries(mock_file_content_edited) + mock_file_content = read_json_file("test_data/chrome_instances.json") + mocker.patch.object(demisto, 'callingContext', mock_context) mocker.patch.object(demisto, 'params', return_value=params) - mocker.patch.object(rasterize, 'read_file', return_value=mock_file_content_edited) - mocker.patch.object(rasterize, 'get_chrome_instances_contents_dictionaries', - return_value=[instance_id_to_chrome_options, instance_id_to_port, instances_id, chromes_options]) + mocker.patch.object(rasterize, 'read_json_file', return_value=mock_file_content) + mocker.patch.object(rasterize, 'get_chrome_browser', return_value="browser_object") terminate_chrome_mocker = mocker.patch.object(rasterize, 'terminate_chrome', return_value=None) generate_new_chrome_instance_mocker = mocker.patch.object(rasterize, 'generate_new_chrome_instance', return_value=["browser_object", "chrome_port"]) browser, chrome_port = chrome_manager() - assert terminate_chrome_mocker.call_count == 0 assert generate_new_chrome_instance_mocker.call_count == 0 assert browser == "browser_object" @@ -614,66 +596,6 @@ def test_generate_chrome_port_no_port_available(mocker): assert not port -def test_get_chrome_instances_contents_dictionaries(): - """ - Given chrome instances file with content - When extract the data from it and parse it for 2 dictionaries and 2 lists: - - instance_id_to_chrome_options (dict): A dictionary mapping instance ID to Chrome options. - - instance_id_to_port (dict): A dictionary mapping instance ID to Chrome port. - - instances_id (list): A list of instances ID extracted from instance_id_to_port keys. - - chromes_options (list): A list of Chrome options extracted from instance_id_to_chrome_options values. - Then make sure the data are extracted correctly according the mock data file content. - """ - from rasterize import get_chrome_instances_contents_dictionaries - mock_file_content = util_read_tsv("test_data/info.tsv") - mock_file_content_edited = mock_file_content.replace('\\t', '\t') - instance_id_to_chrome_options, instance_id_to_port, instances_id, chromes_options = \ - (get_chrome_instances_contents_dictionaries(mock_file_content_edited)) - assert instance_id_to_chrome_options == {'22222222-2222-2222-2222-222222222222': 'chrome_options2', - '33333333-3333-3333-3333-333333333333': 'chrome_options3', - '44444444-4444-4444-4444-444444444444': 'chrome_options4'} - assert instance_id_to_port == {'22222222-2222-2222-2222-222222222222': '2222', '33333333-3333-3333-3333-333333333333': '3333', - '44444444-4444-4444-4444-444444444444': '4444'} - assert instances_id - assert instances_id == ['22222222-2222-2222-2222-222222222222', '33333333-3333-3333-3333-333333333333', - '44444444-4444-4444-4444-444444444444'] - assert chromes_options - assert chromes_options == ['chrome_options2', 'chrome_options3', 'chrome_options4'] - - -def test_delete_row_with_old_chrome_configurations_from_info_file(): - """ - Given chrome instances file with content - When need to delete row with old chrome configurations from the file - Then make sure it delete the specific row should be deleted - """ - from rasterize import delete_row_with_old_chrome_configurations_from_chrome_instances_file - - rasterize.CHROME_INSTANCES_FILE_PATH = "test_data/info.tsv" - - mock_info = """2222\t22222222-2222-2222-2222-222222222222\tchrome_options2 - 3333\t33333333-3333-3333-3333-333333333333\tchrome_options3 - test\ttesttest-test-test-test-testtesttest\tchrome_options0 - 4444\t44444444-4444-4444-4444-444444444444\tchrome_options4 - """ - util_generate_mock_info_file(mock_info) - chrome_port_to_delete = "test" - instance_id_to_delete = "testtest-test-test-test-testtesttest" - - delete_row_with_old_chrome_configurations_from_chrome_instances_file( - mock_info, instance_id_to_delete, chrome_port_to_delete) - - mock_file_content = util_read_tsv("test_data/info.tsv") - mock_file_content_edited = mock_file_content.replace('\\t', '\t') - - expected_mock_file_content = """2222\t22222222-2222-2222-2222-222222222222\tchrome_options2 - 3333\t33333333-3333-3333-3333-333333333333\tchrome_options3 - 4444\t44444444-4444-4444-4444-444444444444\tchrome_options4 - """.strip() - - assert expected_mock_file_content == mock_file_content_edited - - def test_get_chrome_browser_error(mocker: MockerFixture): """ Given A connection error. @@ -729,3 +651,113 @@ def test_is_mailto_urls(mocker: MockerFixture): res = screenshot_image(None, None, 'url', None, None) assert res == (None, 'URLs that start with "mailto:" cannot be rasterized.\nURL: url') + + +def test_increase_counter_chrome_instances_file(mocker): + """ + Given: + - A new Chrome instance content + - A valid Chrome port + - An increase counter + - A terminate port + When: + - Executing the increase_counter_chrome_instances_file function + Then: + - The function writes to the correct file and increase the "rasteriztion_count" by 1 + """ + from rasterize import increase_counter_chrome_instances_file, RASTERIZETION_COUNT + from unittest.mock import mock_open + mocker.patch("os.path.exists", return_value=True) + mock_file_content = util_load_json("test_data/chrome_instances.json") + expected_rasterizetion_count = mock_file_content['2222'][RASTERIZETION_COUNT] + 1 + mock_file = mock_open() + mocker.patch("builtins.open", mock_file) + mocker.patch.object(json, 'load', return_value=mock_file_content) + mocker_json = mocker.patch("json.dump") + increase_counter_chrome_instances_file(chrome_port="2222") + assert mocker_json.called + assert expected_rasterizetion_count == mocker_json.call_args[0][0]['2222'][RASTERIZETION_COUNT] + + +def test_add_new_chrome_instance(mocker): + """ + Given: + - A new Chrome instance content + When: + - Executing the add_new_chrome_instance function + Then: + - The function writes to the correct file the new chrome instance. + """ + from rasterize import add_new_chrome_instance + from unittest.mock import mock_open + mocker.patch("os.path.exists", return_value=True) + mock_file_content = util_load_json("test_data/chrome_instances.json") + mock_file = mock_open() + mocker.patch("builtins.open", mock_file) + mocker.patch.object(json, 'load', return_value=mock_file_content) + mocker_json = mocker.patch("json.dump") + add_new_chrome_instance(new_chrome_instance_content={"9345": { + "instance_id": "44444444-4444-4444-4444-444444444444", + "chrome_options": "chrome_options4", + "rasterize_count": 1 + }}) + assert mocker_json.called + assert '9345' in mocker_json.call_args[0][0] + + +def test_terminate_port_chrome_instances_file(mocker): + """ + Given: + - A port to terminate. + When: + - Executing the terminate_port_chrome_instances_file function + Then: + - The function writes to the correct file the data without the port to terminate. + """ + from rasterize import terminate_port_chrome_instances_file + from unittest.mock import mock_open + mocker.patch("os.path.exists", return_value=True) + mock_file_content = util_load_json("test_data/chrome_instances.json") + mock_file = mock_open() + mocker.patch("builtins.open", mock_file) + mocker.patch.object(json, 'load', return_value=mock_file_content) + mocker_json = mocker.patch("json.dump") + terminate_port_chrome_instances_file(chrome_port="2222") + assert mocker_json.called + assert '2222' not in mocker_json.call_args[0][0] + + +def test_write_chrome_instances_empty(mocker): + """ + Given: + - A new Chrome instance content(first chrome instance). + When: + - Executing the write_chrome_instances_file function + Then: + - The function creates and writes to the correct file, calls json.dump with the expected arguments. + """ + from rasterize import write_chrome_instances_file + from unittest.mock import mock_open + mock_file_content = util_load_json("test_data/chrome_instances.json") + mock_file = mock_open() + mocker.patch("builtins.open", mock_file) + mocker_json = mocker.patch.object(json, 'dump', return_value=mock_file_content) + write_chrome_instances_file(new_chrome_content=mock_file_content) + + assert mocker_json.call_count == 1 + + +def test_read_json_file(mocker): + """ + Given: + - A JSON file at 'test_data/chrome_instances.json' + When: + - Executing the read_json_file function + Then: + - The function reads the JSON file and returns the correct content. + """ + from rasterize import read_json_file + mocker.patch("os.path.exists", return_value=True) + mock_file_content = util_load_json("test_data/chrome_instances.json") + file_result = read_json_file("test_data/chrome_instances.json") + assert file_result == mock_file_content diff --git a/Packs/rasterize/Integrations/rasterize/test_data/chrome_instances.json b/Packs/rasterize/Integrations/rasterize/test_data/chrome_instances.json new file mode 100644 index 000000000000..ac3d45941085 --- /dev/null +++ b/Packs/rasterize/Integrations/rasterize/test_data/chrome_instances.json @@ -0,0 +1,17 @@ +{ + "2222": { + "instance_id": "22222222-2222-2222-2222-222222222222", + "chrome_options": "chrome_options2", + "rasteriztion_count": 1 + }, + "3333": { + "instance_id": "33333333-3333-3333-3333-333333333333", + "chrome_options": "chrome_options3", + "rasteriztion_count": 1 + }, + "9345": { + "instance_id": "44444444-4444-4444-4444-444444444444", + "chrome_options": "chrome_options4", + "rasteriztion_count": 1 + } +} diff --git a/Packs/rasterize/Integrations/rasterize/test_data/example_log_file.txt b/Packs/rasterize/Integrations/rasterize/test_data/example_log_file.txt new file mode 100644 index 000000000000..6ef87d7ef26b --- /dev/null +++ b/Packs/rasterize/Integrations/rasterize/test_data/example_log_file.txt @@ -0,0 +1,6 @@ +timestamp,log_level,message +2024-09-22 10:15:30,INFO,Starting the application +2024-09-22 10:15:31,DEBUG,Loading configuration from config.yaml +2024-09-22 10:15:32,WARN,Memory usage is high +2024-09-22 10:15:33,ERROR,Failed connect to database +2024-09-22 10:15:34,INFO,Application stopped diff --git a/Packs/rasterize/Integrations/rasterize/test_data/info.tsv b/Packs/rasterize/Integrations/rasterize/test_data/info.tsv deleted file mode 100644 index 4c2ce83a8a6d..000000000000 --- a/Packs/rasterize/Integrations/rasterize/test_data/info.tsv +++ /dev/null @@ -1,3 +0,0 @@ -2222 22222222-2222-2222-2222-222222222222 chrome_options2 - 3333 33333333-3333-3333-3333-333333333333 chrome_options3 - 4444 44444444-4444-4444-4444-444444444444 chrome_options4 \ No newline at end of file diff --git a/Packs/rasterize/ReleaseNotes/2_0_24.md b/Packs/rasterize/ReleaseNotes/2_0_24.md new file mode 100644 index 000000000000..235d4a8a6b59 --- /dev/null +++ b/Packs/rasterize/ReleaseNotes/2_0_24.md @@ -0,0 +1,6 @@ + +#### Integrations + +##### Rasterize + +Fixed an issue where utilizing the ***rasterize*** command after reaching the **max rasterize connections** would result in the error message "Could not use local Chrome for rasterize command." diff --git a/Packs/rasterize/pack_metadata.json b/Packs/rasterize/pack_metadata.json index ed064e2c8078..7af9723defb6 100644 --- a/Packs/rasterize/pack_metadata.json +++ b/Packs/rasterize/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Rasterize", "description": "Converts URLs, PDF files, and emails to an image file or PDF file.", "support": "xsoar", - "currentVersion": "2.0.23", + "currentVersion": "2.0.24", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "",