From 13ef285e79f1a4f225d02cdf85dcb61a0147c3b5 Mon Sep 17 00:00:00 2001 From: DinaMeylakh <72339665+DinaMeylakh@users.noreply.github.com> Date: Mon, 19 Dec 2022 10:33:58 +0200 Subject: [PATCH] Add deletion in content managment (#20700) --- Packs/ContentManagement/ReleaseNotes/1_2_0.md | 4 + .../Scripts/DeleteContent/DeleteContent.py | 670 ++++++++++++++++++ .../Scripts/DeleteContent/DeleteContent.yml | 75 ++ .../DeleteContent/DeleteContent_test.py | 621 ++++++++++++++++ .../Scripts/DeleteContent/README.md | 30 + Packs/ContentManagement/pack_metadata.json | 2 +- 6 files changed, 1401 insertions(+), 1 deletion(-) create mode 100644 Packs/ContentManagement/ReleaseNotes/1_2_0.md create mode 100644 Packs/ContentManagement/Scripts/DeleteContent/DeleteContent.py create mode 100644 Packs/ContentManagement/Scripts/DeleteContent/DeleteContent.yml create mode 100644 Packs/ContentManagement/Scripts/DeleteContent/DeleteContent_test.py create mode 100644 Packs/ContentManagement/Scripts/DeleteContent/README.md diff --git a/Packs/ContentManagement/ReleaseNotes/1_2_0.md b/Packs/ContentManagement/ReleaseNotes/1_2_0.md new file mode 100644 index 000000000000..a20665fe5aa3 --- /dev/null +++ b/Packs/ContentManagement/ReleaseNotes/1_2_0.md @@ -0,0 +1,4 @@ + +#### Scripts +##### New: DeleteContent +- Added the DeleteContent script. Use it to keep your XSOAR instance clean and tidy. diff --git a/Packs/ContentManagement/Scripts/DeleteContent/DeleteContent.py b/Packs/ContentManagement/Scripts/DeleteContent/DeleteContent.py new file mode 100644 index 000000000000..bdf6206f23fb --- /dev/null +++ b/Packs/ContentManagement/Scripts/DeleteContent/DeleteContent.py @@ -0,0 +1,670 @@ +"""Delete Content script, used to keep instances tidy.""" +from CommonServerPython import * + +from abc import ABC, abstractmethod +from typing import Tuple +from urllib.parse import quote + +import requests +import json + +SCRIPT_NAME = 'DeleteContent' +CORE_PACKS_LIST_URL = "https://raw.githubusercontent.com/demisto/content/master/Tests/Marketplace/core_packs_list.json" + + +def verify_search_response_in_list(response: Any, name: str): + ids = [entity.get('id', '') for entity in response] if type(response) is list else [] + return False if name not in ids else name + + +def verify_search_response_in_dict(response: Union[dict, str, list]): + if type(response) is dict and response.get("id"): + return response.get("id") + return False + + +class EntityAPI(ABC): + """Abstract class for APIs of different content entities.""" + name = '' + + @abstractmethod + def search_specific_id(self, specific_id: str): + pass + + @abstractmethod + def search_all(self): + pass + + @abstractmethod + def delete_specific_id(self, specific_id: str): + pass + + @abstractmethod + def verify_specific_search_response(self, response: Union[dict, str], name: str): + pass + + def parse_all_entities_response(self, response: Union[dict, str, list]): + return [entity.get('id', '') for entity in response] if type(response) is list else [] + + +class PlaybookAPI(EntityAPI): # works + name = 'playbook' + + def search_specific_id(self, specific_id: str): + return execute_command('demisto-api-get', + {'uri': f'/playbook/{specific_id}'}, + fail_on_error=False) + + def search_all(self): + return execute_command('demisto-api-post', + {'uri': '/playbook/search', + 'body': {'page': 0, 'size': 100}}, + fail_on_error=False) + + def delete_specific_id(self, specific_id: str): + return execute_command('demisto-api-post', + {'uri': '/playbook/delete', + 'body': {'id': specific_id}}, + fail_on_error=False) + + def verify_specific_search_response(self, response: Union[dict, str], name: str): + return verify_search_response_in_dict(response) + + def parse_all_entities_response(self, response: Union[dict, str, list]): + return [entity.get('id', '') for entity in response.get('playbooks', [])] if type(response) is dict else [] + + +class IntegrationAPI(EntityAPI): # works + name = 'integration' + + def search_specific_id(self, specific_id: str): + return execute_command('demisto-api-post', + {'uri': '/settings/integration/search', + 'body': {'page': 0, 'size': 100, 'query': f'name:"{specific_id}"'}}, + fail_on_error=False) + + def search_all(self): + return execute_command('demisto-api-post', + {'uri': '/settings/integration/search', + 'body': {'page': 0, 'size': 100}}, + fail_on_error=False) + + def delete_specific_id(self, specific_id: str): + return execute_command('demisto-api-post', + {'uri': '/settings/integration-conf/delete', + 'body': {'id': quote(specific_id)}}, + fail_on_error=False) + + def verify_specific_search_response(self, response: Union[dict, str, list], name: str): + integrations = response.get('configurations', []) if type(response) is dict else response + return verify_search_response_in_list(integrations, name) + + def parse_all_entities_response(self, response: Union[dict, str, list]): + integrations = response.get('configurations', []) if type(response) is dict else response + return [entity.get('id') for entity in integrations] if type(integrations) is list else [] + + +class ScriptAPI(EntityAPI): # works :) + name = 'script' + always_excluded = ['CommonServerUserPowerShell', 'CommonServerUserPython', 'CommonUserServer', SCRIPT_NAME] + + def search_specific_id(self, specific_id: str): + return execute_command('demisto-api-post', + {'uri': '/automation/search', + 'body': {'page': 0, 'size': 1, 'query': f'name:"{specific_id}"'}}, + fail_on_error=False) + + def search_all(self): + return execute_command('demisto-api-post', + {'uri': '/automation/search', + 'body': {'page': 0, 'size': 100}}, + fail_on_error=False) + + def delete_specific_id(self, specific_id: str): + return execute_command('demisto-api-post', + {'uri': '/automation/delete', + 'body': {'script': {'id': specific_id}}}, + fail_on_error=False) + + def verify_specific_search_response(self, response: Union[dict, str, list], name: str): + scripts = response.get('scripts') if type(response) is dict else response + return verify_search_response_in_list(scripts, name) + + def parse_all_entities_response(self, response: Union[dict, str, list]): + return [entity.get('id', '') for entity in response.get('scripts', [])] if type(response) is dict else [] + + +class IncidentFieldAPI(EntityAPI): # checked and works + name = 'incidentfield' + + def search_specific_id(self, specific_id: str): + return execute_command('demisto-api-get', + {'uri': '/incidentfields'}, + fail_on_error=False) + + def search_all(self): + return execute_command('demisto-api-get', + {'uri': '/incidentfields'}, + fail_on_error=False) + + def delete_specific_id(self, specific_id: str): + return execute_command('demisto-api-delete', + {'uri': f'/incidentfield/{specific_id}'}, + fail_on_error=False) + + def verify_specific_search_response(self, response: Union[dict, str], name: str): + return verify_search_response_in_list(response, name) + + +class PreProcessingRuleAPI(EntityAPI): # checked and works + name = 'pre-process-rule' + + def search_specific_id(self, specific_id: str): + return execute_command('demisto-api-get', + {'uri': '/preprocess/rules'}, + fail_on_error=False) + + def search_all(self): + return execute_command('demisto-api-get', + {'uri': '/preprocess/rules'}, + fail_on_error=False) + + def delete_specific_id(self, specific_id: str): + return execute_command('demisto-api-delete', + {'uri': f'/preprocess/rule/{specific_id}'}, + fail_on_error=False) + + def verify_specific_search_response(self, response: Union[dict, str, list], name: str): + return verify_search_response_in_list(response, name) + + +class WidgetAPI(EntityAPI): # works + name = 'widget' + + def search_specific_id(self, specific_id: str): + return execute_command('demisto-api-get', + {'uri': f'/widgets/{specific_id}'}, + fail_on_error=False) + + def search_all(self): + return execute_command('demisto-api-get', + {'uri': '/widgets'}, + fail_on_error=False) + + def delete_specific_id(self, specific_id: str): + return execute_command('demisto-api-delete', + {'uri': f'/widgets/{specific_id}'}, + fail_on_error=False) + + def verify_specific_search_response(self, response: Union[dict, str], name: str): + return verify_search_response_in_dict(response) + + def parse_all_entities_response(self, response: Union[dict, str, list]): + if type(response) is dict: + return list(response.keys()) + return [entity.get('id', '') for entity in response] if type(response) is list else [] + + +class DashboardAPI(EntityAPI): # works + name = 'dashboard' + + def search_specific_id(self, specific_id: str): + return execute_command('demisto-api-get', + {'uri': f'/dashboards/{specific_id}'}, + fail_on_error=False) + + def search_all(self): + return execute_command('demisto-api-get', + {'uri': '/dashboards'}, + fail_on_error=False) + + def delete_specific_id(self, specific_id: str): + return execute_command('demisto-api-delete', + {'uri': f'/dashboards/{specific_id}'}, + fail_on_error=False) + + def verify_specific_search_response(self, response: Union[dict, str], name: str): + return verify_search_response_in_dict(response) + + def parse_all_entities_response(self, response: Union[dict, str, list]): + if type(response) is dict: + return list(response.keys()) + return [entity.get('id', '') for entity in response] if type(response) is list else [] + + +class ReportAPI(EntityAPI): # works + name = 'report' + + def search_specific_id(self, specific_id: str): + return execute_command('demisto-api-get', + {'uri': f'/reports/{specific_id}'}, + fail_on_error=False) + + def search_all(self): + return execute_command('demisto-api-get', + {'uri': '/reports'}, + fail_on_error=False) + + def delete_specific_id(self, specific_id: str): + return execute_command('demisto-api-delete', + {'uri': f'/report/{specific_id}'}, + fail_on_error=False) + + def verify_specific_search_response(self, response: Union[dict, str], name: str): + return verify_search_response_in_dict(response) + + +class IncidentTypeAPI(EntityAPI): # checked and works + name = 'incidenttype' + + def search_specific_id(self, specific_id: str): + return execute_command('demisto-api-get', + {'uri': '/incidenttypes/export'}, + fail_on_error=False) + + def search_all(self): + return execute_command('demisto-api-get', + {'uri': '/incidenttypes/export'}, + fail_on_error=False) + + def delete_specific_id(self, specific_id: str): + return execute_command('demisto-api-post', + {'uri': '/incidenttype/delete', + 'body': {'id': specific_id}}, + fail_on_error=False) + + def verify_specific_search_response(self, response: Union[dict, str, list], name: str): + return verify_search_response_in_list(response, name) + + +class ClassifierAPI(EntityAPI): # works + name = 'classifier' + + def search_specific_id(self, specific_id: str): + return execute_command('demisto-api-get', + {'uri': f'/classifier/{specific_id}'}, + fail_on_error=False) + + def search_all(self): + return execute_command('demisto-api-post', + {'uri': '/classifier/search', + 'body': {'page': 0, 'size': 100}}, + fail_on_error=False) + + def delete_specific_id(self, specific_id: str): + return execute_command('demisto-api-delete', + {'uri': f'/classifier/{specific_id}'}, + fail_on_error=False) + + def verify_specific_search_response(self, response: Union[dict, str, list], name: str): + return verify_search_response_in_dict(response) + + def parse_all_entities_response(self, response: Union[dict, str, list]): + classifiers = response.get('classifiers', []) if type(response) is dict else [] + return [entity.get('id', '') for entity in classifiers] if type(classifiers) is list else [] + + +class ReputationAPI(EntityAPI): # works + name = 'reputation' + + def search_specific_id(self, specific_id: str): + return execute_command('demisto-api-get', + {'uri': '/reputation/export'}, + fail_on_error=False) + + def search_all(self): + return execute_command('demisto-api-get', + {'uri': '/reputation/export'}, + fail_on_error=False) + + def delete_specific_id(self, specific_id: str): + return execute_command('demisto-api-delete', + {'uri': f'/reputation/{specific_id}'}, + fail_on_error=False) + + def verify_specific_search_response(self, response: Union[dict, str, list], name: str): + return verify_search_response_in_list(response, name) + + +class LayoutAPI(EntityAPI): # works + name = 'layout' + + def search_specific_id(self, specific_id: str): + return execute_command('demisto-api-get', + {'uri': f'/layout/{specific_id}'}, + fail_on_error=False) + + def search_all(self): + return execute_command('demisto-api-get', + {'uri': '/layouts'}, + fail_on_error=False) + + def delete_specific_id(self, specific_id: str): + return execute_command('demisto-api-post', + {'uri': f'/layout/{specific_id}/remove', + 'body': {}}, + fail_on_error=False) + + def verify_specific_search_response(self, response: Union[dict, str, list], name: str): + return verify_search_response_in_dict(response) + + +class JobAPI(EntityAPI): + name = 'job' + + def search_specific_id(self, specific_id: str): + return execute_command('demisto-api-post', + {'uri': '/jobs/search', + 'body': {'page': 0, 'size': 1, 'query': f'name:"{specific_id}"'}}, + fail_on_error=False) + + def search_all(self): + return execute_command('demisto-api-post', + {'uri': '/jobs/search', + 'body': {'page': 0, 'size': 100}}, + fail_on_error=False) + + def delete_specific_id(self, specific_id: str): + return execute_command('demisto-api-delete', + {'uri': f'jobs/{specific_id}'}, + fail_on_error=False) + + def verify_specific_search_response(self, response: Union[dict, str], name: str): + job_params = {} + if type(response) is dict: + search_results = response.get('data') + if search_results: + job_params = search_results[0] + + if not job_params or not job_params.get("id"): + return False + return job_params.get("id") + + def parse_all_entities_response(self, response: Union[dict, str, list]): + return [entity.get('name', '') for entity in response.get('data', [])] if type(response) is dict else [] + + +class ListAPI(EntityAPI): + name = 'list' + + def search_specific_id(self, specific_id: str): + return execute_command('demisto-api-get', + {'uri': f'/lists/download/{specific_id}'}, + fail_on_error=False) + + def search_all(self): + return execute_command('demisto-api-get', + {'uri': '/lists/names'}, + fail_on_error=False) + + def delete_specific_id(self, specific_id: str): + return execute_command('demisto-api-post', + {'uri': '/lists/delete', + 'body': {'id': specific_id}}, + fail_on_error=False) + + def verify_specific_search_response(self, response: Union[dict, str], name: str): + if response: + return name + return False + + def parse_all_entities_response(self, response: Union[dict, str, list]): + return response + + +class InstalledPackAPI(EntityAPI): + name = 'pack' + always_excluded = ['ContentManagement', 'CleanUpContent'] + + def __init__(self, proxy_skip=True, verify=True): + if proxy_skip: + skip_proxy() + core_packs_response = requests.get(CORE_PACKS_LIST_URL, verify=verify) + self.always_excluded = json.loads(core_packs_response.text) + self.always_excluded + + def search_specific_id(self, specific_id: str): + return execute_command('demisto-api-get', + {'uri': f'/contentpacks/installed/{specific_id}'}, + fail_on_error=False) + + def search_all(self): + return execute_command('demisto-api-get', + {'uri': '/contentpacks/installed-expired'}, + fail_on_error=False) + + def delete_specific_id(self, specific_id: str): + return execute_command('demisto-api-delete', + {'uri': f'/contentpacks/installed/{specific_id}'}, + fail_on_error=False) + + def verify_specific_search_response(self, response: Union[dict, str], name: str): + return verify_search_response_in_dict(response) + + +def search_and_delete_existing_entity(name: str, entity_api: EntityAPI, dry_run: bool = True) -> bool: + """Searches the machine for previously configured entity_types with the given name. + + Args: + name (str): The name of the entity to update it's past configurations. + + Returns: + True if deleted, False otherwise. + """ + + status, res = entity_api.search_specific_id(specific_id=name) + + if not status: + demisto.debug(f'Could not find {entity_api.name} with id {name} - Response:\n{res}') + return False + + specific_id = entity_api.verify_specific_search_response(res.get('response'), name) + + if not specific_id: + return False + + if not dry_run: + status, res = entity_api.delete_specific_id(specific_id=specific_id) + else: + demisto.debug(f'DRY RUN - Not deleting {entity_api.name} with id {name}.') + status = True + res = True + + if not status: + demisto.debug(f'Could not delete {entity_api.name} with id {name} - Response:\n{res}') + return False + + return True + + +def search_for_all_entities(entity_api: EntityAPI) -> list: + """Search for all existing entities in xsoar. + + Args: + entity_api (EntityAPI): The entity api to preform api calls on. + + Returns: + list of entity ids. + """ + status, res = entity_api.search_all() + + if not status: + error_message = f'Search All {entity_api.name}s - {res}' + demisto.debug(error_message) + raise Exception(error_message) + + entity_ids = entity_api.parse_all_entities_response(res.get('response', {})) + + return entity_ids + + +def get_and_delete_entities(entity_api: EntityAPI, excluded_ids: list = [], included_ids: list = [], + dry_run: bool = True) -> Tuple[list, list, list]: + """Search and delete entities with provided EntityAPI. + + Args: + entity_api (EntityAPI): The api object to use for the get and delete api calls. + excluded_ids (list): List of ids to exclude from deletion. + included_ids (list): List of ids to include in deletion. + dry_run (bool): If true, will not really delete anything. + + Returns: + (list) successfully deleted ids, (list) not deleted ids + """ + demisto.debug(f'Starting handling {entity_api.name} entities.') + succesfully_deleted = [] + not_deleted = [] + extended_excluded_ids = excluded_ids.copy() + + if not included_ids and not excluded_ids: + return [], [], extended_excluded_ids + + if hasattr(entity_api, 'always_excluded'): + extended_excluded_ids += entity_api.always_excluded # type: ignore + + new_included_ids = [item for item in included_ids if item not in extended_excluded_ids] + demisto.debug(f'Included ids for {entity_api.name} after excluding excluded are {new_included_ids}') + + if included_ids: + for included_id in included_ids: + if included_id in new_included_ids: + if search_and_delete_existing_entity(included_id, entity_api=entity_api, dry_run=dry_run): + succesfully_deleted.append(included_id) + else: + not_deleted.append(included_id) + else: + not_deleted.append(included_id) + + else: + all_entities = search_for_all_entities(entity_api=entity_api) + if not all_entities: + return [], [], extended_excluded_ids + + for entity_id in all_entities: + if entity_id not in extended_excluded_ids: + if search_and_delete_existing_entity(entity_id, entity_api=entity_api, dry_run=dry_run): + succesfully_deleted.append(entity_id) + else: + demisto.debug(f'Did not find or could not delete {entity_api.name} with ' + f'id {entity_id} in xsoar.') + not_deleted.append(entity_id) + else: + not_deleted.append(entity_id) + + return succesfully_deleted, not_deleted, extended_excluded_ids + + +def get_deletion_status(excluded: list, included: list, deleted: list, undeleted: list) -> bool: + if excluded: + if undeleted == excluded: + return True + else: + for excluded_id in excluded: + if excluded_id in deleted: + return False + return True + + elif included: + if set(deleted) == set(included): + return True + # Nothing excluded + elif not undeleted: + return True + return False + + +def handle_content_enitity(entity_api: EntityAPI, + included_ids_dict: Optional[dict], + excluded_ids_dict: Optional[dict], + dry_run: bool) -> Tuple[bool, dict, dict]: + + excluded_ids = excluded_ids_dict.get(entity_api.name, []) if excluded_ids_dict else [] + included_ids = included_ids_dict.get(entity_api.name, []) if included_ids_dict else [] + + deleted_ids, undeleted_ids, new_excluded_ids = get_and_delete_entities(entity_api=entity_api, + excluded_ids=excluded_ids, + included_ids=included_ids, + dry_run=dry_run) + + deletion_status = get_deletion_status(excluded=new_excluded_ids, included=included_ids, + deleted=deleted_ids, undeleted=undeleted_ids) + + return deletion_status, {entity_api.name: deleted_ids}, {entity_api.name: undeleted_ids} + + +def handle_input_json(input_dict: Any) -> Any: + if type(input_dict) == str: + return json.loads(input_dict) + return input_dict + + +def get_and_delete_needed_ids(args: dict) -> CommandResults: + """Search and delete provided ids to delete. + + Args: + args[exclude_ids_dict] (dict): Dict content items ids to exclude. Will delete all the rest of the found ids. + args[include_ids_dict] (dict): Dict content items ids to include. Will delete all the ids specified. + args[dry_run] (str(bool)): If True, will only collect items for deletion and will not delete them. + + Remark: + exclude_ids_dict, include_ids_dict are assumed to be in the {'entity_type': [entity_ids]} format. + (e.g. {'job': ['job1', 'job2'], 'playbook': ['playbook1', 'playbook2']}) + + Raise: + ValueError if both exclude_ids and include_ids are specified. + + Returns: + CommandResults with the following outputs: + successfully_deleted: list of content ids gathered for deletion. + not_deleted: list of content ids gathered not to delete. + status: Deletion status (Failed/Completed/Dry run, nothing really deleted.) + """ + dry_run = argToBoolean(args.get('dry_run', 'true')) + include_ids = handle_input_json(args.get('include_ids_dict')) + exclude_ids = handle_input_json(args.get('exclude_ids_dict')) + skip_proxy = argToBoolean(args.get('skip_proxy', 'false')) + verify_cert = argToBoolean(args.get('verify_cert', 'true')) + + entities_to_delete = [InstalledPackAPI(proxy_skip=skip_proxy, verify=verify_cert), IntegrationAPI(), ScriptAPI(), + PlaybookAPI(), IncidentFieldAPI(), + PreProcessingRuleAPI(), WidgetAPI(), DashboardAPI(), ReportAPI(), JobAPI(), ListAPI(), + IncidentTypeAPI(), ClassifierAPI(), ReputationAPI(), LayoutAPI()] + + all_deleted: dict = dict() + all_not_deleted: dict = dict() + all_deletion_statuses: list = [] + for entity in entities_to_delete: + entity_deletion_status, deleted, undeleted = handle_content_enitity(entity, include_ids, exclude_ids, dry_run) + all_deleted.update(deleted) + all_not_deleted.update(undeleted) + all_deletion_statuses.append(entity_deletion_status) + + deletion_status = 'Failed' + if dry_run: + deletion_status = 'Dry run, nothing really deleted.' + else: + if all(all_deletion_statuses): + deletion_status = 'Completed' + + return CommandResults( + outputs_prefix='ConfigurationSetup.Deletion', + outputs_key_field='name', + outputs={ + # Only show keys with values. + 'successfully_deleted': {key: value for key, value in all_deleted.items() if value}, + 'not_deleted': {key: value for key, value in all_not_deleted.items() if value}, + 'status': deletion_status, + }, + ) + + +def main(): # pragma: no cover + try: + return_results(get_and_delete_needed_ids(demisto.args())) + + except Exception as e: + return_error(f'Error occurred while deleting contents.\n{e}' + f'\n{traceback.format_exc()}') + + +if __name__ in ('__main__', '__builtin__', 'builtins'): # pragma: no cover + main() diff --git a/Packs/ContentManagement/Scripts/DeleteContent/DeleteContent.yml b/Packs/ContentManagement/Scripts/DeleteContent/DeleteContent.yml new file mode 100644 index 000000000000..b95a2fa637dc --- /dev/null +++ b/Packs/ContentManagement/Scripts/DeleteContent/DeleteContent.yml @@ -0,0 +1,75 @@ +args: +- default: false + description: The content items ids to delete, in a JSON format. + isArray: false + name: include_ids_dict + required: false + secret: false +- default: false + description: The content items IDs to preserve, in a JSON format. + isArray: false + name: exclude_ids_dict + required: false + secret: false +- auto: PREDEFINED + default: false + description: If set to true, the flow will work as usuall except that no content items will be deleted from the system. + isArray: false + name: dry_run + required: true + secret: false + predefined: + - 'true' + - 'false' +- auto: PREDEFINED + default: false + defaultValue: 'true' + description: If true, verify certificates when accessing github. + isArray: false + name: verify_cert + required: true + secret: false + predefined: + - 'true' + - 'false' +- auto: PREDEFINED + default: false + defaultValue: 'false' + description: If true, skip system proxy settings. + isArray: false + name: skip_proxy + required: true + secret: false + predefined: + - 'true' + - 'false' +commonfields: + id: DeleteContent + version: -1 +enabled: false +name: DeleteContent +comment: Delete content to keep XSOAR tidy. +outputs: +- contextPath: ConfigurationSetup.Deletion.successfully_deleted + description: Deleted ids + type: String +- contextPath: ConfigurationSetup.Deletion.not_deleted + description: Not deleted ids + type: String +- contextPath: ConfigurationSetup.Deletion.status + description: Deletion status + type: String +script: '-' +system: false +tags: +- configuration +- Content Management +timeout: 3600 +type: python +subtype: python3 +dockerimage: demisto/python3:3.10.9.40422 +tests: +- No tests (auto formatted) +fromversion: 6.0.0 +marketplaces: +- xsoar diff --git a/Packs/ContentManagement/Scripts/DeleteContent/DeleteContent_test.py b/Packs/ContentManagement/Scripts/DeleteContent/DeleteContent_test.py new file mode 100644 index 000000000000..98759c4c814c --- /dev/null +++ b/Packs/ContentManagement/Scripts/DeleteContent/DeleteContent_test.py @@ -0,0 +1,621 @@ +import pytest + +from DeleteContent import get_and_delete_needed_ids, CORE_PACKS_LIST_URL +from abc import ABC, abstractmethod +from typing import Tuple, Any + + +XSOAR_IDS_FULL_STATE = { + 'pack': ['installed_pack_id1', 'installed_pack_id2', 'Base'], + 'list': ['list1', 'list2'], + 'job': ['job1', 'job2'], + 'script': ['script1', 'script2', 'CommonUserServer'], + 'playbook': ['playbook1', 'playbook2'], + 'integration': ['integration1', 'integration2'], + 'incidentfield': ['incidentfield1', 'incidentfield2'], + 'pre-process-rule': ['pre-process-rule1', 'pre-process-rule2'], + 'widget': ['widget1', 'widget2'], + 'dashboard': ['dashboard1', 'dashboard2'], + 'report': ['report1', 'report2'], + 'incidenttype': ['incidenttype1', 'incidenttype2'], + 'classifier': ['classifier1', 'classifier2'], + 'reputation': ['reputation1', 'reputation2'], + 'layout': ['layout1', 'layout2'] +} + + +class MockEntityResponses(ABC): + entity_name = '' + + def __init__(self, xsoar_state): + self.xsoar_state_ids = xsoar_state.get(self.entity_name) + + @abstractmethod + def search_response(self, command_name, command_args) -> Tuple[bool, Any]: + pass + + @abstractmethod + def delete_response(self, command_name, command_args) -> Tuple[bool, Any]: + pass + + +class MockJobResponses(MockEntityResponses): + entity_name = 'job' + + def search_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri == '/jobs/search': + if command_args.get('body', {}).get('size') == 1: + job_name = command_args.get('body', {}).get('query').split('name:"')[1].split('"')[0] + if job_name in self.xsoar_state_ids: + # if search and found + return True, {'data': [{'name': job_name, 'id': job_name}]} + + # if search and not found + return False, 'Id not found' + + # If search all return all + return True, {'data': [{'name': job_name, 'id': job_name} for job_name in self.xsoar_state_ids]} + return False, False + + def delete_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('jobs/') and command_name == 'demisto-api-delete': + job_name = command_uri.split('jobs/')[1] + if job_name in self.xsoar_state_ids: + return True, {'data': [{'name': job_name, 'id': job_name}]} + return False, 'Id not found' + return False, False + + +class MockListResponses(MockEntityResponses): + entity_name = 'list' + + def search_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('/lists/download/'): + list_name = command_uri.split('/lists/download/')[1] + if list_name in self.xsoar_state_ids: + return True, list_name + return False, 'Id not found' + + if command_uri == '/lists/names': + return True, self.xsoar_state_ids + + return False, False + + def delete_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri == '/lists/delete': + list_name = command_args.get('body').get('id') + if list_name in self.xsoar_state_ids: + return True, list_name + return False, 'Id not found' + return False, False + + +class MockPackResponses(MockEntityResponses): + entity_name = 'pack' + + def search_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_name == 'demisto-api-get' and command_uri.startswith('/contentpacks/installed/'): + pack_name = command_uri.split('/contentpacks/installed/')[1] + return (True, {'id': pack_name}) if pack_name in self.xsoar_state_ids else (False, 'Id not found') + + if command_name == 'demisto-api-get' and command_uri.startswith('/contentpacks/installed-expired'): + return True, [{'id': pack_name} for pack_name in self.xsoar_state_ids] + + return False, False + + def delete_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_name == 'demisto-api-delete' and command_uri.startswith('/contentpacks/installed/'): + pack_name = command_uri.split('/contentpacks/installed/')[1] + return (True, {'id': pack_name}) if pack_name in self.xsoar_state_ids else (False, 'Id not found') + return False, False + + +class MockScriptResponses(MockEntityResponses): + entity_name = 'script' + + def search_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri == '/automation/search': + if command_args.get('body', {}).get('size') == 1: + script_name = command_args.get('body', {}).get('query').split('name:"')[1].split('"')[0] + if script_name in self.xsoar_state_ids: + # if search and found + return True, {'scripts': [{'id': script_name}]} + + # if search and not found + return False, 'Id not found' + + # If search all return all + return True, {'scripts': [{'id': script_name} for script_name in self.xsoar_state_ids]} + return False, False + + def delete_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri == '/automation/delete' and command_name == 'demisto-api-post': + script_name = command_args.get('body', {}).get('script', {}).get('id', '') + if script_name in self.xsoar_state_ids: + return True, {'scripts': [{'id': script_name}]} + return False, 'Id not found' + return False, False + + +class MockPlaybookResponses(MockEntityResponses): + entity_name = 'playbook' + + def search_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_name == 'demisto-api-get' and command_uri.startswith('/playbook/'): + name = command_uri.split('/playbook/')[1] + if name in self.xsoar_state_ids: + return True, {'id': name} + return False, 'Id not found' + + if command_name == 'demisto-api-post' and command_uri == '/playbook/search': + return True, {'playbooks': [{'id': name} for name in self.xsoar_state_ids]} + + return False, False + + def delete_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri == '/playbook/delete': + name = command_args.get('body', {}).get('id') + if name in self.xsoar_state_ids: + return True, {'id': name} + return False, 'Id not found' + return False, False + + +class MockIncidentFieldResponses(MockEntityResponses): + entity_name = 'incidentfield' + + def search_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri == '/incidentfields' and command_name == 'demisto-api-get': + return True, [{'id': name} for name in self.xsoar_state_ids] + return False, False + + def delete_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('/incidentfield/') and command_name == 'demisto-api-delete': + name = command_uri.split('/incidentfield/')[1] + if name in self.xsoar_state_ids: + return True, None + return False, 'Id not found' + return False, False + + +class MockIntegrationResponses(MockEntityResponses): + entity_name = 'integration' + + def search_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri == '/settings/integration/search': + if command_args.get('body', {}).get('query'): + name = command_args.get('body', {}).get('query').split('name:"')[1].split('"')[0] + if name in self.xsoar_state_ids: + # if search and found + return True, {'configurations': [{'id': name}]} + + # if search and not found + return False, 'Id not found' + + # If search all return all + return True, {'configurations': [{'id': name} for name in self.xsoar_state_ids]} + return False, False + + def delete_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri == '/settings/integration-conf/delete' and command_name == 'demisto-api-post': + name = command_args.get('body', {}).get('id') + if name in self.xsoar_state_ids: + return True, {'configurations': [{'id': name}]} + return False, 'Id not found' + return False, False + + +class MockPreprocessRuleResponses(MockEntityResponses): + entity_name = 'pre-process-rule' + + def search_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri == '/preprocess/rules' and command_name == 'demisto-api-get': + return True, [{'id': name} for name in self.xsoar_state_ids] + return False, False + + def delete_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('/preprocess/rule/') and command_name == 'demisto-api-delete': + name = command_uri.split('/preprocess/rule/')[1] + if name in self.xsoar_state_ids: + return True, None + return False, 'Id not found' + return False, False + + +class MockWidgetResponses(MockEntityResponses): + entity_name = 'widget' + + def search_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('/widgets') and command_name == 'demisto-api-get': + if command_uri.startswith('/widgets/'): + name = command_uri.split('/widgets/')[1] + if name in self.xsoar_state_ids: + return True, {'id': name} + return True, 'Id not found' + return True, [{'id': name} for name in self.xsoar_state_ids] + return False, False + + def delete_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('/widgets/') and command_name == 'demisto-api-delete': + name = command_uri.split('/widgets/')[1] + if name in self.xsoar_state_ids: + return True, None + return False, 'Id not found' + return False, False + + +class MockDashboardResponses(MockEntityResponses): + entity_name = 'dashboard' + + def search_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('/dashboards') and command_name == 'demisto-api-get': + if command_uri.startswith('/dashboards/'): + name = command_uri.split('/dashboards/')[1] + if name in self.xsoar_state_ids: + return True, {'id': name} + return True, 'Id not found' + return True, [{'id': name} for name in self.xsoar_state_ids] + return False, False + + def delete_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('/dashboards/') and command_name == 'demisto-api-delete': + name = command_uri.split('/dashboards/')[1] + if name in self.xsoar_state_ids: + return True, None + return False, 'Id not found' + return False, False + + +class MockReportResponses(MockEntityResponses): + entity_name = 'report' + + def search_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('/reports') and command_name == 'demisto-api-get': + if command_uri.startswith('/reports/'): + name = command_uri.split('/reports/')[1] + if name in self.xsoar_state_ids: + return True, {'id': name} + return True, 'Id not found' + return True, [{'id': name} for name in self.xsoar_state_ids] + return False, False + + def delete_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('/report/') and command_name == 'demisto-api-delete': + name = command_uri.split('/report/')[1] + if name in self.xsoar_state_ids: + return True, None + return False, 'Id not found' + return False, False + + +class MockIncidentTypeResponses(MockEntityResponses): + entity_name = 'incidenttype' + + def search_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('/incidenttypes/export') and command_name == 'demisto-api-get': + return True, [{'id': name} for name in self.xsoar_state_ids] + return False, False + + def delete_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('/incidenttype/delete') and command_name == 'demisto-api-post': + name = command_args.get('body', {}).get('id') + if name in self.xsoar_state_ids: + return True, None + return False, 'Id not found' + return False, False + + +class MockClassifierResponses(MockEntityResponses): + entity_name = 'classifier' + + def search_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri == '/classifier/search' and command_name == 'demisto-api-post': + return True, {'classifiers': [{'id': name} for name in self.xsoar_state_ids]} + if command_uri.startswith('/classifier/') and command_name == 'demisto-api-get': + name = command_uri.split('/classifier/')[1] + if name in self.xsoar_state_ids: + return True, {'id': name} + return False, 'Id not found' + return False, False + + def delete_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('/classifier/') and command_name == 'demisto-api-delete': + name = command_uri.split('/classifier/')[1] + if name in self.xsoar_state_ids: + return True, None + return False, 'Id not found' + return False, False + + +class MockReputationResponses(MockEntityResponses): + entity_name = 'reputation' + + def search_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('/reputation/export') and command_name == 'demisto-api-get': + return True, [{'id': name} for name in self.xsoar_state_ids] + return False, False + + def delete_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('/reputation/') and command_name == 'demisto-api-delete': + name = command_uri.split('/reputation/')[1] + if name in self.xsoar_state_ids: + return True, None + return False, 'Id not found' + return False, False + + +class MockLayoutResponses(MockEntityResponses): + entity_name = 'layout' + + def search_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('/layout/') and command_name == 'demisto-api-get': + name = command_uri.split('/layout/')[1] + if name in self.xsoar_state_ids: + return True, {'id': name} + return False, 'Id not Found' + if command_uri == '/layouts' and command_name == 'demisto-api-get': + return True, [{'id': name} for name in self.xsoar_state_ids] + return False, False + + def delete_response(self, command_name, command_args): + command_uri = command_args.get('uri') + if command_uri.startswith('/layout/') and command_uri.endswith('/remove') and command_name == 'demisto-api-post': + name = command_uri.split('/layout/')[1] + name = name.split('/remove')[0] + if name in self.xsoar_state_ids: + return True, None + return False, 'Id not found' + return False, False + + +def mock_demisto_responses(command_name, command_args, xsoar_ids_state): + """Mock function for demisto responses to api calls according to xsoar ids state. + + Args: + command_name (str): The command name sent to the executeCommand demisto function. + command_args (dict): The command args sent to the executeCommand demisto function. + xsoar_ids_state (dict): A representation for the content ids in an xsoar instance. + + Returns: + status, demisto response + """ + mocked_entities = [MockJobResponses(xsoar_ids_state), MockPackResponses(xsoar_ids_state), + MockListResponses(xsoar_ids_state), MockScriptResponses(xsoar_ids_state), + MockPlaybookResponses(xsoar_ids_state), MockIntegrationResponses(xsoar_ids_state), + MockIncidentFieldResponses(xsoar_ids_state), MockPreprocessRuleResponses(xsoar_ids_state), + MockWidgetResponses(xsoar_ids_state), MockDashboardResponses(xsoar_ids_state), + MockReportResponses(xsoar_ids_state), MockIncidentTypeResponses(xsoar_ids_state), + MockClassifierResponses(xsoar_ids_state), MockReputationResponses(xsoar_ids_state), + MockLayoutResponses(xsoar_ids_state)] + for mocked_entity in mocked_entities: + status, response = mocked_entity.search_response(command_name, command_args) + if (status, response) != (False, False): + return status, response + + status, response = mocked_entity.delete_response(command_name, command_args) + if (status, response) != (False, False): + return status, response + + return False, 'Command Not Mocked.' + + +@pytest.mark.parametrize('args, xsoar_ids_state, expected_outputs', [ + pytest.param( + {'dry_run': 'false'}, XSOAR_IDS_FULL_STATE, { + 'not_deleted': {}, + 'successfully_deleted': {}, + 'status': 'Completed'}, id='delete nothing'), + pytest.param( + {'dry_run': 'false', 'include_ids_dict': {'job': ['job1'], + 'pack': ['installed_pack_id1'], + 'list': ['list1'], + 'script': ['script1'], + 'playbook': ['playbook1'], + 'integration': ['integration1'], + 'incidentfield': ['incidentfield1'], + 'pre-process-rule': ['pre-process-rule1'], + 'widget': ['widget1'], + 'dashboard': ['dashboard1'], + 'report': ['report1'], + 'incidenttype': ['incidenttype1'], + 'classifier': ['classifier1'], + 'reputation': ['reputation1'], + 'layout': ['layout1']}, + 'delete_unspecified': 'false'}, XSOAR_IDS_FULL_STATE, { + 'not_deleted': {}, + 'successfully_deleted': {'job': ['job1'], 'list': ['list1'], 'pack': ['installed_pack_id1'], + 'script': ['script1'], 'playbook': ['playbook1'], 'integration': ['integration1'], + 'incidentfield': ['incidentfield1'], 'pre-process-rule': ['pre-process-rule1'], + 'widget': ['widget1'], 'dashboard': ['dashboard1'], 'report': ['report1'], + 'incidenttype': ['incidenttype1'], 'classifier': ['classifier1'], + 'reputation': ['reputation1'], 'layout': ['layout1']}, + 'status': 'Completed'}, id='delete only included ids'), + pytest.param( + {'dry_run': 'false', 'exclude_ids_dict': {'job': ['job1'], + 'pack': ['installed_pack_id1'], + 'list': ['list1'], + 'script': ['script1'], + 'playbook': ['playbook1'], + 'integration': ['integration1'], + 'incidentfield': ['incidentfield1'], + 'pre-process-rule': ['pre-process-rule1'], + 'widget': ['widget1'], + 'dashboard': ['dashboard1'], + 'report': ['report1'], + 'incidenttype': ['incidenttype1'], + 'classifier': ['classifier1'], + 'reputation': ['reputation1'], + 'layout': ['layout1']}}, XSOAR_IDS_FULL_STATE, { + 'not_deleted': {'pack': ['installed_pack_id1', 'Base'], 'job': ['job1'], 'list': ['list1'], + 'script': ['script1', 'CommonUserServer'], + 'playbook': ['playbook1'], 'integration': ['integration1'], + 'incidentfield': ['incidentfield1'], 'pre-process-rule': ['pre-process-rule1'], + 'widget': ['widget1'], 'dashboard': ['dashboard1'], 'report': ['report1'], + 'incidenttype': ['incidenttype1'], 'classifier': ['classifier1'], + 'reputation': ['reputation1'], 'layout': ['layout1']}, + 'successfully_deleted': { # packs can only be deleted when included. + 'job': ['job2'], 'list': ['list2'], 'playbook': ['playbook2'], 'script': ['script2'], + 'integration': ['integration2'], 'incidentfield': ['incidentfield2'], + 'pre-process-rule': ['pre-process-rule2'], 'widget': ['widget2'], + 'dashboard': ['dashboard2'], 'report': ['report2'], 'incidenttype': ['incidenttype2'], + 'classifier': ['classifier2'], 'reputation': ['reputation2'], 'layout': ['layout2'], + 'pack': ['installed_pack_id2'], + }, + 'status': 'Completed'}, id='dont delete excluded ids'), + pytest.param( + {'dry_run': 'false', 'exclude_ids_dict': {'job': ['job3'], + 'pack': ['installed_pack3'], + 'list': ['list3'], + 'script': ['script3'], + 'playbook': ['playbook3'], + 'integration': ['integration3'], + 'incidentfield': ['incidentfield3'], + 'pre-process-rule': ['pre-process-rule3'], + 'widget': ['widget3'], + 'dashboard': ['dashboard3'], + 'report': ['report3'], + 'incidenttype': ['incidenttype3'], + 'classifier': ['classifier3'], + 'reputation': ['reputation3'], + 'layout': ['layout3']}}, XSOAR_IDS_FULL_STATE, { + 'not_deleted': {'pack': ['Base'], 'script': ['CommonUserServer']}, + 'successfully_deleted': {'job': ['job1', 'job2'], 'list': ['list1', 'list2'], + 'script': ['script1', 'script2'], 'playbook': ['playbook1', 'playbook2'], + 'integration': ['integration1', 'integration2'], + 'incidentfield': ['incidentfield1', 'incidentfield2'], + 'pre-process-rule': ['pre-process-rule1', 'pre-process-rule2'], + 'widget': ['widget1', 'widget2'], 'dashboard': ['dashboard1', 'dashboard2'], + 'report': ['report1', 'report2'], + 'incidenttype': ['incidenttype1', 'incidenttype2'], + 'classifier': ['classifier1', 'classifier2'], + 'reputation': ['reputation1', 'reputation2'], + 'layout': ['layout1', 'layout2'], + 'pack': ['installed_pack_id1', 'installed_pack_id2']}, + 'status': 'Completed'}, id='exclude unfound id'), + pytest.param( + {'dry_run': 'false', 'include_ids_dict': {'job': ['job3'], + 'pack': ['installed_pack3'], + 'list': ['list3'], + 'script': ['script3'], + 'playbook': ['playbook3'], + 'integration': ['integration3'], + 'incidentfield': ['incidentfield3'], + 'pre-process-rule': ['pre-process-rule3'], + 'widget': ['widget3'], + 'dashboard': ['dashboard3'], + 'report': ['report3'], + 'incidenttype': ['incidenttype3'], + 'classifier': ['classifier3'], + 'reputation': ['reputation3'], + 'layout': ['layout3']}}, XSOAR_IDS_FULL_STATE, { + 'not_deleted': {'job': ['job3'], 'pack': ['installed_pack3'], 'list': ['list3'], + 'script': ['script3'], 'playbook': ['playbook3'], 'integration': ['integration3'], + 'incidentfield': ['incidentfield3'], 'pre-process-rule': ['pre-process-rule3'], + 'widget': ['widget3'], 'dashboard': ['dashboard3'], 'report': ['report3'], + 'incidenttype': ['incidenttype3'], 'classifier': ['classifier3'], + 'reputation': ['reputation3'], + 'layout': ['layout3']}, + 'successfully_deleted': {}, + 'status': 'Failed'}, id='include unfound id'), + pytest.param( + {'dry_run': 'false', 'include_ids_dict': {'script': ['CommonUserServer'], + 'pack': ['Base']}}, XSOAR_IDS_FULL_STATE, { + 'not_deleted': {'pack': ['Base'], 'script': ['CommonUserServer']}, + 'successfully_deleted': {}, + 'status': 'Completed'}, id='include always excluded id'), +]) +def test_get_and_delete_needed_ids(requests_mock, mocker, args, xsoar_ids_state, expected_outputs): + """ + Given: + Xsoar ids state. + Include_ids and exclude_ids lists. + + When: + Running get_and_delete_needed_ids with dry_run set to false. + + Then: + Assert deleted id lists are correct. + """ + requests_mock.get(CORE_PACKS_LIST_URL, text='[\n "Base",\n "rasterize",\n "DemistoRESTAPI"\n]') + + def execute_command_mock(command_name, command_args, fail_on_error=False): + status, response = mock_demisto_responses(command_name, command_args, xsoar_ids_state) + return status, {'response': response} + + mocker.patch("DeleteContent.execute_command", side_effect=execute_command_mock) + + result = get_and_delete_needed_ids(args) + assert result.outputs.get('not_deleted') == expected_outputs.get('not_deleted') + assert result.outputs.get('successfully_deleted') == expected_outputs.get('successfully_deleted') + assert result.outputs.get('status') == expected_outputs.get('status') + + +@pytest.mark.parametrize('args, xsoar_ids_state, expected_outputs, call_count', [ + pytest.param( + {'dry_run': 'true', 'include_ids_dict': {'job': ['job1', 'job2']}}, + XSOAR_IDS_FULL_STATE, { + 'not_deleted': {}, + 'successfully_deleted': {'job': ['job1', 'job2']}, + 'status': 'Dry run, nothing really deleted.'}, 2, id='dry run, delete.'), + pytest.param( + {'dry_run': 'false', 'include_ids_dict': {'job': ['job1', 'job2']}}, + XSOAR_IDS_FULL_STATE, { + 'not_deleted': {}, + 'successfully_deleted': {'job': ['job1', 'job2']}, + 'status': 'Completed'}, 4, id='not dry run, delete.') +]) +def test_dry_run_delete(requests_mock, mocker, args, xsoar_ids_state, expected_outputs, call_count): + """ + Given: + Xsoar ids state. + dry_run flag. + + When: + Running get_and_delete_needed_ids with dry_run toggled. + + Then: + Assert deleted id lists are correct. + Assert call count to executeCommand API does not include calls for actual deletion. + """ + requests_mock.get(CORE_PACKS_LIST_URL, text='[\n "Base",\n "rasterize",\n "DemistoRESTAPI"\n]') + + def execute_command_mock(command_name, command_args, fail_on_error=False): + status, response = mock_demisto_responses(command_name, command_args, xsoar_ids_state) + return status, {'response': response} + + execute_mock = mocker.patch("DeleteContent.execute_command", side_effect=execute_command_mock) + + result = get_and_delete_needed_ids(args) + assert result.outputs.get('not_deleted') == expected_outputs.get('not_deleted') + assert result.outputs.get('successfully_deleted') == expected_outputs.get('successfully_deleted') + assert result.outputs.get('status') == expected_outputs.get('status') + assert execute_mock.call_count == call_count diff --git a/Packs/ContentManagement/Scripts/DeleteContent/README.md b/Packs/ContentManagement/Scripts/DeleteContent/README.md new file mode 100644 index 000000000000..c8cd4af74ae7 --- /dev/null +++ b/Packs/ContentManagement/Scripts/DeleteContent/README.md @@ -0,0 +1,30 @@ +Delete content to keep XSOAR tidy. + +## Script Data +--- + +| **Name** | **Description** | +| --- | --- | +| Script Type | python3 | +| Tags | configuration, Content Management | +| Cortex XSOAR Version | 6.0.0 | + +## Inputs +--- + +| **Argument Name** | **Description** | +| --- | --- | +| include_ids_dict | The content items ids to delete, in a JSON format. | +| exclude_ids_dict | The content items IDs to preserve, in a JSON format. | +| dry_run | If set to true, the flow will work as usuall except that no content items will be deleted from the system. | +| verify_cert | If true, verify certificates when accessing github. | +| skip_proxy | If true, skip system proxy settings. | + +## Outputs +--- + +| **Path** | **Description** | **Type** | +| --- | --- | --- | +| ConfigurationSetup.Deletion.successfully_deleted | Deleted ids | String | +| ConfigurationSetup.Deletion.not_deleted | Not deleted ids | String | +| ConfigurationSetup.Deletion.status | Deletion status | String | diff --git a/Packs/ContentManagement/pack_metadata.json b/Packs/ContentManagement/pack_metadata.json index 64ef5a699dfc..a97bbce9411f 100644 --- a/Packs/ContentManagement/pack_metadata.json +++ b/Packs/ContentManagement/pack_metadata.json @@ -2,7 +2,7 @@ "name": "XSOAR CI/CD", "description": "This pack enables you to orchestrate your XSOAR system configuration.", "support": "xsoar", - "currentVersion": "1.1.7", + "currentVersion": "1.2.0", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "",