From dfea2a0811d846d1a2aca8ff88c1d300407e2bef Mon Sep 17 00:00:00 2001 From: noydavidi Date: Mon, 12 Jan 2026 10:03:10 +0200 Subject: [PATCH 01/27] added the commands --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 254 ++++++++ .../Integrations/CortexXDRIR/CortexXDRIR.yml | 551 ++++++++++++++++++ 2 files changed, 805 insertions(+) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 1c82f3aebe5e..26eb3ee8e9c1 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -513,6 +513,66 @@ def update_alerts_in_xdr_request(self, alerts_ids, severity, status, comment) -> raise DemistoException(f"Parse Error. Response not in format, can't find reply key. The response {response}.") return response["reply"]["alerts_ids"] + def get_biocs(self, request_data: dict): + reply = self._http_request( + method="POST", + url_suffix="/bioc/get/", + json_data={"request_data": request_data}, + headers=self.headers, + timeout=self.timeout, + ) + return reply.get("reply", {}) + + def insert_biocs(self, request_data: dict): + reply = self._http_request( + method="POST", + url_suffix="/bioc/insert/", + json_data={"request_data": request_data}, + headers=self.headers, + timeout=self.timeout, + ) + return reply.get("reply", {}) + + def delete_biocs(self, request_data: dict): + reply = self._http_request( + method="POST", + url_suffix="/bioc/delete/", + json_data={"request_data": request_data}, + headers=self.headers, + timeout=self.timeout, + ) + return reply.get("reply", {}) + + def get_correlation_rules(self, request_data: dict): + reply = self._http_request( + method="POST", + url_suffix="/correlations/get/", + json_data={"request_data": request_data}, + headers=self.headers, + timeout=self.timeout, + ) + return reply.get("reply", {}) + + def insert_correlation_rules(self, request_data: dict): + reply = self._http_request( + method="POST", + url_suffix="/correlations/insert/", + json_data={"request_data": request_data}, + headers=self.headers, + timeout=self.timeout, + ) + return reply.get("reply", {}) + + def delete_correlation_rules(self, request_data: dict): + reply = self._http_request( + method="POST", + url_suffix="/correlations/delete/", + json_data={"request_data": request_data}, + headers=self.headers, + timeout=self.timeout, + ) + return reply.get("reply", {}) + def extract_paths_and_names(paths: list) -> tuple: """ @@ -1416,6 +1476,176 @@ def update_alerts_in_xdr_command(client: Client, args: Dict) -> CommandResults: return CommandResults(readable_output="Alerts with IDs {} have been updated successfully.".format(",".join(array_of_all_ids))) +def bioc_list_command(client: Client, args: Dict) -> CommandResults: + """ + Returns a list of BIOCs. + """ + filters = assign_params( + name=args.get("name"), + severity=args.get("severity"), + type=args.get("type"), + is_xql=argToBoolean(args.get("is_xql")) if args.get("is_xql") else None, + comment=args.get("comment"), + status=args.get("status"), + indicator=argToList(args.get("indicator")), + mitre_technique_id_and_name=argToList(args.get("mitre_technique_id_and_name")), + mitre_tactic_id_and_name=argToList(args.get("mitre_tactic_id_and_name")), + ) + request_data = assign_params( + filters=filters, + extended_view=argToBoolean(args.get("extra_data", False)), + search_from=arg_to_number(args.get("page")), + search_to=arg_to_number(args.get("limit")), + ) + reply = client.get_biocs(request_data) + biocs = reply.get("objects", []) + readable_output = tableToMarkdown("BIOCs", biocs, headers=["name", "type", "severity", "status"], removeNull=True) + return CommandResults( + readable_output=readable_output, + outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.BIOC", + outputs_key_field="name", + outputs=biocs, + raw_response=reply, + ) + + +def bioc_create_command(client: Client, args: Dict) -> CommandResults: + """ + Creates a new BIOC. + """ + bioc_data = assign_params( + rule_id=args.get("rule_id"), + name=args.get("name"), + severity=args.get("severity"), + type=args.get("type"), + is_xql=argToBoolean(args.get("is_xql", False)), + comment=args.get("comment"), + status=args.get("status"), + indicator=argToList(args.get("indicator")), + mitre_technique_id_and_name=argToList(args.get("mitre_technique_id_and_name")), + mitre_tactic_id_and_name=argToList(args.get("mitre_tactic_id_and_name")), + ) + client.insert_biocs(bioc_data) + return bioc_list_command(client, {"name": args.get("name")}) + + +def bioc_update_command(client: Client, args: Dict) -> CommandResults: + """ + Updates an existing BIOC. + """ + return bioc_create_command(client, args) + + +def bioc_delete_command(client: Client, args: Dict) -> str: + """ + Deletes a BIOC. + """ + filters = assign_params( + name=args.get("name"), + severity=args.get("severity"), + type=args.get("type"), + is_xql=argToBoolean(args.get("is_xql")) if args.get("is_xql") else None, + comment=args.get("comment"), + indicator=argToList(args.get("indicator")), + mitre_technique_id_and_name=argToList(args.get("mitre_technique_id_and_name")), + mitre_tactic_id_and_name=argToList(args.get("mitre_tactic_id_and_name")), + ) + client.delete_biocs(filters) + return "BIOC object deleted successfully" + + +def correlation_rule_list_command(client: Client, args: Dict) -> CommandResults: + """ + Returns a list of correlation rules. + """ + filters = assign_params( + name=args.get("name"), + severity=args.get("severity"), + xql_query=args.get("xql_query"), + is_xql=argToBoolean(args.get("is_xql")) if args.get("is_xql") else None, + dataset=args.get("dataset"), + alert_name=args.get("alert_name"), + alert_category=args.get("alert_category"), + alert_fields=argToList(args.get("alert_fields")), + alet_domain=args.get("alert_domain"), + ) + if filter_json := args.get("filter_json"): + filters.update(json.loads(filter_json)) + + request_data = assign_params( + filters=filters, + extended_view=argToBoolean(args.get("extra_data", False)), + search_from=arg_to_number(args.get("page")), + search_to=arg_to_number(args.get("limit")), + ) + reply = client.get_correlation_rules(request_data) + rules = reply.get("objects", []) + readable_output = tableToMarkdown( + "Correlation Rules", rules, headers=["id", "name", "description", "is_enabled"], removeNull=True + ) + return CommandResults( + readable_output=readable_output, + outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CorrelationRule", + outputs_key_field="id", + outputs=rules, + raw_response=reply, + ) + + +def correlation_rule_create_command(client: Client, args: Dict) -> CommandResults: + """ + Creates a new correlation rule. + """ + rule_data = assign_params( + rule_id=args.get("rule_id"), + name=args.get("name"), + severity=args.get("severity"), + xql_query=args.get("xql_query"), + is_enabled=argToBoolean(args.get("is_enabled")) if args.get("is_enabled") else None, + description=args.get("description"), + alert_name=args.get("alert_name"), + alert_category=args.get("alert_category"), + alert_description=args.get("alert_description"), + alert_fields=args.get("alert_fields"), + execution_mode=args.get("execution_mode"), + search_window=args.get("search_window"), + schedule=args.get("schedule"), + schedule_linux=args.get("schedule_linux"), + timezone=args.get("timezone"), + suppression_enabled=argToBoolean(args.get("suppression_enabled")) if args.get("suppression_enabled") else None, + suppression_duration=args.get("suppression_duration"), + suppression_fields=args.get("suppression_fields"), + dataset=args.get("dataset"), + user_defined_severity=args.get("user_defined_severity"), + user_defined_category=args.get("user_defined_category"), + investigation_query_link=args.get("investigation_query_link"), + drilldown_query_timeframe=args.get("drilldown_query_timeframe"), + mapping_strategy=args.get("mapping_strategy"), + ) + if mitre_defs_json := args.get("mitre_defs_json"): + rule_data["mitre_defs"] = json.loads(mitre_defs_json) + + client.insert_correlation_rules(rule_data) + # Run get to return the created rule + return correlation_rule_list_command(client, {"name": args.get("name")}) + + +def correlation_rule_update_command(client: Client, args: Dict) -> CommandResults: + """ + Updates an existing correlation rule. + """ + return correlation_rule_create_command(client, args) + + +def correlation_rule_delete_command(client: Client, args: Dict) -> str: + """ + Deletes correlation rules. + """ + rule_ids = argToList(args.get("rule_id")) + client.delete_correlation_rules({"rule_id_list": rule_ids}) + return "Correlation rule deleted successfully" + + def main(): # pragma: no cover """ Executes an integration command @@ -1886,6 +2116,30 @@ def main(): # pragma: no cover elif command == "xdr-update-alert": return_results(update_alerts_in_xdr_command(client, args)) + elif command == "xdr-bioc-list": + return_results(bioc_list_command(client, args)) + + elif command == "xdr-bioc-create": + return_results(bioc_create_command(client, args)) + + elif command == "xdr-bioc-update": + return_results(bioc_update_command(client, args)) + + elif command == "xdr-bioc-delete": + return_results(bioc_delete_command(client, args)) + + elif command == "xdr-correlation-rule-list": + return_results(correlation_rule_list_command(client, args)) + + elif command == "xdr-correlation-rule-create": + return_results(correlation_rule_create_command(client, args)) + + elif command == "xdr-correlation-rule-update": + return_results(correlation_rule_update_command(client, args)) + + elif command == "xdr-correlation-rule-delete": + return_results(correlation_rule_delete_command(client, args)) + except Exception as err: return_error(str(err)) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml index 28d99fc5d270..b1bea65683cf 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml @@ -3865,6 +3865,557 @@ script: Update one or more alerts with the provided arguments. Required license: Cortex XDR Prevent, Cortex XDR Pro per Endpoint, or Cortex XDR Pro per GB. name: xdr-update-alert + - arguments: + - description: BIOC name. + name: name + - auto: PREDEFINED + description: BIOC severity. + name: severity + predefined: + - info + - low + - medium + - high + - auto: PREDEFINED + description: BIOC type. + name: type + predefined: + - other + - persistence + - evasion + - tampering + - file_type_obfuscation + - privilege_escalation + - credential_access + - lateral_movement + - execution + - collection + - exfiltration + - infiltration + - dropper + - file_privilege_manipulation + - reconnaissance + - discovery + - auto: PREDEFINED + description: Whether the BIOC is XQL. + name: is_xql + predefined: + - 'true' + - 'false' + - description: BIOC comment. + name: comment + - auto: PREDEFINED + description: BIOC status. + name: status + predefined: + - enabled + - disabled + - description: BIOC indicator. + isArray: true + name: indicator + - description: MITRE technique ID and name. + isArray: true + name: mitre_technique_id_and_name + - description: MITRE tactic ID and name. + isArray: true + name: mitre_tactic_id_and_name + - auto: PREDEFINED + description: Whether to return extended view. + name: extra_data + predefined: + - 'true' + - 'false' + - description: Maximum number of results to return. + name: limit + - description: Page size. + name: page_size + - description: Page number. + name: page + description: Returns a list of BIOCs. + name: xdr-bioc-list + outputs: + - contextPath: PaloAltoNetworksXDR.BIOC.name + description: BIOC name. + type: String + - contextPath: PaloAltoNetworksXDR.BIOC.type + description: BIOC type. + type: String + - contextPath: PaloAltoNetworksXDR.BIOC.severity + description: BIOC severity. + type: String + - contextPath: PaloAltoNetworksXDR.BIOC.status + description: BIOC status. + type: String + - arguments: + - description: BIOC rule ID. + name: rule_id + - description: BIOC name. + name: name + - auto: PREDEFINED + description: BIOC severity. + name: severity + predefined: + - info + - low + - medium + - high + - auto: PREDEFINED + description: BIOC type. + name: type + predefined: + - other + - persistence + - evasion + - tampering + - file_type_obfuscation + - privilege_escalation + - credential_access + - lateral_movement + - execution + - collection + - exfiltration + - infiltration + - dropper + - file_privilege_manipulation + - reconnaissance + - discovery + - auto: PREDEFINED + description: Whether the BIOC is XQL. + name: is_xql + predefined: + - 'true' + - 'false' + - description: BIOC comment. + name: comment + - auto: PREDEFINED + description: BIOC status. + name: status + predefined: + - enabled + - disabled + - description: BIOC indicator. + isArray: true + name: indicator + - description: MITRE technique ID and name. + isArray: true + name: mitre_technique_id_and_name + - description: MITRE tactic ID and name. + isArray: true + name: mitre_tactic_id_and_name + description: Creates a new BIOC. + name: xdr-bioc-create + outputs: + - contextPath: PaloAltoNetworksXDR.BIOC.name + description: BIOC name. + type: String + - contextPath: PaloAltoNetworksXDR.BIOC.type + description: BIOC type. + type: String + - contextPath: PaloAltoNetworksXDR.BIOC.severity + description: BIOC severity. + type: String + - contextPath: PaloAltoNetworksXDR.BIOC.status + description: BIOC status. + type: String + - arguments: + - description: BIOC rule ID. + name: rule_id + - description: BIOC name. + name: name + - auto: PREDEFINED + description: BIOC severity. + name: severity + predefined: + - info + - low + - medium + - high + - auto: PREDEFINED + description: BIOC type. + name: type + predefined: + - other + - persistence + - evasion + - tampering + - file_type_obfuscation + - privilege_escalation + - credential_access + - lateral_movement + - execution + - collection + - exfiltration + - infiltration + - dropper + - file_privilege_manipulation + - reconnaissance + - discovery + - auto: PREDEFINED + description: Whether the BIOC is XQL. + name: is_xql + predefined: + - 'true' + - 'false' + - description: BIOC comment. + name: comment + - auto: PREDEFINED + description: BIOC status. + name: status + predefined: + - enabled + - disabled + - description: BIOC indicator. + isArray: true + name: indicator + - description: MITRE technique ID and name. + isArray: true + name: mitre_technique_id_and_name + - description: MITRE tactic ID and name. + isArray: true + name: mitre_tactic_id_and_name + description: Updates an existing BIOC. + name: xdr-bioc-update + outputs: + - contextPath: PaloAltoNetworksXDR.BIOC.name + description: BIOC name. + type: String + - contextPath: PaloAltoNetworksXDR.BIOC.type + description: BIOC type. + type: String + - contextPath: PaloAltoNetworksXDR.BIOC.severity + description: BIOC severity. + type: String + - contextPath: PaloAltoNetworksXDR.BIOC.status + description: BIOC status. + type: String + - arguments: + - description: BIOC name. + name: name + - auto: PREDEFINED + description: BIOC severity. + name: severity + predefined: + - info + - low + - medium + - high + - auto: PREDEFINED + description: BIOC type. + name: type + predefined: + - other + - persistence + - evasion + - tampering + - file_type_obfuscation + - privilege_escalation + - credential_access + - lateral_movement + - execution + - collection + - exfiltration + - infiltration + - dropper + - file_privilege_manipulation + - reconnaissance + - discovery + - auto: PREDEFINED + description: Whether the BIOC is XQL. + name: is_xql + predefined: + - 'true' + - 'false' + - description: BIOC comment. + name: comment + - description: BIOC indicator. + isArray: true + name: indicator + - description: MITRE technique ID and name. + isArray: true + name: mitre_technique_id_and_name + - description: MITRE tactic ID and name. + isArray: true + name: mitre_tactic_id_and_name + description: Deletes a BIOC. + name: xdr-bioc-delete + - arguments: + - description: Correlation rule name. + name: name + - auto: PREDEFINED + description: Correlation rule severity. + name: severity + predefined: + - info + - low + - medium + - high + - description: Correlation rule XQL query. + name: xql_query + - auto: PREDEFINED + description: Whether the correlation rule is XQL. + name: is_xql + predefined: + - 'true' + - 'false' + - description: Dataset. + name: dataset + - description: Alert name. + name: alert_name + - description: Alert category. + name: alert_category + - description: Alert fields. + isArray: true + name: alert_fields + - description: Alert domain. + name: alert_domain + - description: Filter JSON. + name: filter_json + - auto: PREDEFINED + description: Whether to return extended view. + name: extra_data + predefined: + - 'true' + - 'false' + - description: Maximum number of results to return. + name: limit + - description: Page size. + name: page_size + - description: Page number. + name: page + description: Returns a list of correlation rules. + name: xdr-correlation-rule-list + outputs: + - contextPath: PaloAltoNetworksXDR.CorrelationRule.id + description: Correlation rule ID. + type: String + - contextPath: PaloAltoNetworksXDR.CorrelationRule.name + description: Correlation rule name. + type: String + - contextPath: PaloAltoNetworksXDR.CorrelationRule.description + description: Correlation rule description. + type: String + - contextPath: PaloAltoNetworksXDR.CorrelationRule.is_enabled + description: Whether the correlation rule is enabled. + type: Boolean + - arguments: + - description: Correlation rule ID. + name: rule_id + - description: Correlation rule name. + name: name + - auto: PREDEFINED + description: Correlation rule severity. + name: severity + predefined: + - info + - low + - medium + - high + - description: Correlation rule XQL query. + name: xql_query + - auto: PREDEFINED + description: Whether the correlation rule is enabled. + name: is_enabled + predefined: + - 'true' + - 'false' + - description: Correlation rule description. + name: description + - description: Alert name. + name: alert_name + - auto: PREDEFINED + description: Alert category. + name: alert_category + predefined: + - other + - persistence + - evasion + - tampering + - file_type_obfuscation + - privilege_escalation + - credential_access + - lateral_movement + - execution + - collection + - exfiltration + - infiltration + - dropper + - file_privilege_manipulation + - reconnaissance + - discovery + - description: Alert description. + name: alert_description + - description: Alert fields. + name: alert_fields + - auto: PREDEFINED + description: Execution mode. + name: execution_mode + predefined: + - scheduled + - real_time + - description: Search window. + name: search_window + - description: Simple schedule. + name: schedule + - description: Crontab schedule. + name: schedule_linux + - description: Timezone. + name: timezone + - auto: PREDEFINED + description: Whether suppression is enabled. + name: suppression_enabled + predefined: + - 'true' + - 'false' + - description: Suppression duration. + name: suppression_duration + - description: Suppression fields. + name: suppression_fields + - description: Dataset. + name: dataset + - description: User defined severity. + name: user_defined_severity + - description: User defined category. + name: user_defined_category + - description: MITRE definitions JSON. + name: mitre_defs_json + - description: Investigation query link. + name: investigation_query_link + - description: Drilldown query timeframe. + name: drilldown_query_timeframe + - auto: PREDEFINED + description: Mapping strategy. + name: mapping_strategy + predefined: + - auto + - custom + description: Creates a new correlation rule. + name: xdr-correlation-rule-create + outputs: + - contextPath: PaloAltoNetworksXDR.CorrelationRule.id + description: Correlation rule ID. + type: String + - contextPath: PaloAltoNetworksXDR.CorrelationRule.name + description: Correlation rule name. + type: String + - contextPath: PaloAltoNetworksXDR.CorrelationRule.description + description: Correlation rule description. + type: String + - contextPath: PaloAltoNetworksXDR.CorrelationRule.is_enabled + description: Whether the correlation rule is enabled. + type: Boolean + - arguments: + - description: Correlation rule ID. + name: rule_id + - description: Correlation rule name. + name: name + - auto: PREDEFINED + description: Correlation rule severity. + name: severity + predefined: + - info + - low + - medium + - high + - description: Correlation rule XQL query. + name: xql_query + - auto: PREDEFINED + description: Whether the correlation rule is enabled. + name: is_enabled + predefined: + - 'true' + - 'false' + - description: Correlation rule description. + name: description + - description: Alert name. + name: alert_name + - auto: PREDEFINED + description: Alert category. + name: alert_category + predefined: + - other + - persistence + - evasion + - tampering + - file_type_obfuscation + - privilege_escalation + - credential_access + - lateral_movement + - execution + - collection + - exfiltration + - infiltration + - dropper + - file_privilege_manipulation + - reconnaissance + - discovery + - description: Alert description. + name: alert_description + - description: Alert fields. + name: alert_fields + - auto: PREDEFINED + description: Execution mode. + name: execution_mode + predefined: + - scheduled + - real_time + - description: Search window. + name: search_window + - description: Simple schedule. + name: schedule + - description: Crontab schedule. + name: schedule_linux + - description: Timezone. + name: timezone + - auto: PREDEFINED + description: Whether suppression is enabled. + name: suppression_enabled + predefined: + - 'true' + - 'false' + - description: Suppression duration. + name: suppression_duration + - description: Suppression fields. + name: suppression_fields + - description: Dataset. + name: dataset + - description: User defined severity. + name: user_defined_severity + - description: User defined category. + name: user_defined_category + - description: MITRE definitions JSON. + name: mitre_defs_json + - description: Investigation query link. + name: investigation_query_link + - description: Drilldown query timeframe. + name: drilldown_query_timeframe + - auto: PREDEFINED + description: Mapping strategy. + name: mapping_strategy + predefined: + - auto + - custom + description: Updates an existing correlation rule. + name: xdr-correlation-rule-update + outputs: + - contextPath: PaloAltoNetworksXDR.CorrelationRule.id + description: Correlation rule ID. + type: String + - contextPath: PaloAltoNetworksXDR.CorrelationRule.name + description: Correlation rule name. + type: String + - contextPath: PaloAltoNetworksXDR.CorrelationRule.description + description: Correlation rule description. + type: String + - contextPath: PaloAltoNetworksXDR.CorrelationRule.is_enabled + description: Whether the correlation rule is enabled. + type: Boolean + - arguments: + - description: Correlation rule ID. + isArray: true + name: rule_id + required: true + description: Deletes correlation rules. + name: xdr-correlation-rule-delete dockerimage: demisto/python3:3.12.12.5490952 isfetch: true isfetch:xpanse: false From 793f2d968a7ded5edcb22958114d543e496db72f Mon Sep 17 00:00:00 2001 From: noydavidi Date: Mon, 12 Jan 2026 13:51:42 +0200 Subject: [PATCH 02/27] all written, not checked --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 184 +++++++++++++++--- 1 file changed, 160 insertions(+), 24 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 26eb3ee8e9c1..89694b6b6ba1 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -517,26 +517,62 @@ def get_biocs(self, request_data: dict): reply = self._http_request( method="POST", url_suffix="/bioc/get/", - json_data={"request_data": request_data}, - headers=self.headers, - timeout=self.timeout, + json_data={"request_data": {}}, + # headers=self.headers, + # timeout=self.timeout, ) return reply.get("reply", {}) - def insert_biocs(self, request_data: dict): + def insert_biocs(self, request_data): + request_data = [ + { + "name": "TestBIOC", + "type": "EXECUTION", + "severity": "SEV_020_LOW", + "comment": "", + "status": "ENABLED", + "is_xql": False, + "indicator": { + "runOnCGO": True, + "investigationType": "FILE_EVENT", + "investigation": { + "FILE_EVENT": { + "filter": { + "AND": [ + { + "SEARCH_FIELD": "action_file_name", + "SEARCH_TYPE": "EQ", + "SEARCH_VALUE": "aaaaaa", + "EXTRA_FIELDS": [], + "isExtended": False + } + ] + } + } + } + }, + "mitre_tactic_id_and_name": [ + "" + ], + "mitre_technique_id_and_name": [ + "" + ] + } + ] reply = self._http_request( method="POST", url_suffix="/bioc/insert/", - json_data={"request_data": request_data}, + json_data={"request_data": [request_data]}, headers=self.headers, timeout=self.timeout, ) - return reply.get("reply", {}) + # return reply.get("reply", {}) + return reply def delete_biocs(self, request_data: dict): reply = self._http_request( method="POST", - url_suffix="/bioc/delete/", + url_suffix="/bioc/delete", json_data={"request_data": request_data}, headers=self.headers, timeout=self.timeout, @@ -547,7 +583,7 @@ def get_correlation_rules(self, request_data: dict): reply = self._http_request( method="POST", url_suffix="/correlations/get/", - json_data={"request_data": request_data}, + json_data={"request_data": {"search_from": 0, "search_to": 100}}, headers=self.headers, timeout=self.timeout, ) @@ -556,7 +592,7 @@ def get_correlation_rules(self, request_data: dict): def insert_correlation_rules(self, request_data: dict): reply = self._http_request( method="POST", - url_suffix="/correlations/insert/", + url_suffix="/correlations/insert", json_data={"request_data": request_data}, headers=self.headers, timeout=self.timeout, @@ -566,7 +602,7 @@ def insert_correlation_rules(self, request_data: dict): def delete_correlation_rules(self, request_data: dict): reply = self._http_request( method="POST", - url_suffix="/correlations/delete/", + url_suffix="/correlations/delete", json_data={"request_data": request_data}, headers=self.headers, timeout=self.timeout, @@ -1479,6 +1515,11 @@ def update_alerts_in_xdr_command(client: Client, args: Dict) -> CommandResults: def bioc_list_command(client: Client, args: Dict) -> CommandResults: """ Returns a list of BIOCs. + Args: + client (Client): The client to use. + args (Dict): The command arguments. + Returns: + CommandResults: The command results. """ filters = assign_params( name=args.get("name"), @@ -1499,7 +1540,7 @@ def bioc_list_command(client: Client, args: Dict) -> CommandResults: ) reply = client.get_biocs(request_data) biocs = reply.get("objects", []) - readable_output = tableToMarkdown("BIOCs", biocs, headers=["name", "type", "severity", "status"], removeNull=True) + readable_output = tableToMarkdown(name="BIOCs", t=biocs, headers=["name", "type", "severity", "status"], removeNull=True) return CommandResults( readable_output=readable_output, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.BIOC", @@ -1512,9 +1553,13 @@ def bioc_list_command(client: Client, args: Dict) -> CommandResults: def bioc_create_command(client: Client, args: Dict) -> CommandResults: """ Creates a new BIOC. + Args: + client (Client): The client to use. + args (Dict): The command arguments. + Returns: + CommandResults: The command results. """ bioc_data = assign_params( - rule_id=args.get("rule_id"), name=args.get("name"), severity=args.get("severity"), type=args.get("type"), @@ -1525,20 +1570,53 @@ def bioc_create_command(client: Client, args: Dict) -> CommandResults: mitre_technique_id_and_name=argToList(args.get("mitre_technique_id_and_name")), mitre_tactic_id_and_name=argToList(args.get("mitre_tactic_id_and_name")), ) - client.insert_biocs(bioc_data) - return bioc_list_command(client, {"name": args.get("name")}) + reply = client.insert_biocs(bioc_data) + return CommandResults( + readable_output="BIOC created successfully.", + outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.BIOC", + outputs={"rule_id": reply.get("id")}, + raw_response=reply, + ) def bioc_update_command(client: Client, args: Dict) -> CommandResults: """ Updates an existing BIOC. + Args: + client (Client): The client to use. + args (Dict): The command arguments. + Returns: + CommandResults: The command results. """ - return bioc_create_command(client, args) + bioc_data = assign_params( + rule_id=args.get("rule_id"), + name=args.get("name"), + severity=args.get("severity"), + type=args.get("type"), + is_xql=argToBoolean(args.get("is_xql", False)), + comment=args.get("comment"), + status=args.get("status"), + indicator=argToList(args.get("indicator")), + mitre_technique_id_and_name=argToList(args.get("mitre_technique_id_and_name")), + mitre_tactic_id_and_name=argToList(args.get("mitre_tactic_id_and_name")), + ) + reply = client.insert_biocs(bioc_data) + return CommandResults( + readable_output="BIOC updated successfully.", + outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.BIOC", + outputs={"rule_id": reply.get("id")}, + raw_response=reply, + ) def bioc_delete_command(client: Client, args: Dict) -> str: """ Deletes a BIOC. + Args: + client (Client): The client to use. + args (Dict): The command arguments. + Returns: + str: Success message. """ filters = assign_params( name=args.get("name"), @@ -1551,12 +1629,17 @@ def bioc_delete_command(client: Client, args: Dict) -> str: mitre_tactic_id_and_name=argToList(args.get("mitre_tactic_id_and_name")), ) client.delete_biocs(filters) - return "BIOC object deleted successfully" + return "BIOC deleted successfully." def correlation_rule_list_command(client: Client, args: Dict) -> CommandResults: """ Returns a list of correlation rules. + Args: + client (Client): The client to use. + args (Dict): The command arguments. + Returns: + CommandResults: The command results. """ filters = assign_params( name=args.get("name"), @@ -1581,7 +1664,7 @@ def correlation_rule_list_command(client: Client, args: Dict) -> CommandResults: reply = client.get_correlation_rules(request_data) rules = reply.get("objects", []) readable_output = tableToMarkdown( - "Correlation Rules", rules, headers=["id", "name", "description", "is_enabled"], removeNull=True + name="Correlation Rules", t=rules, headers=["id", "name", "description", "is_enabled"], removeNull=True ) return CommandResults( readable_output=readable_output, @@ -1595,9 +1678,13 @@ def correlation_rule_list_command(client: Client, args: Dict) -> CommandResults: def correlation_rule_create_command(client: Client, args: Dict) -> CommandResults: """ Creates a new correlation rule. + Args: + client (Client): The client to use. + args (Dict): The command arguments. + Returns: + CommandResults: The command results. """ rule_data = assign_params( - rule_id=args.get("rule_id"), name=args.get("name"), severity=args.get("severity"), xql_query=args.get("xql_query"), @@ -1625,25 +1712,74 @@ def correlation_rule_create_command(client: Client, args: Dict) -> CommandResult if mitre_defs_json := args.get("mitre_defs_json"): rule_data["mitre_defs"] = json.loads(mitre_defs_json) - client.insert_correlation_rules(rule_data) - # Run get to return the created rule - return correlation_rule_list_command(client, {"name": args.get("name")}) + reply = client.insert_correlation_rules(rule_data) + return CommandResults( + readable_output="Correlation rule created successfully.", + outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CorrelationRule", + outputs={"id": reply.get("id")}, + raw_response=reply, + ) def correlation_rule_update_command(client: Client, args: Dict) -> CommandResults: """ Updates an existing correlation rule. + Args: + client (Client): The client to use. + args (Dict): The command arguments. + Returns: + CommandResults: The command results. """ - return correlation_rule_create_command(client, args) + rule_data = assign_params( + rule_id=args.get("rule_id"), + name=args.get("name"), + severity=args.get("severity"), + xql_query=args.get("xql_query"), + is_enabled=argToBoolean(args.get("is_enabled")) if args.get("is_enabled") else None, + description=args.get("description"), + alert_name=args.get("alert_name"), + alert_category=args.get("alert_category"), + alert_description=args.get("alert_description"), + alert_fields=args.get("alert_fields"), + execution_mode=args.get("execution_mode"), + search_window=args.get("search_window"), + schedule=args.get("schedule"), + schedule_linux=args.get("schedule_linux"), + timezone=args.get("timezone"), + suppression_enabled=argToBoolean(args.get("suppression_enabled")) if args.get("suppression_enabled") else None, + suppression_duration=args.get("suppression_duration"), + suppression_fields=args.get("suppression_fields"), + dataset=args.get("dataset"), + user_defined_severity=args.get("user_defined_severity"), + user_defined_category=args.get("user_defined_category"), + investigation_query_link=args.get("investigation_query_link"), + drilldown_query_timeframe=args.get("drilldown_query_timeframe"), + mapping_strategy=args.get("mapping_strategy"), + ) + if mitre_defs_json := args.get("mitre_defs_json"): + rule_data["mitre_defs"] = json.loads(mitre_defs_json) + + reply = client.insert_correlation_rules(rule_data) + return CommandResults( + readable_output="Correlation rule created successfully.", + outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CorrelationRule", + outputs={"id": reply.get("id")}, + raw_response=reply, + ) -def correlation_rule_delete_command(client: Client, args: Dict) -> str: +def correlation_rule_delete_command(client: Client, args: Dict) -> CommandResults: """ Deletes correlation rules. + Args: + client (Client): The client to use. + args (Dict): The command arguments. + Returns: + str: Success message. """ rule_ids = argToList(args.get("rule_id")) client.delete_correlation_rules({"rule_id_list": rule_ids}) - return "Correlation rule deleted successfully" + return CommandResults(readable_output="Correlation rule deleted successfully") def main(): # pragma: no cover From cf857e37a17b2034906d852deeade0b43807241d Mon Sep 17 00:00:00 2001 From: noydavidi Date: Mon, 19 Jan 2026 11:35:10 +0200 Subject: [PATCH 03/27] started to test commands --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 112 ++++++++---------- .../Integrations/CortexXDRIR/CortexXDRIR.yml | 31 +++-- 2 files changed, 71 insertions(+), 72 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 89694b6b6ba1..5bf8db439dcf 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -50,6 +50,12 @@ XDR_OPEN_STATUS_TO_XSOAR = ["under_investigation", "new"] +BIOC_SEVERITY_MAPPING = { + "info": "SEV_010_INFO", + "low": "SEV_020_LOW", + "medium": "SEV_030_MEDIUM", + "high": "SEV_040_HIGH" +} def convert_epoch_to_milli(timestamp): if timestamp is None: @@ -517,56 +523,16 @@ def get_biocs(self, request_data: dict): reply = self._http_request( method="POST", url_suffix="/bioc/get/", - json_data={"request_data": {}}, - # headers=self.headers, - # timeout=self.timeout, + json_data=request_data, ) - return reply.get("reply", {}) + return reply def insert_biocs(self, request_data): - request_data = [ - { - "name": "TestBIOC", - "type": "EXECUTION", - "severity": "SEV_020_LOW", - "comment": "", - "status": "ENABLED", - "is_xql": False, - "indicator": { - "runOnCGO": True, - "investigationType": "FILE_EVENT", - "investigation": { - "FILE_EVENT": { - "filter": { - "AND": [ - { - "SEARCH_FIELD": "action_file_name", - "SEARCH_TYPE": "EQ", - "SEARCH_VALUE": "aaaaaa", - "EXTRA_FIELDS": [], - "isExtended": False - } - ] - } - } - } - }, - "mitre_tactic_id_and_name": [ - "" - ], - "mitre_technique_id_and_name": [ - "" - ] - } - ] reply = self._http_request( method="POST", url_suffix="/bioc/insert/", - json_data={"request_data": [request_data]}, - headers=self.headers, - timeout=self.timeout, + json_data=request_data, ) - # return reply.get("reply", {}) return reply def delete_biocs(self, request_data: dict): @@ -1512,6 +1478,29 @@ def update_alerts_in_xdr_command(client: Client, args: Dict) -> CommandResults: return CommandResults(readable_output="Alerts with IDs {} have been updated successfully.".format(",".join(array_of_all_ids))) +def create_filters_for_bioc_list(args: dict) -> list: + filters = [] + if name := args.get("name"): + filters.append({"field": "name", "operator": "EQ", "value": name}) + if severity := args.get("severity"): + filters.append({"field": "severity", "operator": "EQ", "value": BIOC_SEVERITY_MAPPING.get(severity, severity)}) + if type_ := args.get("type"): + filters.append({"field": "type", "operator": "EQ", "value": type_}) + if is_xql := args.get("is_xql"): + filters.append({"field": "is_xql", "operator": "EQ", "value": argToBoolean(is_xql)}) + if comment := args.get("comment"): + filters.append({"field": "comment", "operator": "EQ", "value": comment}) + if status := args.get("status"): + filters.append({"field": "status", "operator": "EQ", "value": status}) + if indicator := args.get("indicator"): + filters.append({"field": "indicator", "operator": "EQ", "value": indicator}) + if mitre_technique := argToList(args.get("mitre_technique_id_and_name")): + filters.append({"field": "mitre_technique_id_and_name", "operator": "EQ", "value": mitre_technique}) + if mitre_tactic := argToList(args.get("mitre_tactic_id_and_name")): + filters.append({"field": "mitre_tactic_id_and_name", "operator": "EQ", "value": mitre_tactic}) + return filters + + def bioc_list_command(client: Client, args: Dict) -> CommandResults: """ Returns a list of BIOCs. @@ -1521,30 +1510,29 @@ def bioc_list_command(client: Client, args: Dict) -> CommandResults: Returns: CommandResults: The command results. """ - filters = assign_params( - name=args.get("name"), - severity=args.get("severity"), - type=args.get("type"), - is_xql=argToBoolean(args.get("is_xql")) if args.get("is_xql") else None, - comment=args.get("comment"), - status=args.get("status"), - indicator=argToList(args.get("indicator")), - mitre_technique_id_and_name=argToList(args.get("mitre_technique_id_and_name")), - mitre_tactic_id_and_name=argToList(args.get("mitre_tactic_id_and_name")), - ) + filters = create_filters_for_bioc_list(args) + page = arg_to_number(args.get("page")) or 0 + limit = arg_to_number(args.get("limit")) or 50 + page_size = arg_to_number(args.get("page_size")) or limit + extended_view = argToBoolean(args.get("extra_data", False)) + request_data = assign_params( filters=filters, - extended_view=argToBoolean(args.get("extra_data", False)), - search_from=arg_to_number(args.get("page")), - search_to=arg_to_number(args.get("limit")), + extended_view=extended_view, + search_from=page * page_size, + search_to=(page + 1) * page_size, ) - reply = client.get_biocs(request_data) + reply = client.get_biocs({"request_data": request_data}) biocs = reply.get("objects", []) - readable_output = tableToMarkdown(name="BIOCs", t=biocs, headers=["name", "type", "severity", "status"], removeNull=True) + readable_output = tableToMarkdown(name="BIOCs List", + t=biocs, + headerTransform=string_to_table_header, + headers=["rule_id", "name", "type", "severity", "status"], + removeNull=True) return CommandResults( readable_output=readable_output, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.BIOC", - outputs_key_field="name", + outputs_key_field="rule_id", outputs=biocs, raw_response=reply, ) @@ -1561,7 +1549,7 @@ def bioc_create_command(client: Client, args: Dict) -> CommandResults: """ bioc_data = assign_params( name=args.get("name"), - severity=args.get("severity"), + severity=BIOC_SEVERITY_MAPPING.get(args.get("severity")), type=args.get("type"), is_xql=argToBoolean(args.get("is_xql", False)), comment=args.get("comment"), @@ -1570,7 +1558,7 @@ def bioc_create_command(client: Client, args: Dict) -> CommandResults: mitre_technique_id_and_name=argToList(args.get("mitre_technique_id_and_name")), mitre_tactic_id_and_name=argToList(args.get("mitre_tactic_id_and_name")), ) - reply = client.insert_biocs(bioc_data) + reply = client.insert_biocs({"request_data": [bioc_data]}) return CommandResults( readable_output="BIOC created successfully.", outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.BIOC", diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml index b1bea65683cf..b47c52dd4db4 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml @@ -3866,10 +3866,10 @@ script: Required license: Cortex XDR Prevent, Cortex XDR Pro per Endpoint, or Cortex XDR Pro per GB. name: xdr-update-alert - arguments: - - description: BIOC name. + - description: 'BIOC name. String.' name: name - auto: PREDEFINED - description: BIOC severity. + description: 'BIOC severity. Can be one of the following: SEV_010_INFO, SEV_020_LOW, SEV_030_MEDIUM, SEV_040_HIGH' name: severity predefined: - info @@ -3877,7 +3877,7 @@ script: - medium - high - auto: PREDEFINED - description: BIOC type. + description: 'BIOC type. Can be one of the following: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery.' name: type predefined: - other @@ -3897,26 +3897,25 @@ script: - reconnaissance - discovery - auto: PREDEFINED - description: Whether the BIOC is XQL. + description: 'Whether the BIOC is XQL. Boolean: true or false.' name: is_xql predefined: - 'true' - 'false' - - description: BIOC comment. + - description: 'BIOC comment. String.' name: comment - auto: PREDEFINED - description: BIOC status. + description: 'BIOC status. Can be one of the following: enabled, disabled.' name: status predefined: - enabled - disabled - - description: BIOC indicator. - isArray: true + - description: 'BIOC indicator. String or dictionary.' name: indicator - - description: MITRE technique ID and name. + - description: 'MITRE technique ID and name. List of strings.' isArray: true name: mitre_technique_id_and_name - - description: MITRE tactic ID and name. + - description: 'MITRE tactic ID and name. List of strings.' isArray: true name: mitre_tactic_id_and_name - auto: PREDEFINED @@ -3934,6 +3933,9 @@ script: description: Returns a list of BIOCs. name: xdr-bioc-list outputs: + - contextPath: PaloAltoNetworksXDR.BIOC.rule_id + description: BIOC rule id. + type: String - contextPath: PaloAltoNetworksXDR.BIOC.name description: BIOC name. type: String @@ -3946,6 +3948,15 @@ script: - contextPath: PaloAltoNetworksXDR.BIOC.status description: BIOC status. type: String + - contextPath: PaloAltoNetworksXDR.BIOC.is_xql + description: + type: Bool + - contextPath: PaloAltoNetworksXDR.BIOC.comment + description: + type: String + - contextPath: PaloAltoNetworksXDR.BIOC.indicator + description: + type: Unknown - arguments: - description: BIOC rule ID. name: rule_id From e679a3f91b6d4ac579fb776836e096905b863867 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Mon, 19 Jan 2026 14:43:16 +0200 Subject: [PATCH 04/27] tested comamnds until create-bioc --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 74 ++++++++++++++----- .../Integrations/CortexXDRIR/CortexXDRIR.yml | 46 ++++++------ 2 files changed, 80 insertions(+), 40 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 5bf8db439dcf..d4aa311bc711 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -1532,7 +1532,7 @@ def bioc_list_command(client: Client, args: Dict) -> CommandResults: return CommandResults( readable_output=readable_output, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.BIOC", - outputs_key_field="rule_id", + outputs_key_field="name", outputs=biocs, raw_response=reply, ) @@ -1554,15 +1554,28 @@ def bioc_create_command(client: Client, args: Dict) -> CommandResults: is_xql=argToBoolean(args.get("is_xql", False)), comment=args.get("comment"), status=args.get("status"), - indicator=argToList(args.get("indicator")), - mitre_technique_id_and_name=argToList(args.get("mitre_technique_id_and_name")), - mitre_tactic_id_and_name=argToList(args.get("mitre_tactic_id_and_name")), ) + + indicator = args.get("indicator") + if indicator: + try: + indicator = json.loads(indicator) + bioc_data["indicator"] = indicator + except ValueError: + raise DemistoException("Unable to parse 'indicator'. Please use the JSON format.") + + # required fields but can be empty + bioc_data["mitre_technique_id_and_name"] = argToList(args.get("mitre_technique_id_and_name")) or [] + bioc_data["mitre_tactic_id_and_name"] = argToList(args.get("mitre_tactic_id_and_name")) or [] + reply = client.insert_biocs({"request_data": [bioc_data]}) + rule_id = reply.get("added_objects", [])[0].get("id") + message = reply.get("added_objects", [])[0].get("status") return CommandResults( - readable_output="BIOC created successfully.", + readable_output=message, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.BIOC", - outputs={"rule_id": reply.get("id")}, + outputs_key_field="rule_id", + outputs={"rule_id": rule_id}, raw_response=reply, ) @@ -1577,22 +1590,34 @@ def bioc_update_command(client: Client, args: Dict) -> CommandResults: CommandResults: The command results. """ bioc_data = assign_params( - rule_id=args.get("rule_id"), name=args.get("name"), - severity=args.get("severity"), + severity=BIOC_SEVERITY_MAPPING.get(args.get("severity")), type=args.get("type"), is_xql=argToBoolean(args.get("is_xql", False)), comment=args.get("comment"), status=args.get("status"), - indicator=argToList(args.get("indicator")), - mitre_technique_id_and_name=argToList(args.get("mitre_technique_id_and_name")), - mitre_tactic_id_and_name=argToList(args.get("mitre_tactic_id_and_name")), ) - reply = client.insert_biocs(bioc_data) + + indicator = args.get("indicator") + if indicator: + try: + indicator = json.loads(indicator) + bioc_data["indicator"] = indicator + except ValueError: + raise DemistoException("Unable to parse 'indicator'. Please use the JSON format.") + + # required fields but can be empty + bioc_data["mitre_technique_id_and_name"] = argToList(args.get("mitre_technique_id_and_name")) or [] + bioc_data["mitre_tactic_id_and_name"] = argToList(args.get("mitre_tactic_id_and_name")) or [] + + reply = client.insert_biocs({"request_data": [bioc_data]}) + rule_id = reply.get("added_objects", [])[0].get("id") + message = reply.get("added_objects", [])[0].get("status") return CommandResults( - readable_output="BIOC updated successfully.", + readable_output=message, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.BIOC", - outputs={"rule_id": reply.get("id")}, + outputs_key_field="rule_id", + outputs={"rule_id": rule_id}, raw_response=reply, ) @@ -1608,14 +1633,25 @@ def bioc_delete_command(client: Client, args: Dict) -> str: """ filters = assign_params( name=args.get("name"), - severity=args.get("severity"), + severity=BIOC_SEVERITY_MAPPING.get(args.get("severity")), type=args.get("type"), - is_xql=argToBoolean(args.get("is_xql")) if args.get("is_xql") else None, + is_xql=argToBoolean(args.get("is_xql", False)), comment=args.get("comment"), - indicator=argToList(args.get("indicator")), - mitre_technique_id_and_name=argToList(args.get("mitre_technique_id_and_name")), - mitre_tactic_id_and_name=argToList(args.get("mitre_tactic_id_and_name")), + status=args.get("status"), ) + + indicator = args.get("indicator") + if indicator: + try: + indicator = json.loads(indicator) + filters["indicator"] = indicator + except ValueError: + raise DemistoException("Unable to parse 'indicator'. Please use the JSON format.") + + # required fields but can be empty + filters["mitre_technique_id_and_name"] = argToList(args.get("mitre_technique_id_and_name")) or [] + filters["mitre_tactic_id_and_name"] = argToList(args.get("mitre_tactic_id_and_name")) or [] + client.delete_biocs(filters) return "BIOC deleted successfully." diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml index b47c52dd4db4..a11c162a5235 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml @@ -3866,10 +3866,10 @@ script: Required license: Cortex XDR Prevent, Cortex XDR Pro per Endpoint, or Cortex XDR Pro per GB. name: xdr-update-alert - arguments: - - description: 'BIOC name. String.' + - description: 'The BIOC name to filter by. Can filter only by one name.' name: name - auto: PREDEFINED - description: 'BIOC severity. Can be one of the following: SEV_010_INFO, SEV_020_LOW, SEV_030_MEDIUM, SEV_040_HIGH' + description: 'The BIOC severity to filter by.' name: severity predefined: - info @@ -3877,7 +3877,7 @@ script: - medium - high - auto: PREDEFINED - description: 'BIOC type. Can be one of the following: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery.' + description: 'The BIOC type to filter by.' name: type predefined: - other @@ -3897,29 +3897,29 @@ script: - reconnaissance - discovery - auto: PREDEFINED - description: 'Whether the BIOC is XQL. Boolean: true or false.' + description: 'Whether the BIOC is XQL.' name: is_xql predefined: - 'true' - 'false' - - description: 'BIOC comment. String.' + - description: 'The BIOC comment to filter by.' name: comment - auto: PREDEFINED - description: 'BIOC status. Can be one of the following: enabled, disabled.' + description: 'The BIOC status to filter by. Can be one of the following: enabled, disabled.' name: status predefined: - enabled - disabled - - description: 'BIOC indicator. String or dictionary.' + - description: 'The BIOC indicator to filter by.' name: indicator - - description: 'MITRE technique ID and name. List of strings.' + - description: 'MITRE technique ID and name.' isArray: true name: mitre_technique_id_and_name - - description: 'MITRE tactic ID and name. List of strings.' + - description: 'MITRE tactic ID and name.' isArray: true name: mitre_tactic_id_and_name - auto: PREDEFINED - description: Whether to return extended view. + description: Whether to return extended data. name: extra_data predefined: - 'true' @@ -3958,21 +3958,22 @@ script: description: type: Unknown - arguments: - - description: BIOC rule ID. - name: rule_id - - description: BIOC name. + - description: The BIOC name. name: name + required: true - auto: PREDEFINED - description: BIOC severity. + description: The BIOC severity. name: severity + required: true predefined: - info - low - medium - high - auto: PREDEFINED - description: BIOC type. + description: The BIOC type. name: type + required: true predefined: - other - persistence @@ -3991,26 +3992,29 @@ script: - reconnaissance - discovery - auto: PREDEFINED - description: Whether the BIOC is XQL. + description: Whether the new BIOC is XQL. name: is_xql predefined: - 'true' - 'false' - - description: BIOC comment. + - description: The BIOC comment. name: comment + required: true - auto: PREDEFINED - description: BIOC status. + description: The BIOC status. name: status + required: true predefined: - enabled - disabled - - description: BIOC indicator. + - description: "The BIOC indicator, for example: '{\"runOnCGO\":true,\"investigationType\":\"FILE_EVENT\",\"investigation\":{\"FILE_EVENT\":{\"filter\":{\"AND\":[{\"SEARCH_FIELD\":\"action_file_name\",\"SEARCH_TYPE\":\"EQ\",\"SEARCH_VALUE\":\"testfile.exe\"}]}}}}'. For more information please refer to the documentation https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-BIOCs." isArray: true name: indicator - - description: MITRE technique ID and name. + required: true + - description: "The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']." isArray: true name: mitre_technique_id_and_name - - description: MITRE tactic ID and name. + - description: "The MITRE tactic ID and name. Must be in format 'ID - Name', for example: ['TA0001 - Initial Access']." isArray: true name: mitre_tactic_id_and_name description: Creates a new BIOC. From 288afecaf64a41b4b7fca9b846e09a5f53d52f3e Mon Sep 17 00:00:00 2001 From: noydavidi Date: Tue, 20 Jan 2026 11:52:36 +0200 Subject: [PATCH 05/27] fixed create and update bioc --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 115 ++++++++---------- .../Integrations/CortexXDRIR/CortexXDRIR.yml | 29 +++-- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index d4aa311bc711..991e3c26c335 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -527,7 +527,7 @@ def get_biocs(self, request_data: dict): ) return reply - def insert_biocs(self, request_data): + def insert_or_update_biocs(self, request_data): reply = self._http_request( method="POST", url_suffix="/bioc/insert/", @@ -539,11 +539,11 @@ def delete_biocs(self, request_data: dict): reply = self._http_request( method="POST", url_suffix="/bioc/delete", - json_data={"request_data": request_data}, + json_data=request_data, headers=self.headers, timeout=self.timeout, ) - return reply.get("reply", {}) + return reply def get_correlation_rules(self, request_data: dict): reply = self._http_request( @@ -1478,14 +1478,14 @@ def update_alerts_in_xdr_command(client: Client, args: Dict) -> CommandResults: return CommandResults(readable_output="Alerts with IDs {} have been updated successfully.".format(",".join(array_of_all_ids))) -def create_filters_for_bioc_list(args: dict) -> list: +def create_filters_for_bioc(args: dict) -> list: filters = [] if name := args.get("name"): filters.append({"field": "name", "operator": "EQ", "value": name}) if severity := args.get("severity"): filters.append({"field": "severity", "operator": "EQ", "value": BIOC_SEVERITY_MAPPING.get(severity, severity)}) - if type_ := args.get("type"): - filters.append({"field": "type", "operator": "EQ", "value": type_}) + if bioc_type := args.get("type"): + filters.append({"field": "type", "operator": "EQ", "value": bioc_type}) if is_xql := args.get("is_xql"): filters.append({"field": "is_xql", "operator": "EQ", "value": argToBoolean(is_xql)}) if comment := args.get("comment"): @@ -1503,6 +1503,7 @@ def create_filters_for_bioc_list(args: dict) -> list: def bioc_list_command(client: Client, args: Dict) -> CommandResults: """ + API Docs https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Get-BIOCs Returns a list of BIOCs. Args: client (Client): The client to use. @@ -1510,7 +1511,7 @@ def bioc_list_command(client: Client, args: Dict) -> CommandResults: Returns: CommandResults: The command results. """ - filters = create_filters_for_bioc_list(args) + filters = create_filters_for_bioc(args) page = arg_to_number(args.get("page")) or 0 limit = arg_to_number(args.get("limit")) or 50 page_size = arg_to_number(args.get("page_size")) or limit @@ -1538,16 +1539,9 @@ def bioc_list_command(client: Client, args: Dict) -> CommandResults: ) -def bioc_create_command(client: Client, args: Dict) -> CommandResults: - """ - Creates a new BIOC. - Args: - client (Client): The client to use. - args (Dict): The command arguments. - Returns: - CommandResults: The command results. - """ +def bioc_create_or_update_helper(client: Client, args: Dict) -> dict: bioc_data = assign_params( + rule_id=args.get("rule_id"), # for update only name=args.get("name"), severity=BIOC_SEVERITY_MAPPING.get(args.get("severity")), type=args.get("type"), @@ -1568,20 +1562,36 @@ def bioc_create_command(client: Client, args: Dict) -> CommandResults: bioc_data["mitre_technique_id_and_name"] = argToList(args.get("mitre_technique_id_and_name")) or [] bioc_data["mitre_tactic_id_and_name"] = argToList(args.get("mitre_tactic_id_and_name")) or [] - reply = client.insert_biocs({"request_data": [bioc_data]}) - rule_id = reply.get("added_objects", [])[0].get("id") - message = reply.get("added_objects", [])[0].get("status") + return client.insert_or_update_biocs({"request_data": [bioc_data]}) + + +def bioc_create_command(client: Client, args: Dict) -> CommandResults: + """ + API Docs https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-BIOCs + Creates a new BIOC. + Args: + client (Client): The client to use. + args (Dict): The command arguments. + Returns: + CommandResults: The command results. + """ + reply = bioc_create_or_update_helper(client, args) + added_objects = reply.get("added_objects") + message = added_objects[0].get("status") + outputs = {"rule_id": added_objects[0].get("id")} + return CommandResults( readable_output=message, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.BIOC", outputs_key_field="rule_id", - outputs={"rule_id": rule_id}, + outputs=outputs, raw_response=reply, ) def bioc_update_command(client: Client, args: Dict) -> CommandResults: """ + API Docs https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-BIOCs Updates an existing BIOC. Args: client (Client): The client to use. @@ -1589,41 +1599,26 @@ def bioc_update_command(client: Client, args: Dict) -> CommandResults: Returns: CommandResults: The command results. """ - bioc_data = assign_params( - name=args.get("name"), - severity=BIOC_SEVERITY_MAPPING.get(args.get("severity")), - type=args.get("type"), - is_xql=argToBoolean(args.get("is_xql", False)), - comment=args.get("comment"), - status=args.get("status"), - ) - - indicator = args.get("indicator") - if indicator: - try: - indicator = json.loads(indicator) - bioc_data["indicator"] = indicator - except ValueError: - raise DemistoException("Unable to parse 'indicator'. Please use the JSON format.") - - # required fields but can be empty - bioc_data["mitre_technique_id_and_name"] = argToList(args.get("mitre_technique_id_and_name")) or [] - bioc_data["mitre_tactic_id_and_name"] = argToList(args.get("mitre_tactic_id_and_name")) or [] + reply = bioc_create_or_update_helper(client, args) + if updated_objects := reply.get("updated_objects"): + message = updated_objects[0].get("status") + outputs = {"rule_id": updated_objects[0].get("id")} + else: + message = "No BIOCs updated." + outputs = {} - reply = client.insert_biocs({"request_data": [bioc_data]}) - rule_id = reply.get("added_objects", [])[0].get("id") - message = reply.get("added_objects", [])[0].get("status") return CommandResults( readable_output=message, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.BIOC", outputs_key_field="rule_id", - outputs={"rule_id": rule_id}, + outputs=outputs, raw_response=reply, ) def bioc_delete_command(client: Client, args: Dict) -> str: """ + API Docs https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Delete-BIOCs Deletes a BIOC. Args: client (Client): The client to use. @@ -1631,29 +1626,21 @@ def bioc_delete_command(client: Client, args: Dict) -> str: Returns: str: Success message. """ - filters = assign_params( - name=args.get("name"), - severity=BIOC_SEVERITY_MAPPING.get(args.get("severity")), - type=args.get("type"), - is_xql=argToBoolean(args.get("is_xql", False)), - comment=args.get("comment"), - status=args.get("status"), - ) + filters: list = create_filters_for_bioc(args) + request_data = {"request_data": {"filters": filters}} + res = client.delete_biocs(request_data) + rule_ids = res.get("objects", []) + count = len(rule_ids) - indicator = args.get("indicator") - if indicator: - try: - indicator = json.loads(indicator) - filters["indicator"] = indicator - except ValueError: - raise DemistoException("Unable to parse 'indicator'. Please use the JSON format.") + if count == 1: + return f"BIOC with id {rule_ids[0]} deleted successfully." - # required fields but can be empty - filters["mitre_technique_id_and_name"] = argToList(args.get("mitre_technique_id_and_name")) or [] - filters["mitre_tactic_id_and_name"] = argToList(args.get("mitre_tactic_id_and_name")) or [] + elif count > 1: + ids_str = ", ".join(map(str, rule_ids)) + return f"BIOCs with ids {ids_str} deleted successfully." - client.delete_biocs(filters) - return "BIOC deleted successfully." + else: + return "No BIOCs were found to delete." def correlation_rule_list_command(client: Client, args: Dict) -> CommandResults: diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml index a11c162a5235..3a5e2e874a71 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml @@ -3912,10 +3912,10 @@ script: - disabled - description: 'The BIOC indicator to filter by.' name: indicator - - description: 'MITRE technique ID and name.' + - description: "The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']." isArray: true name: mitre_technique_id_and_name - - description: 'MITRE tactic ID and name.' + - description: "The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']." isArray: true name: mitre_tactic_id_and_name - auto: PREDEFINED @@ -4007,7 +4007,10 @@ script: predefined: - enabled - disabled - - description: "The BIOC indicator, for example: '{\"runOnCGO\":true,\"investigationType\":\"FILE_EVENT\",\"investigation\":{\"FILE_EVENT\":{\"filter\":{\"AND\":[{\"SEARCH_FIELD\":\"action_file_name\",\"SEARCH_TYPE\":\"EQ\",\"SEARCH_VALUE\":\"testfile.exe\"}]}}}}'. For more information please refer to the documentation https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-BIOCs." + - description: |- + The BIOC indicator, + for example: '{\"runOnCGO\":true,\"investigationType\":\"FILE_EVENT\",\"investigation\":{\"FILE_EVENT\":{\"filter\":{\"AND\":[{\"SEARCH_FIELD\":\"action_file_name\",\"SEARCH_TYPE\":\"EQ\",\"SEARCH_VALUE\":\"testfile.exe\"}]}}}}'. + For more information please refer to the documentation https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-BIOCs. isArray: true name: indicator required: true @@ -4035,11 +4038,14 @@ script: - arguments: - description: BIOC rule ID. name: rule_id + required: true - description: BIOC name. name: name + required: true - auto: PREDEFINED description: BIOC severity. name: severity + required: true predefined: - info - low @@ -4048,6 +4054,7 @@ script: - auto: PREDEFINED description: BIOC type. name: type + required: true predefined: - other - persistence @@ -4073,19 +4080,25 @@ script: - 'false' - description: BIOC comment. name: comment + required: true - auto: PREDEFINED description: BIOC status. name: status + required: true predefined: - enabled - disabled - - description: BIOC indicator. + - description: |- + The BIOC indicator, + for example: '{\"runOnCGO\":true,\"investigationType\":\"FILE_EVENT\",\"investigation\":{\"FILE_EVENT\":{\"filter\":{\"AND\":[{\"SEARCH_FIELD\":\"action_file_name\",\"SEARCH_TYPE\":\"EQ\",\"SEARCH_VALUE\":\"testfile.exe\"}]}}}}'. + For more information please refer to the documentation https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-BIOCs. isArray: true name: indicator - - description: MITRE technique ID and name. + required: true + - description: "The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']." isArray: true name: mitre_technique_id_and_name - - description: MITRE tactic ID and name. + - description: "The MITRE tactic ID and name. Must be in format 'ID - Name', for example: ['TA0001 - Initial Access']." isArray: true name: mitre_tactic_id_and_name description: Updates an existing BIOC. @@ -4145,10 +4158,10 @@ script: - description: BIOC indicator. isArray: true name: indicator - - description: MITRE technique ID and name. + - description: "The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']." isArray: true name: mitre_technique_id_and_name - - description: MITRE tactic ID and name. + - description: "The MITRE tactic ID and name. Must be in format 'ID - Name', for example: ['TA0001 - Initial Access']." isArray: true name: mitre_tactic_id_and_name description: Deletes a BIOC. From cb3b9335fb3237b5d0c8c0c159bcbea830937411 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Tue, 20 Jan 2026 14:43:22 +0200 Subject: [PATCH 06/27] started testing cr commands --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 146 ++++++++++++------ .../Integrations/CortexXDRIR/CortexXDRIR.yml | 65 ++++---- 2 files changed, 135 insertions(+), 76 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 991e3c26c335..6d639973d282 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -50,7 +50,7 @@ XDR_OPEN_STATUS_TO_XSOAR = ["under_investigation", "new"] -BIOC_SEVERITY_MAPPING = { +BIOC_AND_CR_SEVERITY_MAPPING = { "info": "SEV_010_INFO", "low": "SEV_020_LOW", "medium": "SEV_030_MEDIUM", @@ -540,8 +540,6 @@ def delete_biocs(self, request_data: dict): method="POST", url_suffix="/bioc/delete", json_data=request_data, - headers=self.headers, - timeout=self.timeout, ) return reply @@ -549,9 +547,7 @@ def get_correlation_rules(self, request_data: dict): reply = self._http_request( method="POST", url_suffix="/correlations/get/", - json_data={"request_data": {"search_from": 0, "search_to": 100}}, - headers=self.headers, - timeout=self.timeout, + json_data=request_data, ) return reply.get("reply", {}) @@ -559,19 +555,15 @@ def insert_correlation_rules(self, request_data: dict): reply = self._http_request( method="POST", url_suffix="/correlations/insert", - json_data={"request_data": request_data}, - headers=self.headers, - timeout=self.timeout, + json_data=request_data, ) - return reply.get("reply", {}) + return reply def delete_correlation_rules(self, request_data: dict): reply = self._http_request( method="POST", url_suffix="/correlations/delete", json_data={"request_data": request_data}, - headers=self.headers, - timeout=self.timeout, ) return reply.get("reply", {}) @@ -1478,12 +1470,21 @@ def update_alerts_in_xdr_command(client: Client, args: Dict) -> CommandResults: return CommandResults(readable_output="Alerts with IDs {} have been updated successfully.".format(",".join(array_of_all_ids))) -def create_filters_for_bioc(args: dict) -> list: +def create_filters_for_bioc_and_correlation_rules(args: dict) -> list: + """ + Creates a list of filters for BIOC and correlation rules based on the provided arguments. + + Args: + args (dict): The command arguments containing filter criteria. + + Returns: + list: A list of filter dictionaries compatible with the Cortex XDR API. + """ filters = [] if name := args.get("name"): filters.append({"field": "name", "operator": "EQ", "value": name}) if severity := args.get("severity"): - filters.append({"field": "severity", "operator": "EQ", "value": BIOC_SEVERITY_MAPPING.get(severity, severity)}) + filters.append({"field": "severity", "operator": "EQ", "value": BIOC_AND_CR_SEVERITY_MAPPING.get(severity, severity)}) if bioc_type := args.get("type"): filters.append({"field": "type", "operator": "EQ", "value": bioc_type}) if is_xql := args.get("is_xql"): @@ -1498,6 +1499,52 @@ def create_filters_for_bioc(args: dict) -> list: filters.append({"field": "mitre_technique_id_and_name", "operator": "EQ", "value": mitre_technique}) if mitre_tactic := argToList(args.get("mitre_tactic_id_and_name")): filters.append({"field": "mitre_tactic_id_and_name", "operator": "EQ", "value": mitre_tactic}) + if xql_query := args.get("xql_query"): + filters.append({"field": "xql_query", "operator": "EQ", "value": xql_query}) + if is_enabled := args.get("is_enabled"): + filters.append({"field": "is_enabled", "operator": "EQ", "value": argToBoolean(is_enabled)}) + if description := args.get("description"): + filters.append({"field": "description", "operator": "EQ", "value": description}) + if alert_name := args.get("alert_name"): + filters.append({"field": "alert_name", "operator": "EQ", "value": alert_name}) + if alert_category := args.get("alert_category"): + filters.append({"field": "alert_category", "operator": "EQ", "value": alert_category}) + if alert_description := args.get("alert_description"): + filters.append({"field": "alert_description", "operator": "EQ", "value": alert_description}) + if alert_fields := argToList(args.get("alert_fields")): + filters.append({"field": "alert_fields", "operator": "EQ", "value": alert_fields}) + if execution_mode := args.get("execution_mode"): + filters.append({"field": "execution_mode", "operator": "EQ", "value": execution_mode}) + if search_window := args.get("search_window"): + filters.append({"field": "search_window", "operator": "EQ", "value": search_window}) + if simple_schedule := args.get("schedule"): + filters.append({"field": "schedule", "operator": "EQ", "value": simple_schedule}) + if timezone := args.get("timezone"): + filters.append({"field": "timezone", "operator": "EQ", "value": timezone}) + if crontab := args.get("schedule_linux"): + filters.append({"field": "schedule_linux", "operator": "EQ", "value": crontab}) + if suppression_enabled := args.get("suppression_enabled"): + filters.append({"field": "suppression_enabled", "operator": "EQ", "value": argToBoolean(suppression_enabled)}) + if suppression_duration := args.get("suppression_duration"): + filters.append({"field": "suppression_duration", "operator": "EQ", "value": suppression_duration}) + if suppression_fields := args.get("suppression_fields"): + filters.append({"field": "suppression_fields", "operator": "EQ", "value": suppression_fields}) + if dataset := args.get("dataset"): + filters.append({"field": "dataset", "operator": "EQ", "value": dataset}) + if user_defined_severity := args.get("user_defined_severity"): + filters.append({"field": "user_defined_severity", "operator": "EQ", "value": user_defined_severity}) + if user_defined_category := args.get("user_defined_category"): + filters.append({"field": "user_defined_category", "operator": "EQ", "value": user_defined_category}) + if mitre_defs := args.get("mitre_defs_json"): + filters.append({"field": "mitre_defs", "operator": "EQ", "value": json.loads(mitre_defs)}) + if investigation_query_link := args.get("investigation_query_link"): + filters.append({"field": "investigation_query_link", "operator": "EQ", "value": investigation_query_link}) + if drilldown_query_timeframe := args.get("drilldown_query_timeframe"): + filters.append({"field": "drilldown_query_timeframe", "operator": "EQ", "value": drilldown_query_timeframe}) + if mapping_strategy := args.get("mapping_strategy"): + filters.append({"field": "mapping_strategy", "operator": "EQ", "value": mapping_strategy}) + if alert_domain := args.get("alert_domain"): + filters.append({"field": "alert_domain", "operator": "EQ", "value": alert_domain}) return filters @@ -1511,7 +1558,7 @@ def bioc_list_command(client: Client, args: Dict) -> CommandResults: Returns: CommandResults: The command results. """ - filters = create_filters_for_bioc(args) + filters = create_filters_for_bioc_and_correlation_rules(args) page = arg_to_number(args.get("page")) or 0 limit = arg_to_number(args.get("limit")) or 50 page_size = arg_to_number(args.get("page_size")) or limit @@ -1543,7 +1590,7 @@ def bioc_create_or_update_helper(client: Client, args: Dict) -> dict: bioc_data = assign_params( rule_id=args.get("rule_id"), # for update only name=args.get("name"), - severity=BIOC_SEVERITY_MAPPING.get(args.get("severity")), + severity=BIOC_AND_CR_SEVERITY_MAPPING.get(args.get("severity")), type=args.get("type"), is_xql=argToBoolean(args.get("is_xql", False)), comment=args.get("comment"), @@ -1626,7 +1673,7 @@ def bioc_delete_command(client: Client, args: Dict) -> str: Returns: str: Success message. """ - filters: list = create_filters_for_bioc(args) + filters: list = create_filters_for_bioc_and_correlation_rules(args) request_data = {"request_data": {"filters": filters}} res = client.delete_biocs(request_data) rule_ids = res.get("objects", []) @@ -1652,19 +1699,9 @@ def correlation_rule_list_command(client: Client, args: Dict) -> CommandResults: Returns: CommandResults: The command results. """ - filters = assign_params( - name=args.get("name"), - severity=args.get("severity"), - xql_query=args.get("xql_query"), - is_xql=argToBoolean(args.get("is_xql")) if args.get("is_xql") else None, - dataset=args.get("dataset"), - alert_name=args.get("alert_name"), - alert_category=args.get("alert_category"), - alert_fields=argToList(args.get("alert_fields")), - alet_domain=args.get("alert_domain"), - ) + filters = create_filters_for_bioc_and_correlation_rules(args) if filter_json := args.get("filter_json"): - filters.update(json.loads(filter_json)) + filters.extend(json.loads(filter_json)) request_data = assign_params( filters=filters, @@ -1672,10 +1709,13 @@ def correlation_rule_list_command(client: Client, args: Dict) -> CommandResults: search_from=arg_to_number(args.get("page")), search_to=arg_to_number(args.get("limit")), ) - reply = client.get_correlation_rules(request_data) + reply = client.get_correlation_rules({"request_data": request_data}) rules = reply.get("objects", []) - readable_output = tableToMarkdown( - name="Correlation Rules", t=rules, headers=["id", "name", "description", "is_enabled"], removeNull=True + readable_output = tableToMarkdown(name="Correlation Rules List", + t=rules, + headers=["id", "name", "description", "is_enabled"], + removeNull=True, + headerTransform=string_to_table_header, ) return CommandResults( readable_output=readable_output, @@ -1688,6 +1728,7 @@ def correlation_rule_list_command(client: Client, args: Dict) -> CommandResults: def correlation_rule_create_command(client: Client, args: Dict) -> CommandResults: """ + API Docs https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-Correlation-Rules Creates a new correlation rule. Args: client (Client): The client to use. @@ -1697,19 +1738,20 @@ def correlation_rule_create_command(client: Client, args: Dict) -> CommandResult """ rule_data = assign_params( name=args.get("name"), - severity=args.get("severity"), + severity=BIOC_AND_CR_SEVERITY_MAPPING.get(args.get("severity")), xql_query=args.get("xql_query"), is_enabled=argToBoolean(args.get("is_enabled")) if args.get("is_enabled") else None, description=args.get("description"), alert_name=args.get("alert_name"), - alert_category=args.get("alert_category"), + action="ALERTS", + alert_category=args.get("alert_category").upper() if args.get("alert_category") else None, alert_description=args.get("alert_description"), alert_fields=args.get("alert_fields"), - execution_mode=args.get("execution_mode"), + execution_mode=args.get("execution_mode").upper() if args.get("execution_mode") else None, search_window=args.get("search_window"), - schedule=args.get("schedule"), - schedule_linux=args.get("schedule_linux"), + crontab=args.get("schedule_linux"), timezone=args.get("timezone"), + simple_schedule=args.get("schedule"), suppression_enabled=argToBoolean(args.get("suppression_enabled")) if args.get("suppression_enabled") else None, suppression_duration=args.get("suppression_duration"), suppression_fields=args.get("suppression_fields"), @@ -1718,12 +1760,30 @@ def correlation_rule_create_command(client: Client, args: Dict) -> CommandResult user_defined_category=args.get("user_defined_category"), investigation_query_link=args.get("investigation_query_link"), drilldown_query_timeframe=args.get("drilldown_query_timeframe"), - mapping_strategy=args.get("mapping_strategy"), + mapping_strategy=args.get("mapping_strategy").upper() if args.get("mapping_strategy") else None, ) - if mitre_defs_json := args.get("mitre_defs_json"): - rule_data["mitre_defs"] = json.loads(mitre_defs_json) - reply = client.insert_correlation_rules(rule_data) + required_fields = [ + "dataset", "investigation_query_link", "user_defined_severity", + "simple_schedule", "suppression_duration", "suppression_fields", + "drilldown_query_timeframe", "timezone", "user_defined_category", "crontab" + ] + + for field in required_fields: + if field not in rule_data: + rule_data[field] = "" + + mitre_defs_json = args.get("mitre_defs_json") or "{}" + rule_data["mitre_defs"] = json.loads(mitre_defs_json) + + alert_fields_json = args.get("alert_fields") or "{}" + rule_data["alert_fields"] = json.loads(alert_fields_json) + + # If dataset is missing in args, send explicit empty string instead of letting it be removed + if "dataset" not in rule_data: + rule_data["dataset"] = "" + + reply = client.insert_correlation_rules({"request_data": [rule_data]}) return CommandResults( readable_output="Correlation rule created successfully.", outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CorrelationRule", @@ -1754,8 +1814,8 @@ def correlation_rule_update_command(client: Client, args: Dict) -> CommandResult alert_fields=args.get("alert_fields"), execution_mode=args.get("execution_mode"), search_window=args.get("search_window"), - schedule=args.get("schedule"), - schedule_linux=args.get("schedule_linux"), + simple_schedule=args.get("schedule"), + crontab=args.get("schedule_linux"), timezone=args.get("timezone"), suppression_enabled=argToBoolean(args.get("suppression_enabled")) if args.get("suppression_enabled") else None, suppression_duration=args.get("suppression_duration"), diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml index 3a5e2e874a71..9d8b8a295dfb 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml @@ -4185,7 +4185,7 @@ script: predefined: - 'true' - 'false' - - description: Dataset. + - description: Correlation rule dataset. name: dataset - description: Alert name. name: alert_name @@ -4226,10 +4226,9 @@ script: description: Whether the correlation rule is enabled. type: Boolean - arguments: - - description: Correlation rule ID. - name: rule_id - description: Correlation rule name. name: name + required: true - auto: PREDEFINED description: Correlation rule severity. name: severity @@ -4241,7 +4240,7 @@ script: - description: Correlation rule XQL query. name: xql_query - auto: PREDEFINED - description: Whether the correlation rule is enabled. + description: Whether the correlation rule is enabled or disabled. name: is_enabled predefined: - 'true' @@ -4275,43 +4274,43 @@ script: - description: Alert fields. name: alert_fields - auto: PREDEFINED - description: Execution mode. + description: Whether execution mode is scheduled or real time. name: execution_mode predefined: - scheduled - real_time - - description: Search window. + - description: Amount of time for search window. name: search_window - - description: Simple schedule. + - description: Correlation rule schedule. name: schedule - - description: Crontab schedule. + - description: Linux scheduling for correlation rule. name: schedule_linux - - description: Timezone. + - description: Correlation rule timezone. name: timezone - auto: PREDEFINED - description: Whether suppression is enabled. + description: Whether suppression is enabled for correlation rule. name: suppression_enabled predefined: - 'true' - 'false' - - description: Suppression duration. + - description: Duration of correlation rule suppression. name: suppression_duration - - description: Suppression fields. + - description: Suppration fields. name: suppression_fields - - description: Dataset. + - description: Correlation rule dataset. name: dataset - - description: User defined severity. + - description: User-defined severity. name: user_defined_severity - - description: User defined category. + - description: User-defined category. name: user_defined_category - - description: MITRE definitions JSON. + - description: MITRE definitions. name: mitre_defs_json - description: Investigation query link. name: investigation_query_link - - description: Drilldown query timeframe. + - description: Whether the drilldown query timeframe is query or alert. name: drilldown_query_timeframe - auto: PREDEFINED - description: Mapping strategy. + description: Whether the mapping strategy is auto or custom. name: mapping_strategy predefined: - auto @@ -4347,7 +4346,7 @@ script: - description: Correlation rule XQL query. name: xql_query - auto: PREDEFINED - description: Whether the correlation rule is enabled. + description: Whether the correlation rule is enabled or disabled. name: is_enabled predefined: - 'true' @@ -4381,43 +4380,43 @@ script: - description: Alert fields. name: alert_fields - auto: PREDEFINED - description: Execution mode. + description: Whether execution mode is scheduled or real time. name: execution_mode predefined: - scheduled - real_time - - description: Search window. + - description: Amount of time for search window. name: search_window - - description: Simple schedule. + - description: Correlation rule schedule. name: schedule - - description: Crontab schedule. + - description: Linux scheduling for correlation rule. name: schedule_linux - - description: Timezone. + - description: Correlation rule timezone. name: timezone - auto: PREDEFINED - description: Whether suppression is enabled. + description: Whether suppression is enabled for correlation rule. name: suppression_enabled predefined: - 'true' - 'false' - - description: Suppression duration. + - description: Duration of correlation rule suppression. name: suppression_duration - - description: Suppression fields. + - description: Suppration fields. name: suppression_fields - - description: Dataset. + - description: Correlation rule dataset. name: dataset - - description: User defined severity. + - description: User-defined severity. name: user_defined_severity - - description: User defined category. + - description: User-defined category. name: user_defined_category - - description: MITRE definitions JSON. + - description: MITRE definitions. name: mitre_defs_json - description: Investigation query link. name: investigation_query_link - - description: Drilldown query timeframe. + - description: Whether the drilldown query timeframe is query or alert. name: drilldown_query_timeframe - auto: PREDEFINED - description: Mapping strategy. + description: Whether the mapping strategy is auto or custom. name: mapping_strategy predefined: - auto From 7d6a16940aaeaa48bd58a8bbff9c510bd124981c Mon Sep 17 00:00:00 2001 From: noydavidi Date: Tue, 3 Feb 2026 11:41:34 +0200 Subject: [PATCH 07/27] changes --- Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml index 9d8b8a295dfb..2904384ab556 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml @@ -4191,8 +4191,7 @@ script: name: alert_name - description: Alert category. name: alert_category - - description: Alert fields. - isArray: true + - description: Alert fields. Can be a string or a dictionary. name: alert_fields - description: Alert domain. name: alert_domain @@ -4271,7 +4270,7 @@ script: - discovery - description: Alert description. name: alert_description - - description: Alert fields. + - description: Alert fields. Can be a string or a dictionary. name: alert_fields - auto: PREDEFINED description: Whether execution mode is scheduled or real time. @@ -4377,7 +4376,7 @@ script: - discovery - description: Alert description. name: alert_description - - description: Alert fields. + - description: Alert fields. Can be a string or a dictionary. name: alert_fields - auto: PREDEFINED description: Whether execution mode is scheduled or real time. From 32798916223e0eee83600439faaad50607ae3364 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Tue, 3 Feb 2026 14:37:23 +0200 Subject: [PATCH 08/27] checking cr commands --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 34 ++++++++++++------- .../Integrations/CortexXDRIR/CortexXDRIR.yml | 5 +++ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 6d639973d282..2c2022d3ef46 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -1737,17 +1737,17 @@ def correlation_rule_create_command(client: Client, args: Dict) -> CommandResult CommandResults: The command results. """ rule_data = assign_params( - name=args.get("name"), - severity=BIOC_AND_CR_SEVERITY_MAPPING.get(args.get("severity")), - xql_query=args.get("xql_query"), + name=args.get("name"), # required, can't be empty + severity=BIOC_AND_CR_SEVERITY_MAPPING.get(args.get("severity")), # required, can't be empty + xql_query=args.get("xql_query"), # required, can't be empty is_enabled=argToBoolean(args.get("is_enabled")) if args.get("is_enabled") else None, description=args.get("description"), alert_name=args.get("alert_name"), action="ALERTS", - alert_category=args.get("alert_category").upper() if args.get("alert_category") else None, + alert_category=args.get("alert_category").upper() if args.get("alert_category") else None, # required, can't be empty alert_description=args.get("alert_description"), alert_fields=args.get("alert_fields"), - execution_mode=args.get("execution_mode").upper() if args.get("execution_mode") else None, + execution_mode=args.get("execution_mode").upper() if args.get("execution_mode") else None, # required, can't be empty search_window=args.get("search_window"), crontab=args.get("schedule_linux"), timezone=args.get("timezone"), @@ -1760,13 +1760,25 @@ def correlation_rule_create_command(client: Client, args: Dict) -> CommandResult user_defined_category=args.get("user_defined_category"), investigation_query_link=args.get("investigation_query_link"), drilldown_query_timeframe=args.get("drilldown_query_timeframe"), - mapping_strategy=args.get("mapping_strategy").upper() if args.get("mapping_strategy") else None, + mapping_strategy=args.get("mapping_strategy").upper() if args.get("mapping_strategy") else None, # required, can't be empty ) required_fields = [ - "dataset", "investigation_query_link", "user_defined_severity", - "simple_schedule", "suppression_duration", "suppression_fields", - "drilldown_query_timeframe", "timezone", "user_defined_category", "crontab" + "investigation_query_link", + # "user_defined_severity", + "alert_description", + "is_enabled", + "suppression_enabled", + "search_window", + "simple_schedule", + "suppression_duration", + "suppression_fields", + "description", + "alert_name", + "drilldown_query_timeframe", + "timezone", + "user_defined_category", + # "crontab" ] for field in required_fields: @@ -1779,10 +1791,6 @@ def correlation_rule_create_command(client: Client, args: Dict) -> CommandResult alert_fields_json = args.get("alert_fields") or "{}" rule_data["alert_fields"] = json.loads(alert_fields_json) - # If dataset is missing in args, send explicit empty string instead of letting it be removed - if "dataset" not in rule_data: - rule_data["dataset"] = "" - reply = client.insert_correlation_rules({"request_data": [rule_data]}) return CommandResults( readable_output="Correlation rule created successfully.", diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml index 2904384ab556..a4926a779566 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml @@ -4231,6 +4231,7 @@ script: - auto: PREDEFINED description: Correlation rule severity. name: severity + required: true predefined: - info - low @@ -4238,6 +4239,7 @@ script: - high - description: Correlation rule XQL query. name: xql_query + required: true - auto: PREDEFINED description: Whether the correlation rule is enabled or disabled. name: is_enabled @@ -4251,6 +4253,7 @@ script: - auto: PREDEFINED description: Alert category. name: alert_category + required: true predefined: - other - persistence @@ -4275,6 +4278,7 @@ script: - auto: PREDEFINED description: Whether execution mode is scheduled or real time. name: execution_mode + required: true predefined: - scheduled - real_time @@ -4311,6 +4315,7 @@ script: - auto: PREDEFINED description: Whether the mapping strategy is auto or custom. name: mapping_strategy + required: true predefined: - auto - custom From 9c4abdf2a6ee908b54c807e275f3aa99a5116c00 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Tue, 3 Feb 2026 17:22:35 +0200 Subject: [PATCH 09/27] tested all commands --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 209 ++++++++++-------- .../Integrations/CortexXDRIR/CortexXDRIR.yml | 131 ++++++----- 2 files changed, 186 insertions(+), 154 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 2c2022d3ef46..bb967c8e7b06 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -549,9 +549,9 @@ def get_correlation_rules(self, request_data: dict): url_suffix="/correlations/get/", json_data=request_data, ) - return reply.get("reply", {}) + return reply - def insert_correlation_rules(self, request_data: dict): + def create_or_update_correlation_rules(self, request_data: dict): reply = self._http_request( method="POST", url_suffix="/correlations/insert", @@ -565,7 +565,7 @@ def delete_correlation_rules(self, request_data: dict): url_suffix="/correlations/delete", json_data={"request_data": request_data}, ) - return reply.get("reply", {}) + return reply def extract_paths_and_names(paths: list) -> tuple: @@ -1713,7 +1713,7 @@ def correlation_rule_list_command(client: Client, args: Dict) -> CommandResults: rules = reply.get("objects", []) readable_output = tableToMarkdown(name="Correlation Rules List", t=rules, - headers=["id", "name", "description", "is_enabled"], + headers=["rule_id", "name", "description", "is_enabled"], removeNull=True, headerTransform=string_to_table_header, ) @@ -1736,66 +1736,51 @@ def correlation_rule_create_command(client: Client, args: Dict) -> CommandResult Returns: CommandResults: The command results. """ - rule_data = assign_params( - name=args.get("name"), # required, can't be empty - severity=BIOC_AND_CR_SEVERITY_MAPPING.get(args.get("severity")), # required, can't be empty - xql_query=args.get("xql_query"), # required, can't be empty - is_enabled=argToBoolean(args.get("is_enabled")) if args.get("is_enabled") else None, - description=args.get("description"), - alert_name=args.get("alert_name"), - action="ALERTS", - alert_category=args.get("alert_category").upper() if args.get("alert_category") else None, # required, can't be empty - alert_description=args.get("alert_description"), - alert_fields=args.get("alert_fields"), - execution_mode=args.get("execution_mode").upper() if args.get("execution_mode") else None, # required, can't be empty - search_window=args.get("search_window"), - crontab=args.get("schedule_linux"), - timezone=args.get("timezone"), - simple_schedule=args.get("schedule"), - suppression_enabled=argToBoolean(args.get("suppression_enabled")) if args.get("suppression_enabled") else None, - suppression_duration=args.get("suppression_duration"), - suppression_fields=args.get("suppression_fields"), - dataset=args.get("dataset"), - user_defined_severity=args.get("user_defined_severity"), - user_defined_category=args.get("user_defined_category"), - investigation_query_link=args.get("investigation_query_link"), - drilldown_query_timeframe=args.get("drilldown_query_timeframe"), - mapping_strategy=args.get("mapping_strategy").upper() if args.get("mapping_strategy") else None, # required, can't be empty - ) + rule_data = { + # Required fields + "name": args.get("name"), + "severity": BIOC_AND_CR_SEVERITY_MAPPING.get(args.get("severity")), + "xql_query": args.get("xql_query"), + "is_enabled": argToBoolean(args.get("is_enabled")), + "action": "ALERTS", + "timezone": args.get("timezone"), + "dataset": args.get("dataset"), + + # Uppercase handling for Enum fields + "alert_category": args.get("alert_category").upper() if args.get("alert_category") else None, + "execution_mode": args.get("execution_mode").upper() if args.get("execution_mode") else None, + "mapping_strategy": args.get("mapping_strategy").upper() if args.get("mapping_strategy") else None, + + # Use 'or None' to convert empty strings ("") into JSON null. We must put a value for those fields. + "description": args.get("description") or None, + "alert_name": args.get("alert_name") or None, + "alert_description": args.get("alert_description") or None, + "search_window": args.get("search_window") or None, + "crontab": args.get("schedule_linux") or None, + "simple_schedule": args.get("schedule") or None, + "drilldown_query_timeframe": args.get("drilldown_query_timeframe") or None, + "investigation_query_link": args.get("investigation_query_link") or None, + "suppression_enabled": args.get("suppression_enabled") or None, + "suppression_duration": args.get("suppression_duration") or None, + "suppression_fields": args.get("suppression_fields") or None, + + # User Defined Severity (Must be None unless Severity is "User Defined") + "user_defined_severity": args.get("user_defined_severity") or None, + "user_defined_category": args.get("user_defined_category") or None, + + # Defaults to "{}" string if missing so json.loads doesn't fail + "mitre_defs": json.loads(args.get("mitre_defs_json") or "{}"), + "alert_fields": json.loads(args.get("alert_fields") or "{}") + } - required_fields = [ - "investigation_query_link", - # "user_defined_severity", - "alert_description", - "is_enabled", - "suppression_enabled", - "search_window", - "simple_schedule", - "suppression_duration", - "suppression_fields", - "description", - "alert_name", - "drilldown_query_timeframe", - "timezone", - "user_defined_category", - # "crontab" - ] - - for field in required_fields: - if field not in rule_data: - rule_data[field] = "" - - mitre_defs_json = args.get("mitre_defs_json") or "{}" - rule_data["mitre_defs"] = json.loads(mitre_defs_json) - - alert_fields_json = args.get("alert_fields") or "{}" - rule_data["alert_fields"] = json.loads(alert_fields_json) - - reply = client.insert_correlation_rules({"request_data": [rule_data]}) + reply = client.create_or_update_correlation_rules({"request_data": [rule_data]}) + updated_objects = reply.get("added_objects")[0] if reply.get("added_objects") else {} + rule_id = updated_objects.get("id") + status = updated_objects.get("status") return CommandResults( - readable_output="Correlation rule created successfully.", + readable_output=status, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CorrelationRule", - outputs={"id": reply.get("id")}, + outputs={"rule_id": rule_id}, raw_response=reply, ) @@ -1809,40 +1794,56 @@ def correlation_rule_update_command(client: Client, args: Dict) -> CommandResult Returns: CommandResults: The command results. """ - rule_data = assign_params( - rule_id=args.get("rule_id"), - name=args.get("name"), - severity=args.get("severity"), - xql_query=args.get("xql_query"), - is_enabled=argToBoolean(args.get("is_enabled")) if args.get("is_enabled") else None, - description=args.get("description"), - alert_name=args.get("alert_name"), - alert_category=args.get("alert_category"), - alert_description=args.get("alert_description"), - alert_fields=args.get("alert_fields"), - execution_mode=args.get("execution_mode"), - search_window=args.get("search_window"), - simple_schedule=args.get("schedule"), - crontab=args.get("schedule_linux"), - timezone=args.get("timezone"), - suppression_enabled=argToBoolean(args.get("suppression_enabled")) if args.get("suppression_enabled") else None, - suppression_duration=args.get("suppression_duration"), - suppression_fields=args.get("suppression_fields"), - dataset=args.get("dataset"), - user_defined_severity=args.get("user_defined_severity"), - user_defined_category=args.get("user_defined_category"), - investigation_query_link=args.get("investigation_query_link"), - drilldown_query_timeframe=args.get("drilldown_query_timeframe"), - mapping_strategy=args.get("mapping_strategy"), - ) - if mitre_defs_json := args.get("mitre_defs_json"): - rule_data["mitre_defs"] = json.loads(mitre_defs_json) + severity_arg = args.get("severity") + severity_mapped = BIOC_AND_CR_SEVERITY_MAPPING.get(severity_arg) if severity_arg else None + + rule_data = { + # Required fields + "rule_id": args.get("rule_id"), + "name": args.get("name"), + "severity": severity_mapped, + "xql_query": args.get("xql_query"), + "is_enabled": argToBoolean(args.get("is_enabled")), + "action": "ALERTS", + "timezone": args.get("timezone"), + "dataset": args.get("dataset"), + + # Enums - Only .upper() if value exists + "alert_category": args.get("alert_category").upper() if args.get("alert_category") else None, + "execution_mode": args.get("execution_mode").upper() if args.get("execution_mode") else None, + "mapping_strategy": args.get("mapping_strategy").upper() if args.get("mapping_strategy") else None, + + # Optional fields + "description": args.get("description") or None, + "alert_name": args.get("alert_name") or None, + "alert_description": args.get("alert_description") or None, + "search_window": args.get("search_window") or None, + "crontab": args.get("schedule_linux") or None, + "simple_schedule": args.get("schedule") or None, + "drilldown_query_timeframe": args.get("drilldown_query_timeframe") or None, + "investigation_query_link": args.get("investigation_query_link") or None, + "suppression_enabled": argToBoolean(args.get("suppression_enabled")) if args.get("suppression_enabled") is not None else None, + "suppression_duration": args.get("suppression_duration") or None, + "suppression_fields": args.get("suppression_fields") or None, + + # User Defined Severity (Must be None unless Severity is "User Defined") + "user_defined_severity": args.get("user_defined_severity") or None, + "user_defined_category": args.get("user_defined_category") or None, + + # Defaults to "{}" string if missing so json.loads doesn't fail + "mitre_defs": json.loads(args.get("mitre_defs_json") or "{}"), + "alert_fields": json.loads(args.get("alert_fields") or "{}") + } + + reply = client.create_or_update_correlation_rules({"request_data": [rule_data]}) + updated_objects = reply.get("updated_objects")[0] if reply.get("updated_objects") else {} + rule_id = updated_objects.get("id") + status = updated_objects.get("status") - reply = client.insert_correlation_rules(rule_data) return CommandResults( - readable_output="Correlation rule created successfully.", + readable_output=status, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CorrelationRule", - outputs={"id": reply.get("id")}, + outputs={"rule_id": rule_id}, raw_response=reply, ) @@ -1857,8 +1858,28 @@ def correlation_rule_delete_command(client: Client, args: Dict) -> CommandResult str: Success message. """ rule_ids = argToList(args.get("rule_id")) - client.delete_correlation_rules({"rule_id_list": rule_ids}) - return CommandResults(readable_output="Correlation rule deleted successfully") + rule_id_list = [] + for rule_id in rule_ids: + try: + rule_id_list.append({"field": "rule_id", "operator": "EQ", "value": int(rule_id)}) + except (ValueError, TypeError): + # If rule_id is None, "abc", or "", skip it safely + continue + + reply = client.delete_correlation_rules({"filters": rule_id_list}) + deleted_ids = reply.get("objects", []) + objects_count = reply.get("objects_count") + + if objects_count == 0: + status = "Could not find any correlation rules to delete." + elif objects_count == 1: + status = f"Correlation Rule {deleted_ids[0]} was deleted." + else: + ids_str = ", ".join(map(str, deleted_ids)) + status = f"Correlation Rules {ids_str} were deleted." + + return CommandResults(readable_output=status, + raw_response=reply) def main(): # pragma: no cover diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml index a4926a779566..0ec3252f9912 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml @@ -4225,11 +4225,11 @@ script: description: Whether the correlation rule is enabled. type: Boolean - arguments: - - description: Correlation rule name. + - description: 'The correlation rule name. Example: name="This is a CR test 6"' name: name required: true - auto: PREDEFINED - description: Correlation rule severity. + description: 'The correlation rule severity. Example: severity=low' name: severity required: true predefined: @@ -4237,21 +4237,18 @@ script: - low - medium - high - - description: Correlation rule XQL query. + - description: 'The correlation rule XQL query. Example: xql_query="dataset = xdr_data | limit 1"' name: xql_query required: true - auto: PREDEFINED - description: Whether the correlation rule is enabled or disabled. + description: 'Whether the rule is enabled. Example: is_enabled=true' name: is_enabled + required: true predefined: - 'true' - 'false' - - description: Correlation rule description. - name: description - - description: Alert name. - name: alert_name - auto: PREDEFINED - description: Alert category. + description: 'The alert category. Example: alert_category=dropper' name: alert_category required: true predefined: @@ -4271,36 +4268,48 @@ script: - file_privilege_manipulation - reconnaissance - discovery - - description: Alert description. - name: alert_description - - description: Alert fields. Can be a string or a dictionary. - name: alert_fields - auto: PREDEFINED - description: Whether execution mode is scheduled or real time. + description: 'The rule execution mode. Example: execution_mode=scheduled' name: execution_mode required: true predefined: - scheduled - real_time - - description: Amount of time for search window. + - description: 'The correlation rule timezone. Example: timezone="Asia/Jerusalem"' + name: timezone + required: true + - auto: PREDEFINED + description: 'The rule mapping strategy. Example: mapping_strategy=auto' + name: mapping_strategy + required: true + predefined: + - auto + - custom + - description: The correlation rule description. + name: description + - description: The alert name. + name: alert_name + - description: The alert description. + name: alert_description + - description: Alert fields (string or dictionary). + name: alert_fields + - description: 'The search window timeframe. Example: search_window="1 hours"' name: search_window - - description: Correlation rule schedule. + - description: 'The correlation rule schedule. Example: schedule="10 minutes"' name: schedule - - description: Linux scheduling for correlation rule. + - description: 'Linux scheduling for the rule. Example: schedule_linux="*/10 * * * *"' name: schedule_linux - - description: Correlation rule timezone. - name: timezone - auto: PREDEFINED - description: Whether suppression is enabled for correlation rule. + description: Whether suppression is enabled. name: suppression_enabled predefined: - 'true' - 'false' - description: Duration of correlation rule suppression. name: suppression_duration - - description: Suppration fields. + - description: Suppression fields. name: suppression_fields - - description: Correlation rule dataset. + - description: 'The correlation rule dataset. Example: dataset=alerts' name: dataset - description: User-defined severity. name: user_defined_severity @@ -4310,15 +4319,8 @@ script: name: mitre_defs_json - description: Investigation query link. name: investigation_query_link - - description: Whether the drilldown query timeframe is query or alert. + - description: 'The drilldown query timeframe. Example: drilldown_query_timeframe="ALERT"' name: drilldown_query_timeframe - - auto: PREDEFINED - description: Whether the mapping strategy is auto or custom. - name: mapping_strategy - required: true - predefined: - - auto - - custom description: Creates a new correlation rule. name: xdr-correlation-rule-create outputs: @@ -4337,31 +4339,33 @@ script: - arguments: - description: Correlation rule ID. name: rule_id - - description: Correlation rule name. + required: true + - description: 'The correlation rule name. Example: name="This is a CR test 6"' name: name + required: true - auto: PREDEFINED - description: Correlation rule severity. + description: 'The correlation rule severity. Example: severity=low' name: severity + required: true predefined: - info - low - medium - high - - description: Correlation rule XQL query. + - description: 'The correlation rule XQL query. Example: xql_query="dataset = xdr_data | limit 1"' name: xql_query + required: true - auto: PREDEFINED - description: Whether the correlation rule is enabled or disabled. + description: 'Whether the rule is enabled. Example: is_enabled=true' name: is_enabled + required: true predefined: - 'true' - 'false' - - description: Correlation rule description. - name: description - - description: Alert name. - name: alert_name - auto: PREDEFINED - description: Alert category. + description: 'The alert category. Example: alert_category=dropper' name: alert_category + required: true predefined: - other - persistence @@ -4379,36 +4383,49 @@ script: - file_privilege_manipulation - reconnaissance - discovery - - description: Alert description. - name: alert_description - - description: Alert fields. Can be a string or a dictionary. - name: alert_fields - - auto: PREDEFINED - description: Whether execution mode is scheduled or real time. + - description: 'The rule execution mode. Example: execution_mode=scheduled' name: execution_mode + required: true predefined: - scheduled - real_time - - description: Amount of time for search window. + - description: 'The correlation rule timezone. Example: timezone="Asia/Jerusalem"' + name: timezone + required: true + - auto: PREDEFINED + description: 'The rule mapping strategy. Example: mapping_strategy=auto' + name: mapping_strategy + required: true + predefined: + - auto + - custom + - description: The correlation rule description. + name: description + - description: The alert name. + name: alert_name + - description: The alert description. + name: alert_description + - description: Alert fields (string or dictionary). + name: alert_fields + - description: 'The search window timeframe. Example: search_window="1 hours"' name: search_window - - description: Correlation rule schedule. + - description: 'The correlation rule schedule. Example: schedule="10 minutes"' name: schedule - - description: Linux scheduling for correlation rule. + - description: 'Linux scheduling for the rule. Example: schedule_linux="*/10 * * * *"' name: schedule_linux - - description: Correlation rule timezone. - name: timezone - auto: PREDEFINED - description: Whether suppression is enabled for correlation rule. + description: Whether suppression is enabled. name: suppression_enabled predefined: - 'true' - 'false' - description: Duration of correlation rule suppression. name: suppression_duration - - description: Suppration fields. + - description: Suppression fields. name: suppression_fields - - description: Correlation rule dataset. + - description: 'The correlation rule dataset. Example: dataset=alerts' name: dataset + required: true - description: User-defined severity. name: user_defined_severity - description: User-defined category. @@ -4417,14 +4434,8 @@ script: name: mitre_defs_json - description: Investigation query link. name: investigation_query_link - - description: Whether the drilldown query timeframe is query or alert. + - description: 'The drilldown query timeframe. Example: drilldown_query_timeframe="ALERT"' name: drilldown_query_timeframe - - auto: PREDEFINED - description: Whether the mapping strategy is auto or custom. - name: mapping_strategy - predefined: - - auto - - custom description: Updates an existing correlation rule. name: xdr-correlation-rule-update outputs: From 47d9a9a7fbd6e21f103fc68b9a340516e03a0263 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Wed, 4 Feb 2026 11:07:59 +0200 Subject: [PATCH 10/27] tested all commands --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 46 +++++++++++-------- .../Integrations/CortexXDRIR/CortexXDRIR.yml | 4 -- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index bb967c8e7b06..28900d7c18db 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -1587,17 +1587,29 @@ def bioc_list_command(client: Client, args: Dict) -> CommandResults: def bioc_create_or_update_helper(client: Client, args: Dict) -> dict: - bioc_data = assign_params( - rule_id=args.get("rule_id"), # for update only - name=args.get("name"), - severity=BIOC_AND_CR_SEVERITY_MAPPING.get(args.get("severity")), - type=args.get("type"), - is_xql=argToBoolean(args.get("is_xql", False)), - comment=args.get("comment"), - status=args.get("status"), - ) - - indicator = args.get("indicator") + """ + Creates or updates BIOC indicators based on provided arguments. + Args: + client (Client): The client to use. + args (Dict): The command arguments containing rule configuration. + Returns: + dict: The raw response from the client containing the update/creation results. + """ + bioc_data = { + # required for updating + "rule_id": args.get("rule_id"), + # required fields + "name": args.get("name"), + "severity": BIOC_AND_CR_SEVERITY_MAPPING.get(args.get("severity")), + # not required but need to be null + "type": args.get("type"), + "is_xql": argToBoolean(args.get("is_xql")) if args.get("is_xql") else None, + "comment": args.get("comment"), + "status": args.get("status"), + "mitre_technique_id_and_name": args.get("mitre_technique_id_and_name") or [], + "mitre_tactic_id_and_name": args.get("mitre_tactic_id_and_name") or [], + } + indicator = args.get("indicator") # required if indicator: try: indicator = json.loads(indicator) @@ -1605,10 +1617,6 @@ def bioc_create_or_update_helper(client: Client, args: Dict) -> dict: except ValueError: raise DemistoException("Unable to parse 'indicator'. Please use the JSON format.") - # required fields but can be empty - bioc_data["mitre_technique_id_and_name"] = argToList(args.get("mitre_technique_id_and_name")) or [] - bioc_data["mitre_tactic_id_and_name"] = argToList(args.get("mitre_tactic_id_and_name")) or [] - return client.insert_or_update_biocs({"request_data": [bioc_data]}) @@ -1776,9 +1784,9 @@ def correlation_rule_create_command(client: Client, args: Dict) -> CommandResult reply = client.create_or_update_correlation_rules({"request_data": [rule_data]}) updated_objects = reply.get("added_objects")[0] if reply.get("added_objects") else {} rule_id = updated_objects.get("id") - status = updated_objects.get("status") + message = updated_objects.get("status") return CommandResults( - readable_output=status, + readable_output=message, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CorrelationRule", outputs={"rule_id": rule_id}, raw_response=reply, @@ -1838,10 +1846,10 @@ def correlation_rule_update_command(client: Client, args: Dict) -> CommandResult reply = client.create_or_update_correlation_rules({"request_data": [rule_data]}) updated_objects = reply.get("updated_objects")[0] if reply.get("updated_objects") else {} rule_id = updated_objects.get("id") - status = updated_objects.get("status") + message = updated_objects.get("status") return CommandResults( - readable_output=status, + readable_output=message, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CorrelationRule", outputs={"rule_id": rule_id}, raw_response=reply, diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml index 0ec3252f9912..b26586f9bc0b 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml @@ -3973,7 +3973,6 @@ script: - auto: PREDEFINED description: The BIOC type. name: type - required: true predefined: - other - persistence @@ -3999,11 +3998,9 @@ script: - 'false' - description: The BIOC comment. name: comment - required: true - auto: PREDEFINED description: The BIOC status. name: status - required: true predefined: - enabled - disabled @@ -4013,7 +4010,6 @@ script: For more information please refer to the documentation https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-BIOCs. isArray: true name: indicator - required: true - description: "The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']." isArray: true name: mitre_technique_id_and_name From 17590a09ff4f99425b9a3e73c6f8a8037c4248f6 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Wed, 4 Feb 2026 11:16:49 +0200 Subject: [PATCH 11/27] removed comment --- Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 37eff71bedc0..e030d240d857 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -2349,7 +2349,6 @@ def update_asset_group_command(client: Client, args: Dict) -> CommandResults: client.update_asset_group(group_id, request_data=request_data) return CommandResults(readable_output="Asset group updated successfully") ->>>>>>> master def main(): # pragma: no cover From f9c4573c8055da4bb0ea56759edad6945eb07570 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Wed, 4 Feb 2026 11:19:35 +0200 Subject: [PATCH 12/27] added an helper function --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 82 +++++++------------ 1 file changed, 29 insertions(+), 53 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index e030d240d857..9e4a3ba2eb4c 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -1847,20 +1847,23 @@ def correlation_rule_list_command(client: Client, args: Dict) -> CommandResults: ) -def correlation_rule_create_command(client: Client, args: Dict) -> CommandResults: +def correlation_rule_create_or_update_helper(client: Client, args: Dict) -> dict: """ - API Docs https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-Correlation-Rules - Creates a new correlation rule. + Creates or updates correlation rules based on provided arguments. Args: client (Client): The client to use. - args (Dict): The command arguments. + args (Dict): The command arguments containing rule configuration. Returns: - CommandResults: The command results. + dict: The raw response from the client containing the update/creation results. """ + severity_arg = args.get("severity") + severity_mapped = BIOC_AND_CR_SEVERITY_MAPPING.get(severity_arg) if severity_arg else None + rule_data = { # Required fields + "rule_id": args.get("rule_id"), "name": args.get("name"), - "severity": BIOC_AND_CR_SEVERITY_MAPPING.get(args.get("severity")), + "severity": severity_mapped, "xql_query": args.get("xql_query"), "is_enabled": argToBoolean(args.get("is_enabled")), "action": "ALERTS", @@ -1881,7 +1884,7 @@ def correlation_rule_create_command(client: Client, args: Dict) -> CommandResult "simple_schedule": args.get("schedule") or None, "drilldown_query_timeframe": args.get("drilldown_query_timeframe") or None, "investigation_query_link": args.get("investigation_query_link") or None, - "suppression_enabled": args.get("suppression_enabled") or None, + "suppression_enabled": argToBoolean(args.get("suppression_enabled")) if args.get("suppression_enabled") else None, "suppression_duration": args.get("suppression_duration") or None, "suppression_fields": args.get("suppression_fields") or None, @@ -1894,10 +1897,23 @@ def correlation_rule_create_command(client: Client, args: Dict) -> CommandResult "alert_fields": json.loads(args.get("alert_fields") or "{}") } - reply = client.create_or_update_correlation_rules({"request_data": [rule_data]}) - updated_objects = reply.get("added_objects")[0] if reply.get("added_objects") else {} - rule_id = updated_objects.get("id") - message = updated_objects.get("status") + return client.create_or_update_correlation_rules({"request_data": [rule_data]}) + + +def correlation_rule_create_command(client: Client, args: Dict) -> CommandResults: + """ + API Docs https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-Correlation-Rules + Creates a new correlation rule. + Args: + client (Client): The client to use. + args (Dict): The command arguments. + Returns: + CommandResults: The command results. + """ + reply = correlation_rule_create_or_update_helper(client, args) + added_objects = reply.get("added_objects")[0] if reply.get("added_objects") else {} + rule_id = added_objects.get("id") + message = added_objects.get("status") return CommandResults( readable_output=message, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CorrelationRule", @@ -1915,48 +1931,7 @@ def correlation_rule_update_command(client: Client, args: Dict) -> CommandResult Returns: CommandResults: The command results. """ - severity_arg = args.get("severity") - severity_mapped = BIOC_AND_CR_SEVERITY_MAPPING.get(severity_arg) if severity_arg else None - - rule_data = { - # Required fields - "rule_id": args.get("rule_id"), - "name": args.get("name"), - "severity": severity_mapped, - "xql_query": args.get("xql_query"), - "is_enabled": argToBoolean(args.get("is_enabled")), - "action": "ALERTS", - "timezone": args.get("timezone"), - "dataset": args.get("dataset"), - - # Enums - Only .upper() if value exists - "alert_category": args.get("alert_category").upper() if args.get("alert_category") else None, - "execution_mode": args.get("execution_mode").upper() if args.get("execution_mode") else None, - "mapping_strategy": args.get("mapping_strategy").upper() if args.get("mapping_strategy") else None, - - # Optional fields - "description": args.get("description") or None, - "alert_name": args.get("alert_name") or None, - "alert_description": args.get("alert_description") or None, - "search_window": args.get("search_window") or None, - "crontab": args.get("schedule_linux") or None, - "simple_schedule": args.get("schedule") or None, - "drilldown_query_timeframe": args.get("drilldown_query_timeframe") or None, - "investigation_query_link": args.get("investigation_query_link") or None, - "suppression_enabled": argToBoolean(args.get("suppression_enabled")) if args.get("suppression_enabled") is not None else None, - "suppression_duration": args.get("suppression_duration") or None, - "suppression_fields": args.get("suppression_fields") or None, - - # User Defined Severity (Must be None unless Severity is "User Defined") - "user_defined_severity": args.get("user_defined_severity") or None, - "user_defined_category": args.get("user_defined_category") or None, - - # Defaults to "{}" string if missing so json.loads doesn't fail - "mitre_defs": json.loads(args.get("mitre_defs_json") or "{}"), - "alert_fields": json.loads(args.get("alert_fields") or "{}") - } - - reply = client.create_or_update_correlation_rules({"request_data": [rule_data]}) + reply = correlation_rule_create_or_update_helper(client, args) updated_objects = reply.get("updated_objects")[0] if reply.get("updated_objects") else {} rule_id = updated_objects.get("id") message = updated_objects.get("status") @@ -2002,6 +1977,7 @@ def correlation_rule_delete_command(client: Client, args: Dict) -> CommandResult return CommandResults(readable_output=status, raw_response=reply) + def api_key_list_command(client: Client, args: Dict[str, Any]) -> CommandResults: """ API Docs: https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Get-existing-API-keys From bd37f981f10fc4d08604c1d6710ab4650d259e08 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Wed, 4 Feb 2026 11:25:47 +0200 Subject: [PATCH 13/27] added unittests and descriptions --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 2 + .../CortexXDRIR/CortexXDRIR_test.py | 166 ++++++++++++++++++ 2 files changed, 168 insertions(+) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 9e4a3ba2eb4c..235277838b39 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -1924,6 +1924,7 @@ def correlation_rule_create_command(client: Client, args: Dict) -> CommandResult def correlation_rule_update_command(client: Client, args: Dict) -> CommandResults: """ + API Docs https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-Correlation-Rules Updates an existing correlation rule. Args: client (Client): The client to use. @@ -1946,6 +1947,7 @@ def correlation_rule_update_command(client: Client, args: Dict) -> CommandResult def correlation_rule_delete_command(client: Client, args: Dict) -> CommandResults: """ + API Docs https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Delete-Correlation-Rules Deletes correlation rules. Args: client (Client): The client to use. diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py index c019b9c6ef0d..82ed2f7100cb 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py @@ -2785,3 +2785,169 @@ def test_replace_dots_in_keys(): # Test with non-dict/list input assert replace_dots_in_keys("string.with.dots") == "string.with.dots" assert replace_dots_in_keys(123) == 123 + + +def test_bioc_list_command(mocker): + """ + Given: + - Arguments for filtering BIOCs. + When: + - Running bioc_list_command. + Then: + - Verify the client.get_biocs is called and results are correct. + """ + from CortexXDRIR import Client, bioc_list_command + client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) + mock_reply = {"objects": [{"rule_id": "1", "name": "test_bioc", "type": "host", "severity": "high", "status": "enabled"}]} + mocker.patch.object(Client, "get_biocs", return_value=mock_reply) + + args = {"name": "test_bioc", "severity": "high"} + res = bioc_list_command(client, args) + + assert res.outputs == mock_reply["objects"] + assert "test_bioc" in res.readable_output + + +def test_bioc_create_command(mocker): + """ + Given: + - Arguments for creating a BIOC. + When: + - Running bioc_create_command. + Then: + - Verify the client.insert_or_update_biocs is called and results are correct. + """ + from CortexXDRIR import Client, bioc_create_command + client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) + mock_reply = {"added_objects": [{"id": "1", "status": "BIOC created successfully"}]} + mocker.patch.object(Client, "insert_or_update_biocs", return_value=mock_reply) + + args = {"name": "test_bioc", "severity": "high", "indicator": '{"field": "value"}'} + res = bioc_create_command(client, args) + + assert res.outputs == {"rule_id": "1"} + assert res.readable_output == "BIOC created successfully" + + +def test_bioc_update_command(mocker): + """ + Given: + - Arguments for updating a BIOC. + When: + - Running bioc_update_command. + Then: + - Verify the client.insert_or_update_biocs is called and results are correct. + """ + from CortexXDRIR import Client, bioc_update_command + client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) + mock_reply = {"updated_objects": [{"id": "1", "status": "BIOC updated successfully"}]} + mocker.patch.object(Client, "insert_or_update_biocs", return_value=mock_reply) + + args = {"rule_id": "1", "name": "test_bioc", "severity": "high"} + res = bioc_update_command(client, args) + + assert res.outputs == {"rule_id": "1"} + assert res.readable_output == "BIOC updated successfully" + + +def test_bioc_delete_command(mocker): + """ + Given: + - Arguments for deleting a BIOC. + When: + - Running bioc_delete_command. + Then: + - Verify the client.delete_biocs is called and results are correct. + """ + from CortexXDRIR import Client, bioc_delete_command + client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) + mock_reply = {"objects": ["1"]} + mocker.patch.object(Client, "delete_biocs", return_value=mock_reply) + + args = {"name": "test_bioc"} + res = bioc_delete_command(client, args) + + assert res == "BIOC with id 1 deleted successfully." + + +def test_correlation_rule_list_command(mocker): + """ + Given: + - Arguments for filtering correlation rules. + When: + - Running correlation_rule_list_command. + Then: + - Verify the client.get_correlation_rules is called and results are correct. + """ + from CortexXDRIR import Client, correlation_rule_list_command + client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) + mock_reply = {"objects": [{"rule_id": "1", "name": "test_rule", "description": "desc", "is_enabled": True}]} + mocker.patch.object(Client, "get_correlation_rules", return_value=mock_reply) + + args = {"name": "test_rule"} + res = correlation_rule_list_command(client, args) + + assert res.outputs == mock_reply["objects"] + assert "test_rule" in res.readable_output + + +def test_correlation_rule_create_command(mocker): + """ + Given: + - Arguments for creating a correlation rule. + When: + - Running correlation_rule_create_command. + Then: + - Verify the client.create_or_update_correlation_rules is called and results are correct. + """ + from CortexXDRIR import Client, correlation_rule_create_command + client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) + mock_reply = {"added_objects": [{"id": "1", "status": "Rule created successfully"}]} + mocker.patch.object(Client, "create_or_update_correlation_rules", return_value=mock_reply) + + args = {"name": "test_rule", "severity": "high", "xql_query": "dataset = xdr_data", "is_enabled": "true"} + res = correlation_rule_create_command(client, args) + + assert res.outputs == {"rule_id": "1"} + assert res.readable_output == "Rule created successfully" + + +def test_correlation_rule_update_command(mocker): + """ + Given: + - Arguments for updating a correlation rule. + When: + - Running correlation_rule_update_command. + Then: + - Verify the client.create_or_update_correlation_rules is called and results are correct. + """ + from CortexXDRIR import Client, correlation_rule_update_command + client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) + mock_reply = {"updated_objects": [{"id": "1", "status": "Rule updated successfully"}]} + mocker.patch.object(Client, "create_or_update_correlation_rules", return_value=mock_reply) + + args = {"rule_id": "1", "name": "test_rule", "severity": "high"} + res = correlation_rule_update_command(client, args) + + assert res.outputs == {"rule_id": "1"} + assert res.readable_output == "Rule updated successfully" + + +def test_correlation_rule_delete_command(mocker): + """ + Given: + - Arguments for deleting a correlation rule. + When: + - Running correlation_rule_delete_command. + Then: + - Verify the client.delete_correlation_rules is called and results are correct. + """ + from CortexXDRIR import Client, correlation_rule_delete_command + client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) + mock_reply = {"objects": ["1"], "objects_count": 1} + mocker.patch.object(Client, "delete_correlation_rules", return_value=mock_reply) + + args = {"rule_id": "1"} + res = correlation_rule_delete_command(client, args) + + assert res.readable_output == "Correlation Rule 1 was deleted." From 39e4bbac82617db7f88266825a0b134cd0aa59fb Mon Sep 17 00:00:00 2001 From: noydavidi Date: Wed, 4 Feb 2026 11:28:02 +0200 Subject: [PATCH 14/27] added rn --- Packs/CortexXDR/ReleaseNotes/6_3_5.md | 13 +++++++++++++ Packs/CortexXDR/pack_metadata.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 Packs/CortexXDR/ReleaseNotes/6_3_5.md diff --git a/Packs/CortexXDR/ReleaseNotes/6_3_5.md b/Packs/CortexXDR/ReleaseNotes/6_3_5.md new file mode 100644 index 000000000000..9b18ea6cd9cf --- /dev/null +++ b/Packs/CortexXDR/ReleaseNotes/6_3_5.md @@ -0,0 +1,13 @@ + +#### Integrations + +##### Palo Alto Networks Cortex XDR - Investigation and Response + +- Added support for the **xdr-bioc-create** command which creates a new BIOC. +- Added support for the **xdr-bioc-update** command which updates an existing BIOC. +- Added support for the **xdr-bioc-delete** command which deletes a BIOC. +- Added support for the **xdr-bioc-list** command which returns a list of BIOCs. +- Added support for the **xdr-correlation-rule-list** command which returns a list of correlation rules. +- Added support for the **xdr-correlation-rule-update** command which updates an existing correlation rule. +- Added support for the **xdr-correlation-rule-delete** command which deletes correlation rules. +- Added support for the **xdr-correlation-rule-create** command which creates a new correlation rule. diff --git a/Packs/CortexXDR/pack_metadata.json b/Packs/CortexXDR/pack_metadata.json index 8c79f76bf836..bc4010a16d6b 100644 --- a/Packs/CortexXDR/pack_metadata.json +++ b/Packs/CortexXDR/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Cortex XDR by Palo Alto Networks", "description": "Automates Cortex XDR incident response, and includes custom Cortex XDR incident views and layouts to aid analyst investigations.", "support": "xsoar", - "currentVersion": "6.3.4", + "currentVersion": "6.3.5", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "", From 9a75433ac8e8adf1c596aab1a733cd9d4e321358 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Wed, 4 Feb 2026 11:36:30 +0200 Subject: [PATCH 15/27] added commands examples --- .../Integrations/CortexXDRIR/command_examples.txt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/command_examples.txt b/Packs/CortexXDR/Integrations/CortexXDRIR/command_examples.txt index 015bfe520045..b42fae4c5c4d 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/command_examples.txt +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/command_examples.txt @@ -61,4 +61,14 @@ !xdr-asset-group-list sort_field="XDM.ASSET_GROUP.LAST_UPDATE_TIME" page=1 page_size=3 !xdr-asset-group-create group_name="assetGroupTest" group_type=Dynamic membership_predicate_json="{\"AND\":[{\"SEARCH_FIELD\":\"xdm.asset.type.class\",\"SEARCH_TYPE\":\"NEQ\",\"SEARCH_VALUE\":\"Other\"}]}" !xdr-asset-group-update group_id=1 group_type=Dynamic group_description=test group_name="Test" membership_predicate_json="{\"AND\":[{\"SEARCH_FIELD\":\"xdm.asset.provider\",\"SEARCH_TYPE\":\"NEQ\",\"SEARCH_VALUE\":\"Other\"}]}" -!xdr-asset-group-delete group_id=1 \ No newline at end of file +!xdr-asset-group-delete group_id=1 +!xdr-bioc-list severity=high type=collection +!xdr-bioc-create name="TestBIOC" severity=high mitre_tactic_id_and_name="['TA0002 - Execution']" mitre_technique_id_and_name="['T1059 - Command and Scripting Interpreter']" indicator="{\"runOnCGO\":true,\"investigationType\":\"FILE_EVENT\",\"investigation\":{\"FILE_EVENT\":{\"filter\":{\"AND\":[{\"SEARCH_FIELD\":\"action_file_name\",\"SEARCH_TYPE\":\"EQ\",\"SEARCH_VALUE\":\"testfile.exe\"}]}}}}" type=collection status=enabled comment="Test" +!xdr-bioc-update name="TestBIOC" severity=high mitre_tactic_id_and_name="['TA0002 - Execution']" mitre_technique_id_and_name="['T1059 - Command and Scripting Interpreter']" indicator="{\"runOnCGO\":true,\"investigationType\":\"FILE_EVENT\",\"investigation\":{\"FILE_EVENT\":{\"filter\":{\"AND\":[{\"SEARCH_FIELD\":\"action_file_name\",\"SEARCH_TYPE\":\"EQ\",\"SEARCH_VALUE\":\"testfile.exe\"}]}}}}" type=collection status=enabled comment="Test" +!xdr-bioc-delete type=collection +!xdr-correlation-rule-list extra_data=true +!xdr-correlation-rule-create name="CR test" severity=low alert_category=dropper execution_mode=scheduled mapping_strategy=auto xql_query="dataset = xdr_data | limit 1" dataset=alerts alert_name="Test" is_enabled=true drilldown_query_timeframe="ALERT" investigation_query_link="dataset = xdr_data | limit 1" schedule="10 minutes" schedule_linux="*/10 * * * *" search_window="1 hours" suppression_duration="1 hours" suppression_enabled=true suppression_fields="event_type" timezone="Asia/Jerusalem" +!xdr-correlation-rule-update name="CR test" severity=low alert_category=dropper execution_mode=scheduled mapping_strategy=auto xql_query="dataset = xdr_data | limit 1" dataset=alerts alert_name="Test" is_enabled=true drilldown_query_timeframe="ALERT" investigation_query_link="dataset = xdr_data | limit 1" schedule="10 minutes" schedule_linux="*/10 * * * *" search_window="1 hours" suppression_duration="1 hours" suppression_enabled=true suppression_fields="event_type" timezone="Asia/Jerusalem" +!xdr-correlation-rule-delete rule_id=14,15 + + From 02857867784485c49c9efa4b0722dd2b6da63987 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Wed, 4 Feb 2026 11:40:54 +0200 Subject: [PATCH 16/27] fixed yml and created reamdme --- .../Integrations/CortexXDRIR/CortexXDRIR.yml | 6 +- .../Integrations/CortexXDRIR/README.md | 274 ++++++++++++++++++ 2 files changed, 277 insertions(+), 3 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml index 83662191ad84..2408e0e8d5c0 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml @@ -4216,13 +4216,13 @@ script: description: BIOC status. type: String - contextPath: PaloAltoNetworksXDR.BIOC.is_xql - description: + description: Whether the BIOC is XQL. type: Bool - contextPath: PaloAltoNetworksXDR.BIOC.comment - description: + description: The BIOC comment. type: String - contextPath: PaloAltoNetworksXDR.BIOC.indicator - description: + description: The BIOC indicator. type: Unknown - arguments: - description: The BIOC name. diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/README.md b/Packs/CortexXDR/Integrations/CortexXDRIR/README.md index 90f71626678c..3c3fc5539a95 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/README.md +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/README.md @@ -4122,3 +4122,277 @@ Gets a list of existing API keys. | PaloAltoNetworksXDR.APIKeyData.id | String | The API key ID. | | PaloAltoNetworksXDR.APIKeyData.roles | String | The roles associated with the API key. | | PaloAltoNetworksXDR.APIKeyData.expiration | Date | The expiration date of the API key. | +### xdr-bioc-list + +*** +Returns a list of BIOCs. + +#### Base Command + +`xdr-bioc-list` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| name | The BIOC name to filter by. Can filter only by one name. | Optional | +| severity | The BIOC severity to filter by. Possible values are: info, low, medium, high. | Optional | +| type | The BIOC type to filter by. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Optional | +| is_xql | Whether the BIOC is XQL. Possible values are: true, false. | Optional | +| comment | The BIOC comment to filter by. | Optional | +| status | The BIOC status to filter by. Can be one of the following: enabled, disabled. Possible values are: enabled, disabled. | Optional | +| indicator | The BIOC indicator to filter by. | Optional | +| mitre_technique_id_and_name | The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']. | Optional | +| mitre_tactic_id_and_name | The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']. | Optional | +| extra_data | Whether to return extended data. Possible values are: true, false. | Optional | +| limit | Maximum number of results to return. | Optional | +| page_size | Page size. | Optional | +| page | Page number. | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| PaloAltoNetworksXDR.BIOC.rule_id | String | BIOC rule id. | +| PaloAltoNetworksXDR.BIOC.name | String | BIOC name. | +| PaloAltoNetworksXDR.BIOC.type | String | BIOC type. | +| PaloAltoNetworksXDR.BIOC.severity | String | BIOC severity. | +| PaloAltoNetworksXDR.BIOC.status | String | BIOC status. | +| PaloAltoNetworksXDR.BIOC.is_xql | Bool | Whether the BIOC is XQL. | +| PaloAltoNetworksXDR.BIOC.comment | String | The BIOC comment. | +| PaloAltoNetworksXDR.BIOC.indicator | Unknown | The BIOC indicator. | +### xdr-bioc-create + +*** +Creates a new BIOC. + +#### Base Command + +`xdr-bioc-create` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| name | The BIOC name. | Required | +| severity | The BIOC severity. Possible values are: info, low, medium, high. | Required | +| type | The BIOC type. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Required | +| is_xql | Whether the new BIOC is XQL. Possible values are: true, false. | Optional | +| comment | The BIOC comment. | Required | +| status | The BIOC status. Possible values are: enabled, disabled. | Required | +| indicator | The BIOC indicator,
for example: '{\"runOnCGO\":true,\"investigationType\":\"FILE_EVENT\",\"investigation\":{\"FILE_EVENT\":{\"filter\":{\"AND\":[{\"SEARCH_FIELD\":\"action_file_name\",\"SEARCH_TYPE\":\"EQ\",\"SEARCH_VALUE\":\"testfile.exe\"}]}}}}'.
For more information please refer to the documentation https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-BIOCs. | Required | +| mitre_technique_id_and_name | The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']. | Optional | +| mitre_tactic_id_and_name | The MITRE tactic ID and name. Must be in format 'ID - Name', for example: ['TA0001 - Initial Access']. | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| PaloAltoNetworksXDR.BIOC.name | String | BIOC name. | +| PaloAltoNetworksXDR.BIOC.type | String | BIOC type. | +| PaloAltoNetworksXDR.BIOC.severity | String | BIOC severity. | +| PaloAltoNetworksXDR.BIOC.status | String | BIOC status. | +### xdr-bioc-update + +*** +Updates an existing BIOC. + +#### Base Command + +`xdr-bioc-update` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| rule_id | BIOC rule ID. | Required | +| name | BIOC name. | Required | +| severity | BIOC severity. Possible values are: info, low, medium, high. | Required | +| type | BIOC type. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Required | +| is_xql | Whether the BIOC is XQL. Possible values are: true, false. | Optional | +| comment | BIOC comment. | Required | +| status | BIOC status. Possible values are: enabled, disabled. | Required | +| indicator | The BIOC indicator,
for example: '{\"runOnCGO\":true,\"investigationType\":\"FILE_EVENT\",\"investigation\":{\"FILE_EVENT\":{\"filter\":{\"AND\":[{\"SEARCH_FIELD\":\"action_file_name\",\"SEARCH_TYPE\":\"EQ\",\"SEARCH_VALUE\":\"testfile.exe\"}]}}}}'.
For more information please refer to the documentation https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-BIOCs. | Required | +| mitre_technique_id_and_name | The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']. | Optional | +| mitre_tactic_id_and_name | The MITRE tactic ID and name. Must be in format 'ID - Name', for example: ['TA0001 - Initial Access']. | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| PaloAltoNetworksXDR.BIOC.name | String | BIOC name. | +| PaloAltoNetworksXDR.BIOC.type | String | BIOC type. | +| PaloAltoNetworksXDR.BIOC.severity | String | BIOC severity. | +| PaloAltoNetworksXDR.BIOC.status | String | BIOC status. | +### xdr-bioc-delete + +*** +Deletes a BIOC. + +#### Base Command + +`xdr-bioc-delete` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| name | BIOC name. | Optional | +| severity | BIOC severity. Possible values are: info, low, medium, high. | Optional | +| type | BIOC type. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Optional | +| is_xql | Whether the BIOC is XQL. Possible values are: true, false. | Optional | +| comment | BIOC comment. | Optional | +| indicator | BIOC indicator. | Optional | +| mitre_technique_id_and_name | The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']. | Optional | +| mitre_tactic_id_and_name | The MITRE tactic ID and name. Must be in format 'ID - Name', for example: ['TA0001 - Initial Access']. | Optional | + +#### Context Output + +There is no context output for this command. +### xdr-correlation-rule-list + +*** +Returns a list of correlation rules. + +#### Base Command + +`xdr-correlation-rule-list` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| name | Correlation rule name. | Optional | +| severity | Correlation rule severity. Possible values are: info, low, medium, high. | Optional | +| xql_query | Correlation rule XQL query. | Optional | +| is_xql | Whether the correlation rule is XQL. Possible values are: true, false. | Optional | +| dataset | Correlation rule dataset. | Optional | +| alert_name | Alert name. | Optional | +| alert_category | Alert category. | Optional | +| alert_fields | Alert fields. Can be a string or a dictionary. | Optional | +| alert_domain | Alert domain. | Optional | +| filter_json | Filter JSON. | Optional | +| extra_data | Whether to return extended view. Possible values are: true, false. | Optional | +| limit | Maximum number of results to return. | Optional | +| page_size | Page size. | Optional | +| page | Page number. | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| PaloAltoNetworksXDR.CorrelationRule.id | String | Correlation rule ID. | +| PaloAltoNetworksXDR.CorrelationRule.name | String | Correlation rule name. | +| PaloAltoNetworksXDR.CorrelationRule.description | String | Correlation rule description. | +| PaloAltoNetworksXDR.CorrelationRule.is_enabled | Boolean | Whether the correlation rule is enabled. | +### xdr-correlation-rule-create + +*** +Creates a new correlation rule. + +#### Base Command + +`xdr-correlation-rule-create` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| name | The correlation rule name. Example: name="This is a CR test 6". | Required | +| severity | The correlation rule severity. Example: severity=low. Possible values are: info, low, medium, high. | Required | +| xql_query | The correlation rule XQL query. Example: xql_query="dataset = xdr_data \| limit 1". | Required | +| is_enabled | Whether the rule is enabled. Example: is_enabled=true. Possible values are: true, false. | Required | +| alert_category | The alert category. Example: alert_category=dropper. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Required | +| execution_mode | The rule execution mode. Example: execution_mode=scheduled. Possible values are: scheduled, real_time. | Required | +| timezone | The correlation rule timezone. Example: timezone="Asia/Jerusalem". | Required | +| mapping_strategy | The rule mapping strategy. Example: mapping_strategy=auto. Possible values are: auto, custom. | Required | +| description | The correlation rule description. | Optional | +| alert_name | The alert name. | Optional | +| alert_description | The alert description. | Optional | +| alert_fields | Alert fields (string or dictionary). | Optional | +| search_window | The search window timeframe. Example: search_window="1 hours". | Optional | +| schedule | The correlation rule schedule. Example: schedule="10 minutes". | Optional | +| schedule_linux | Linux scheduling for the rule. Example: schedule_linux="*/10 * * * *". | Optional | +| suppression_enabled | Whether suppression is enabled. Possible values are: true, false. | Optional | +| suppression_duration | Duration of correlation rule suppression. | Optional | +| suppression_fields | Suppression fields. | Optional | +| dataset | The correlation rule dataset. Example: dataset=alerts. | Optional | +| user_defined_severity | User-defined severity. | Optional | +| user_defined_category | User-defined category. | Optional | +| mitre_defs_json | MITRE definitions. | Optional | +| investigation_query_link | Investigation query link. | Optional | +| drilldown_query_timeframe | The drilldown query timeframe. Example: drilldown_query_timeframe="ALERT". | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| PaloAltoNetworksXDR.CorrelationRule.id | String | Correlation rule ID. | +| PaloAltoNetworksXDR.CorrelationRule.name | String | Correlation rule name. | +| PaloAltoNetworksXDR.CorrelationRule.description | String | Correlation rule description. | +| PaloAltoNetworksXDR.CorrelationRule.is_enabled | Boolean | Whether the correlation rule is enabled. | +### xdr-correlation-rule-update + +*** +Updates an existing correlation rule. + +#### Base Command + +`xdr-correlation-rule-update` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| rule_id | Correlation rule ID. | Required | +| name | The correlation rule name. Example: name="This is a CR test 6". | Required | +| severity | The correlation rule severity. Example: severity=low. Possible values are: info, low, medium, high. | Required | +| xql_query | The correlation rule XQL query. Example: xql_query="dataset = xdr_data \| limit 1". | Required | +| is_enabled | Whether the rule is enabled. Example: is_enabled=true. Possible values are: true, false. | Required | +| alert_category | The alert category. Example: alert_category=dropper. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Required | +| execution_mode | The rule execution mode. Example: execution_mode=scheduled. Possible values are: scheduled, real_time. | Required | +| timezone | The correlation rule timezone. Example: timezone="Asia/Jerusalem". | Required | +| mapping_strategy | The rule mapping strategy. Example: mapping_strategy=auto. Possible values are: auto, custom. | Required | +| description | The correlation rule description. | Optional | +| alert_name | The alert name. | Optional | +| alert_description | The alert description. | Optional | +| alert_fields | Alert fields (string or dictionary). | Optional | +| search_window | The search window timeframe. Example: search_window="1 hours". | Optional | +| schedule | The correlation rule schedule. Example: schedule="10 minutes". | Optional | +| schedule_linux | Linux scheduling for the rule. Example: schedule_linux="*/10 * * * *". | Optional | +| suppression_enabled | Whether suppression is enabled. Possible values are: true, false. | Optional | +| suppression_duration | Duration of correlation rule suppression. | Optional | +| suppression_fields | Suppression fields. | Optional | +| dataset | The correlation rule dataset. Example: dataset=alerts. | Required | +| user_defined_severity | User-defined severity. | Optional | +| user_defined_category | User-defined category. | Optional | +| mitre_defs_json | MITRE definitions. | Optional | +| investigation_query_link | Investigation query link. | Optional | +| drilldown_query_timeframe | The drilldown query timeframe. Example: drilldown_query_timeframe="ALERT". | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| PaloAltoNetworksXDR.CorrelationRule.id | String | Correlation rule ID. | +| PaloAltoNetworksXDR.CorrelationRule.name | String | Correlation rule name. | +| PaloAltoNetworksXDR.CorrelationRule.description | String | Correlation rule description. | +| PaloAltoNetworksXDR.CorrelationRule.is_enabled | Boolean | Whether the correlation rule is enabled. | +### xdr-correlation-rule-delete + +*** +Deletes correlation rules. + +#### Base Command + +`xdr-correlation-rule-delete` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| rule_id | Correlation rule ID. | Required | + +#### Context Output + +There is no context output for this command. \ No newline at end of file From 74edb4404a3f3cf4f66cc75565fa86282bcfafd4 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Wed, 4 Feb 2026 11:43:25 +0200 Subject: [PATCH 17/27] run pre commit --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 40 ++- .../CortexXDRIR/CortexXDRIR_test.py | 8 + .../Integrations/CortexXDRIR/README.md | 274 +++++++++--------- 3 files changed, 166 insertions(+), 156 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 235277838b39..4e905e247917 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -50,12 +50,8 @@ XDR_OPEN_STATUS_TO_XSOAR = ["under_investigation", "new"] -BIOC_AND_CR_SEVERITY_MAPPING = { - "info": "SEV_010_INFO", - "low": "SEV_020_LOW", - "medium": "SEV_030_MEDIUM", - "high": "SEV_040_HIGH" -} +BIOC_AND_CR_SEVERITY_MAPPING = {"info": "SEV_010_INFO", "low": "SEV_020_LOW", "medium": "SEV_030_MEDIUM", "high": "SEV_040_HIGH"} + def convert_epoch_to_milli(timestamp): if timestamp is None: @@ -1685,11 +1681,13 @@ def bioc_list_command(client: Client, args: Dict) -> CommandResults: ) reply = client.get_biocs({"request_data": request_data}) biocs = reply.get("objects", []) - readable_output = tableToMarkdown(name="BIOCs List", - t=biocs, - headerTransform=string_to_table_header, - headers=["rule_id", "name", "type", "severity", "status"], - removeNull=True) + readable_output = tableToMarkdown( + name="BIOCs List", + t=biocs, + headerTransform=string_to_table_header, + headers=["rule_id", "name", "type", "severity", "status"], + removeNull=True, + ) return CommandResults( readable_output=readable_output, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.BIOC", @@ -1832,11 +1830,12 @@ def correlation_rule_list_command(client: Client, args: Dict) -> CommandResults: ) reply = client.get_correlation_rules({"request_data": request_data}) rules = reply.get("objects", []) - readable_output = tableToMarkdown(name="Correlation Rules List", - t=rules, - headers=["rule_id", "name", "description", "is_enabled"], - removeNull=True, - headerTransform=string_to_table_header, + readable_output = tableToMarkdown( + name="Correlation Rules List", + t=rules, + headers=["rule_id", "name", "description", "is_enabled"], + removeNull=True, + headerTransform=string_to_table_header, ) return CommandResults( readable_output=readable_output, @@ -1869,12 +1868,10 @@ def correlation_rule_create_or_update_helper(client: Client, args: Dict) -> dict "action": "ALERTS", "timezone": args.get("timezone"), "dataset": args.get("dataset"), - # Uppercase handling for Enum fields "alert_category": args.get("alert_category").upper() if args.get("alert_category") else None, "execution_mode": args.get("execution_mode").upper() if args.get("execution_mode") else None, "mapping_strategy": args.get("mapping_strategy").upper() if args.get("mapping_strategy") else None, - # Use 'or None' to convert empty strings ("") into JSON null. We must put a value for those fields. "description": args.get("description") or None, "alert_name": args.get("alert_name") or None, @@ -1887,14 +1884,12 @@ def correlation_rule_create_or_update_helper(client: Client, args: Dict) -> dict "suppression_enabled": argToBoolean(args.get("suppression_enabled")) if args.get("suppression_enabled") else None, "suppression_duration": args.get("suppression_duration") or None, "suppression_fields": args.get("suppression_fields") or None, - # User Defined Severity (Must be None unless Severity is "User Defined") "user_defined_severity": args.get("user_defined_severity") or None, "user_defined_category": args.get("user_defined_category") or None, - # Defaults to "{}" string if missing so json.loads doesn't fail "mitre_defs": json.loads(args.get("mitre_defs_json") or "{}"), - "alert_fields": json.loads(args.get("alert_fields") or "{}") + "alert_fields": json.loads(args.get("alert_fields") or "{}"), } return client.create_or_update_correlation_rules({"request_data": [rule_data]}) @@ -1976,8 +1971,7 @@ def correlation_rule_delete_command(client: Client, args: Dict) -> CommandResult ids_str = ", ".join(map(str, deleted_ids)) status = f"Correlation Rules {ids_str} were deleted." - return CommandResults(readable_output=status, - raw_response=reply) + return CommandResults(readable_output=status, raw_response=reply) def api_key_list_command(client: Client, args: Dict[str, Any]) -> CommandResults: diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py index 82ed2f7100cb..ec2f2a710dc5 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py @@ -2797,6 +2797,7 @@ def test_bioc_list_command(mocker): - Verify the client.get_biocs is called and results are correct. """ from CortexXDRIR import Client, bioc_list_command + client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) mock_reply = {"objects": [{"rule_id": "1", "name": "test_bioc", "type": "host", "severity": "high", "status": "enabled"}]} mocker.patch.object(Client, "get_biocs", return_value=mock_reply) @@ -2818,6 +2819,7 @@ def test_bioc_create_command(mocker): - Verify the client.insert_or_update_biocs is called and results are correct. """ from CortexXDRIR import Client, bioc_create_command + client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) mock_reply = {"added_objects": [{"id": "1", "status": "BIOC created successfully"}]} mocker.patch.object(Client, "insert_or_update_biocs", return_value=mock_reply) @@ -2839,6 +2841,7 @@ def test_bioc_update_command(mocker): - Verify the client.insert_or_update_biocs is called and results are correct. """ from CortexXDRIR import Client, bioc_update_command + client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) mock_reply = {"updated_objects": [{"id": "1", "status": "BIOC updated successfully"}]} mocker.patch.object(Client, "insert_or_update_biocs", return_value=mock_reply) @@ -2860,6 +2863,7 @@ def test_bioc_delete_command(mocker): - Verify the client.delete_biocs is called and results are correct. """ from CortexXDRIR import Client, bioc_delete_command + client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) mock_reply = {"objects": ["1"]} mocker.patch.object(Client, "delete_biocs", return_value=mock_reply) @@ -2880,6 +2884,7 @@ def test_correlation_rule_list_command(mocker): - Verify the client.get_correlation_rules is called and results are correct. """ from CortexXDRIR import Client, correlation_rule_list_command + client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) mock_reply = {"objects": [{"rule_id": "1", "name": "test_rule", "description": "desc", "is_enabled": True}]} mocker.patch.object(Client, "get_correlation_rules", return_value=mock_reply) @@ -2901,6 +2906,7 @@ def test_correlation_rule_create_command(mocker): - Verify the client.create_or_update_correlation_rules is called and results are correct. """ from CortexXDRIR import Client, correlation_rule_create_command + client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) mock_reply = {"added_objects": [{"id": "1", "status": "Rule created successfully"}]} mocker.patch.object(Client, "create_or_update_correlation_rules", return_value=mock_reply) @@ -2922,6 +2928,7 @@ def test_correlation_rule_update_command(mocker): - Verify the client.create_or_update_correlation_rules is called and results are correct. """ from CortexXDRIR import Client, correlation_rule_update_command + client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) mock_reply = {"updated_objects": [{"id": "1", "status": "Rule updated successfully"}]} mocker.patch.object(Client, "create_or_update_correlation_rules", return_value=mock_reply) @@ -2943,6 +2950,7 @@ def test_correlation_rule_delete_command(mocker): - Verify the client.delete_correlation_rules is called and results are correct. """ from CortexXDRIR import Client, correlation_rule_delete_command + client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) mock_reply = {"objects": ["1"], "objects_count": 1} mocker.patch.object(Client, "delete_correlation_rules", return_value=mock_reply) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/README.md b/Packs/CortexXDR/Integrations/CortexXDRIR/README.md index 3c3fc5539a95..ea27ec036a88 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/README.md +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/README.md @@ -4122,6 +4122,7 @@ Gets a list of existing API keys. | PaloAltoNetworksXDR.APIKeyData.id | String | The API key ID. | | PaloAltoNetworksXDR.APIKeyData.roles | String | The roles associated with the API key. | | PaloAltoNetworksXDR.APIKeyData.expiration | Date | The expiration date of the API key. | + ### xdr-bioc-list *** @@ -4135,32 +4136,33 @@ Returns a list of BIOCs. | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| name | The BIOC name to filter by. Can filter only by one name. | Optional | -| severity | The BIOC severity to filter by. Possible values are: info, low, medium, high. | Optional | -| type | The BIOC type to filter by. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Optional | -| is_xql | Whether the BIOC is XQL. Possible values are: true, false. | Optional | -| comment | The BIOC comment to filter by. | Optional | -| status | The BIOC status to filter by. Can be one of the following: enabled, disabled. Possible values are: enabled, disabled. | Optional | -| indicator | The BIOC indicator to filter by. | Optional | -| mitre_technique_id_and_name | The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']. | Optional | -| mitre_tactic_id_and_name | The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']. | Optional | -| extra_data | Whether to return extended data. Possible values are: true, false. | Optional | -| limit | Maximum number of results to return. | Optional | -| page_size | Page size. | Optional | -| page | Page number. | Optional | +| name | The BIOC name to filter by. Can filter only by one name. | Optional | +| severity | The BIOC severity to filter by. Possible values are: info, low, medium, high. | Optional | +| type | The BIOC type to filter by. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Optional | +| is_xql | Whether the BIOC is XQL. Possible values are: true, false. | Optional | +| comment | The BIOC comment to filter by. | Optional | +| status | The BIOC status to filter by. Can be one of the following: enabled, disabled. Possible values are: enabled, disabled. | Optional | +| indicator | The BIOC indicator to filter by. | Optional | +| mitre_technique_id_and_name | The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']. | Optional | +| mitre_tactic_id_and_name | The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']. | Optional | +| extra_data | Whether to return extended data. Possible values are: true, false. | Optional | +| limit | Maximum number of results to return. | Optional | +| page_size | Page size. | Optional | +| page | Page number. | Optional | #### Context Output | **Path** | **Type** | **Description** | | --- | --- | --- | -| PaloAltoNetworksXDR.BIOC.rule_id | String | BIOC rule id. | -| PaloAltoNetworksXDR.BIOC.name | String | BIOC name. | -| PaloAltoNetworksXDR.BIOC.type | String | BIOC type. | -| PaloAltoNetworksXDR.BIOC.severity | String | BIOC severity. | -| PaloAltoNetworksXDR.BIOC.status | String | BIOC status. | -| PaloAltoNetworksXDR.BIOC.is_xql | Bool | Whether the BIOC is XQL. | -| PaloAltoNetworksXDR.BIOC.comment | String | The BIOC comment. | -| PaloAltoNetworksXDR.BIOC.indicator | Unknown | The BIOC indicator. | +| PaloAltoNetworksXDR.BIOC.rule_id | String | BIOC rule id. | +| PaloAltoNetworksXDR.BIOC.name | String | BIOC name. | +| PaloAltoNetworksXDR.BIOC.type | String | BIOC type. | +| PaloAltoNetworksXDR.BIOC.severity | String | BIOC severity. | +| PaloAltoNetworksXDR.BIOC.status | String | BIOC status. | +| PaloAltoNetworksXDR.BIOC.is_xql | Bool | Whether the BIOC is XQL. | +| PaloAltoNetworksXDR.BIOC.comment | String | The BIOC comment. | +| PaloAltoNetworksXDR.BIOC.indicator | Unknown | The BIOC indicator. | + ### xdr-bioc-create *** @@ -4174,24 +4176,25 @@ Creates a new BIOC. | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| name | The BIOC name. | Required | -| severity | The BIOC severity. Possible values are: info, low, medium, high. | Required | -| type | The BIOC type. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Required | -| is_xql | Whether the new BIOC is XQL. Possible values are: true, false. | Optional | -| comment | The BIOC comment. | Required | -| status | The BIOC status. Possible values are: enabled, disabled. | Required | -| indicator | The BIOC indicator,
for example: '{\"runOnCGO\":true,\"investigationType\":\"FILE_EVENT\",\"investigation\":{\"FILE_EVENT\":{\"filter\":{\"AND\":[{\"SEARCH_FIELD\":\"action_file_name\",\"SEARCH_TYPE\":\"EQ\",\"SEARCH_VALUE\":\"testfile.exe\"}]}}}}'.
For more information please refer to the documentation https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-BIOCs. | Required | -| mitre_technique_id_and_name | The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']. | Optional | -| mitre_tactic_id_and_name | The MITRE tactic ID and name. Must be in format 'ID - Name', for example: ['TA0001 - Initial Access']. | Optional | +| name | The BIOC name. | Required | +| severity | The BIOC severity. Possible values are: info, low, medium, high. | Required | +| type | The BIOC type. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Required | +| is_xql | Whether the new BIOC is XQL. Possible values are: true, false. | Optional | +| comment | The BIOC comment. | Required | +| status | The BIOC status. Possible values are: enabled, disabled. | Required | +| indicator | The BIOC indicator,
for example: '{\"runOnCGO\":true,\"investigationType\":\"FILE_EVENT\",\"investigation\":{\"FILE_EVENT\":{\"filter\":{\"AND\":[{\"SEARCH_FIELD\":\"action_file_name\",\"SEARCH_TYPE\":\"EQ\",\"SEARCH_VALUE\":\"testfile.exe\"}]}}}}'.
For more information please refer to the documentation https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-BIOCs. | Required | +| mitre_technique_id_and_name | The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']. | Optional | +| mitre_tactic_id_and_name | The MITRE tactic ID and name. Must be in format 'ID - Name', for example: ['TA0001 - Initial Access']. | Optional | #### Context Output | **Path** | **Type** | **Description** | | --- | --- | --- | -| PaloAltoNetworksXDR.BIOC.name | String | BIOC name. | -| PaloAltoNetworksXDR.BIOC.type | String | BIOC type. | -| PaloAltoNetworksXDR.BIOC.severity | String | BIOC severity. | -| PaloAltoNetworksXDR.BIOC.status | String | BIOC status. | +| PaloAltoNetworksXDR.BIOC.name | String | BIOC name. | +| PaloAltoNetworksXDR.BIOC.type | String | BIOC type. | +| PaloAltoNetworksXDR.BIOC.severity | String | BIOC severity. | +| PaloAltoNetworksXDR.BIOC.status | String | BIOC status. | + ### xdr-bioc-update *** @@ -4205,25 +4208,26 @@ Updates an existing BIOC. | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| rule_id | BIOC rule ID. | Required | -| name | BIOC name. | Required | -| severity | BIOC severity. Possible values are: info, low, medium, high. | Required | -| type | BIOC type. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Required | -| is_xql | Whether the BIOC is XQL. Possible values are: true, false. | Optional | -| comment | BIOC comment. | Required | -| status | BIOC status. Possible values are: enabled, disabled. | Required | -| indicator | The BIOC indicator,
for example: '{\"runOnCGO\":true,\"investigationType\":\"FILE_EVENT\",\"investigation\":{\"FILE_EVENT\":{\"filter\":{\"AND\":[{\"SEARCH_FIELD\":\"action_file_name\",\"SEARCH_TYPE\":\"EQ\",\"SEARCH_VALUE\":\"testfile.exe\"}]}}}}'.
For more information please refer to the documentation https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-BIOCs. | Required | -| mitre_technique_id_and_name | The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']. | Optional | -| mitre_tactic_id_and_name | The MITRE tactic ID and name. Must be in format 'ID - Name', for example: ['TA0001 - Initial Access']. | Optional | +| rule_id | BIOC rule ID. | Required | +| name | BIOC name. | Required | +| severity | BIOC severity. Possible values are: info, low, medium, high. | Required | +| type | BIOC type. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Required | +| is_xql | Whether the BIOC is XQL. Possible values are: true, false. | Optional | +| comment | BIOC comment. | Required | +| status | BIOC status. Possible values are: enabled, disabled. | Required | +| indicator | The BIOC indicator,
for example: '{\"runOnCGO\":true,\"investigationType\":\"FILE_EVENT\",\"investigation\":{\"FILE_EVENT\":{\"filter\":{\"AND\":[{\"SEARCH_FIELD\":\"action_file_name\",\"SEARCH_TYPE\":\"EQ\",\"SEARCH_VALUE\":\"testfile.exe\"}]}}}}'.
For more information please refer to the documentation https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Insert-or-update-BIOCs. | Required | +| mitre_technique_id_and_name | The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']. | Optional | +| mitre_tactic_id_and_name | The MITRE tactic ID and name. Must be in format 'ID - Name', for example: ['TA0001 - Initial Access']. | Optional | #### Context Output | **Path** | **Type** | **Description** | | --- | --- | --- | -| PaloAltoNetworksXDR.BIOC.name | String | BIOC name. | -| PaloAltoNetworksXDR.BIOC.type | String | BIOC type. | -| PaloAltoNetworksXDR.BIOC.severity | String | BIOC severity. | -| PaloAltoNetworksXDR.BIOC.status | String | BIOC status. | +| PaloAltoNetworksXDR.BIOC.name | String | BIOC name. | +| PaloAltoNetworksXDR.BIOC.type | String | BIOC type. | +| PaloAltoNetworksXDR.BIOC.severity | String | BIOC severity. | +| PaloAltoNetworksXDR.BIOC.status | String | BIOC status. | + ### xdr-bioc-delete *** @@ -4237,18 +4241,19 @@ Deletes a BIOC. | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| name | BIOC name. | Optional | -| severity | BIOC severity. Possible values are: info, low, medium, high. | Optional | -| type | BIOC type. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Optional | -| is_xql | Whether the BIOC is XQL. Possible values are: true, false. | Optional | -| comment | BIOC comment. | Optional | -| indicator | BIOC indicator. | Optional | -| mitre_technique_id_and_name | The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']. | Optional | -| mitre_tactic_id_and_name | The MITRE tactic ID and name. Must be in format 'ID - Name', for example: ['TA0001 - Initial Access']. | Optional | +| name | BIOC name. | Optional | +| severity | BIOC severity. Possible values are: info, low, medium, high. | Optional | +| type | BIOC type. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Optional | +| is_xql | Whether the BIOC is XQL. Possible values are: true, false. | Optional | +| comment | BIOC comment. | Optional | +| indicator | BIOC indicator. | Optional | +| mitre_technique_id_and_name | The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']. | Optional | +| mitre_tactic_id_and_name | The MITRE tactic ID and name. Must be in format 'ID - Name', for example: ['TA0001 - Initial Access']. | Optional | #### Context Output There is no context output for this command. + ### xdr-correlation-rule-list *** @@ -4262,29 +4267,30 @@ Returns a list of correlation rules. | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| name | Correlation rule name. | Optional | -| severity | Correlation rule severity. Possible values are: info, low, medium, high. | Optional | -| xql_query | Correlation rule XQL query. | Optional | -| is_xql | Whether the correlation rule is XQL. Possible values are: true, false. | Optional | -| dataset | Correlation rule dataset. | Optional | -| alert_name | Alert name. | Optional | -| alert_category | Alert category. | Optional | -| alert_fields | Alert fields. Can be a string or a dictionary. | Optional | -| alert_domain | Alert domain. | Optional | -| filter_json | Filter JSON. | Optional | -| extra_data | Whether to return extended view. Possible values are: true, false. | Optional | -| limit | Maximum number of results to return. | Optional | -| page_size | Page size. | Optional | -| page | Page number. | Optional | +| name | Correlation rule name. | Optional | +| severity | Correlation rule severity. Possible values are: info, low, medium, high. | Optional | +| xql_query | Correlation rule XQL query. | Optional | +| is_xql | Whether the correlation rule is XQL. Possible values are: true, false. | Optional | +| dataset | Correlation rule dataset. | Optional | +| alert_name | Alert name. | Optional | +| alert_category | Alert category. | Optional | +| alert_fields | Alert fields. Can be a string or a dictionary. | Optional | +| alert_domain | Alert domain. | Optional | +| filter_json | Filter JSON. | Optional | +| extra_data | Whether to return extended view. Possible values are: true, false. | Optional | +| limit | Maximum number of results to return. | Optional | +| page_size | Page size. | Optional | +| page | Page number. | Optional | #### Context Output | **Path** | **Type** | **Description** | | --- | --- | --- | -| PaloAltoNetworksXDR.CorrelationRule.id | String | Correlation rule ID. | -| PaloAltoNetworksXDR.CorrelationRule.name | String | Correlation rule name. | -| PaloAltoNetworksXDR.CorrelationRule.description | String | Correlation rule description. | -| PaloAltoNetworksXDR.CorrelationRule.is_enabled | Boolean | Whether the correlation rule is enabled. | +| PaloAltoNetworksXDR.CorrelationRule.id | String | Correlation rule ID. | +| PaloAltoNetworksXDR.CorrelationRule.name | String | Correlation rule name. | +| PaloAltoNetworksXDR.CorrelationRule.description | String | Correlation rule description. | +| PaloAltoNetworksXDR.CorrelationRule.is_enabled | Boolean | Whether the correlation rule is enabled. | + ### xdr-correlation-rule-create *** @@ -4298,39 +4304,40 @@ Creates a new correlation rule. | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| name | The correlation rule name. Example: name="This is a CR test 6". | Required | -| severity | The correlation rule severity. Example: severity=low. Possible values are: info, low, medium, high. | Required | -| xql_query | The correlation rule XQL query. Example: xql_query="dataset = xdr_data \| limit 1". | Required | -| is_enabled | Whether the rule is enabled. Example: is_enabled=true. Possible values are: true, false. | Required | -| alert_category | The alert category. Example: alert_category=dropper. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Required | -| execution_mode | The rule execution mode. Example: execution_mode=scheduled. Possible values are: scheduled, real_time. | Required | -| timezone | The correlation rule timezone. Example: timezone="Asia/Jerusalem". | Required | -| mapping_strategy | The rule mapping strategy. Example: mapping_strategy=auto. Possible values are: auto, custom. | Required | -| description | The correlation rule description. | Optional | -| alert_name | The alert name. | Optional | -| alert_description | The alert description. | Optional | -| alert_fields | Alert fields (string or dictionary). | Optional | -| search_window | The search window timeframe. Example: search_window="1 hours". | Optional | -| schedule | The correlation rule schedule. Example: schedule="10 minutes". | Optional | -| schedule_linux | Linux scheduling for the rule. Example: schedule_linux="*/10 * * * *". | Optional | -| suppression_enabled | Whether suppression is enabled. Possible values are: true, false. | Optional | -| suppression_duration | Duration of correlation rule suppression. | Optional | -| suppression_fields | Suppression fields. | Optional | -| dataset | The correlation rule dataset. Example: dataset=alerts. | Optional | -| user_defined_severity | User-defined severity. | Optional | -| user_defined_category | User-defined category. | Optional | -| mitre_defs_json | MITRE definitions. | Optional | -| investigation_query_link | Investigation query link. | Optional | -| drilldown_query_timeframe | The drilldown query timeframe. Example: drilldown_query_timeframe="ALERT". | Optional | +| name | The correlation rule name. Example: name="This is a CR test 6". | Required | +| severity | The correlation rule severity. Example: severity=low. Possible values are: info, low, medium, high. | Required | +| xql_query | The correlation rule XQL query. Example: xql_query="dataset = xdr_data \| limit 1". | Required | +| is_enabled | Whether the rule is enabled. Example: is_enabled=true. Possible values are: true, false. | Required | +| alert_category | The alert category. Example: alert_category=dropper. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Required | +| execution_mode | The rule execution mode. Example: execution_mode=scheduled. Possible values are: scheduled, real_time. | Required | +| timezone | The correlation rule timezone. Example: timezone="Asia/Jerusalem". | Required | +| mapping_strategy | The rule mapping strategy. Example: mapping_strategy=auto. Possible values are: auto, custom. | Required | +| description | The correlation rule description. | Optional | +| alert_name | The alert name. | Optional | +| alert_description | The alert description. | Optional | +| alert_fields | Alert fields (string or dictionary). | Optional | +| search_window | The search window timeframe. Example: search_window="1 hours". | Optional | +| schedule | The correlation rule schedule. Example: schedule="10 minutes". | Optional | +| schedule_linux | Linux scheduling for the rule. Example: schedule_linux="*/10* ** *". | Optional | +| suppression_enabled | Whether suppression is enabled. Possible values are: true, false. | Optional | +| suppression_duration | Duration of correlation rule suppression. | Optional | +| suppression_fields | Suppression fields. | Optional | +| dataset | The correlation rule dataset. Example: dataset=alerts. | Optional | +| user_defined_severity | User-defined severity. | Optional | +| user_defined_category | User-defined category. | Optional | +| mitre_defs_json | MITRE definitions. | Optional | +| investigation_query_link | Investigation query link. | Optional | +| drilldown_query_timeframe | The drilldown query timeframe. Example: drilldown_query_timeframe="ALERT". | Optional | #### Context Output | **Path** | **Type** | **Description** | | --- | --- | --- | -| PaloAltoNetworksXDR.CorrelationRule.id | String | Correlation rule ID. | -| PaloAltoNetworksXDR.CorrelationRule.name | String | Correlation rule name. | -| PaloAltoNetworksXDR.CorrelationRule.description | String | Correlation rule description. | -| PaloAltoNetworksXDR.CorrelationRule.is_enabled | Boolean | Whether the correlation rule is enabled. | +| PaloAltoNetworksXDR.CorrelationRule.id | String | Correlation rule ID. | +| PaloAltoNetworksXDR.CorrelationRule.name | String | Correlation rule name. | +| PaloAltoNetworksXDR.CorrelationRule.description | String | Correlation rule description. | +| PaloAltoNetworksXDR.CorrelationRule.is_enabled | Boolean | Whether the correlation rule is enabled. | + ### xdr-correlation-rule-update *** @@ -4344,40 +4351,41 @@ Updates an existing correlation rule. | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| rule_id | Correlation rule ID. | Required | -| name | The correlation rule name. Example: name="This is a CR test 6". | Required | -| severity | The correlation rule severity. Example: severity=low. Possible values are: info, low, medium, high. | Required | -| xql_query | The correlation rule XQL query. Example: xql_query="dataset = xdr_data \| limit 1". | Required | -| is_enabled | Whether the rule is enabled. Example: is_enabled=true. Possible values are: true, false. | Required | -| alert_category | The alert category. Example: alert_category=dropper. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Required | -| execution_mode | The rule execution mode. Example: execution_mode=scheduled. Possible values are: scheduled, real_time. | Required | -| timezone | The correlation rule timezone. Example: timezone="Asia/Jerusalem". | Required | -| mapping_strategy | The rule mapping strategy. Example: mapping_strategy=auto. Possible values are: auto, custom. | Required | -| description | The correlation rule description. | Optional | -| alert_name | The alert name. | Optional | -| alert_description | The alert description. | Optional | -| alert_fields | Alert fields (string or dictionary). | Optional | -| search_window | The search window timeframe. Example: search_window="1 hours". | Optional | -| schedule | The correlation rule schedule. Example: schedule="10 minutes". | Optional | -| schedule_linux | Linux scheduling for the rule. Example: schedule_linux="*/10 * * * *". | Optional | -| suppression_enabled | Whether suppression is enabled. Possible values are: true, false. | Optional | -| suppression_duration | Duration of correlation rule suppression. | Optional | -| suppression_fields | Suppression fields. | Optional | -| dataset | The correlation rule dataset. Example: dataset=alerts. | Required | -| user_defined_severity | User-defined severity. | Optional | -| user_defined_category | User-defined category. | Optional | -| mitre_defs_json | MITRE definitions. | Optional | -| investigation_query_link | Investigation query link. | Optional | -| drilldown_query_timeframe | The drilldown query timeframe. Example: drilldown_query_timeframe="ALERT". | Optional | +| rule_id | Correlation rule ID. | Required | +| name | The correlation rule name. Example: name="This is a CR test 6". | Required | +| severity | The correlation rule severity. Example: severity=low. Possible values are: info, low, medium, high. | Required | +| xql_query | The correlation rule XQL query. Example: xql_query="dataset = xdr_data \| limit 1". | Required | +| is_enabled | Whether the rule is enabled. Example: is_enabled=true. Possible values are: true, false. | Required | +| alert_category | The alert category. Example: alert_category=dropper. Possible values are: other, persistence, evasion, tampering, file_type_obfuscation, privilege_escalation, credential_access, lateral_movement, execution, collection, exfiltration, infiltration, dropper, file_privilege_manipulation, reconnaissance, discovery. | Required | +| execution_mode | The rule execution mode. Example: execution_mode=scheduled. Possible values are: scheduled, real_time. | Required | +| timezone | The correlation rule timezone. Example: timezone="Asia/Jerusalem". | Required | +| mapping_strategy | The rule mapping strategy. Example: mapping_strategy=auto. Possible values are: auto, custom. | Required | +| description | The correlation rule description. | Optional | +| alert_name | The alert name. | Optional | +| alert_description | The alert description. | Optional | +| alert_fields | Alert fields (string or dictionary). | Optional | +| search_window | The search window timeframe. Example: search_window="1 hours". | Optional | +| schedule | The correlation rule schedule. Example: schedule="10 minutes". | Optional | +| schedule_linux | Linux scheduling for the rule. Example: schedule_linux="*/10* ** *". | Optional | +| suppression_enabled | Whether suppression is enabled. Possible values are: true, false. | Optional | +| suppression_duration | Duration of correlation rule suppression. | Optional | +| suppression_fields | Suppression fields. | Optional | +| dataset | The correlation rule dataset. Example: dataset=alerts. | Required | +| user_defined_severity | User-defined severity. | Optional | +| user_defined_category | User-defined category. | Optional | +| mitre_defs_json | MITRE definitions. | Optional | +| investigation_query_link | Investigation query link. | Optional | +| drilldown_query_timeframe | The drilldown query timeframe. Example: drilldown_query_timeframe="ALERT". | Optional | #### Context Output | **Path** | **Type** | **Description** | | --- | --- | --- | -| PaloAltoNetworksXDR.CorrelationRule.id | String | Correlation rule ID. | -| PaloAltoNetworksXDR.CorrelationRule.name | String | Correlation rule name. | -| PaloAltoNetworksXDR.CorrelationRule.description | String | Correlation rule description. | -| PaloAltoNetworksXDR.CorrelationRule.is_enabled | Boolean | Whether the correlation rule is enabled. | +| PaloAltoNetworksXDR.CorrelationRule.id | String | Correlation rule ID. | +| PaloAltoNetworksXDR.CorrelationRule.name | String | Correlation rule name. | +| PaloAltoNetworksXDR.CorrelationRule.description | String | Correlation rule description. | +| PaloAltoNetworksXDR.CorrelationRule.is_enabled | Boolean | Whether the correlation rule is enabled. | + ### xdr-correlation-rule-delete *** @@ -4391,8 +4399,8 @@ Deletes correlation rules. | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| rule_id | Correlation rule ID. | Required | +| rule_id | Correlation rule ID. | Required | #### Context Output -There is no context output for this command. \ No newline at end of file +There is no context output for this command. From de4a9a0680db6b6bdd9a88940221673554ad7f3e Mon Sep 17 00:00:00 2001 From: noydavidi Date: Wed, 4 Feb 2026 12:54:21 +0200 Subject: [PATCH 18/27] fixed unittest --- Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py index ec2f2a710dc5..74b616b7d917 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py @@ -2933,7 +2933,7 @@ def test_correlation_rule_update_command(mocker): mock_reply = {"updated_objects": [{"id": "1", "status": "Rule updated successfully"}]} mocker.patch.object(Client, "create_or_update_correlation_rules", return_value=mock_reply) - args = {"rule_id": "1", "name": "test_rule", "severity": "high"} + args = {"rule_id": "1", "name": "test_rule", "severity": "high", "is_enabled": "true"} res = correlation_rule_update_command(client, args) assert res.outputs == {"rule_id": "1"} From 516e79193ee245bc8046f07092503f53e6cb9813 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Thu, 5 Feb 2026 10:59:02 +0200 Subject: [PATCH 19/27] checked all commands again --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 1 - .../Integrations/CortexXDRIR/CortexXDRIR.yml | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 4e905e247917..af6c331d8117 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -1868,7 +1868,6 @@ def correlation_rule_create_or_update_helper(client: Client, args: Dict) -> dict "action": "ALERTS", "timezone": args.get("timezone"), "dataset": args.get("dataset"), - # Uppercase handling for Enum fields "alert_category": args.get("alert_category").upper() if args.get("alert_category") else None, "execution_mode": args.get("execution_mode").upper() if args.get("execution_mode") else None, "mapping_strategy": args.get("mapping_strategy").upper() if args.get("mapping_strategy") else None, diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml index 2408e0e8d5c0..48fad9c94593 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml @@ -4240,7 +4240,7 @@ script: - auto: PREDEFINED description: The BIOC type. name: type - required: true +# required: true predefined: - other - persistence @@ -4266,11 +4266,11 @@ script: - 'false' - description: The BIOC comment. name: comment - required: true +# required: true - auto: PREDEFINED description: The BIOC status. name: status - required: true +# required: true predefined: - enabled - disabled @@ -4321,7 +4321,7 @@ script: - auto: PREDEFINED description: BIOC type. name: type - required: true +# required: true predefined: - other - persistence @@ -4347,11 +4347,11 @@ script: - 'false' - description: BIOC comment. name: comment - required: true +# required: true - auto: PREDEFINED description: BIOC status. name: status - required: true +# required: true predefined: - enabled - disabled From a0607b2e8ec1a08e6020524f45752ae031bb80b4 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Thu, 5 Feb 2026 13:37:21 +0200 Subject: [PATCH 20/27] added unittests --- .../CortexXDRIR/CortexXDRIR_test.py | 410 ++++++++++++++++-- 1 file changed, 377 insertions(+), 33 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py index 74b616b7d917..611864f386ac 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py @@ -2787,14 +2787,44 @@ def test_replace_dots_in_keys(): assert replace_dots_in_keys(123) == 123 -def test_bioc_list_command(mocker): +@pytest.mark.parametrize( + "args, expected_request_data", + [ + ( + {"name": "test_bioc", "severity": "high", "page": "1", "limit": "10", "extra_data": "true"}, + { + "request_data": { + "filters": [ + {"field": "name", "operator": "EQ", "value": "test_bioc"}, + {"field": "severity", "operator": "EQ", "value": "SEV_040_HIGH"}, + ], + "extended_view": True, + "search_from": 10, + "search_to": 20, + } + }, + ), + ( + {}, + { + "request_data": { + "filters": [], + "extended_view": False, + "search_from": 0, + "search_to": 50, + } + }, + ), + ], +) +def test_bioc_list_command(mocker, args, expected_request_data): """ Given: - Arguments for filtering BIOCs. When: - Running bioc_list_command. Then: - - Verify the client.get_biocs is called and results are correct. + - Verify the client.get_biocs is called with correct parameters. """ from CortexXDRIR import Client, bioc_list_command @@ -2802,14 +2832,55 @@ def test_bioc_list_command(mocker): mock_reply = {"objects": [{"rule_id": "1", "name": "test_bioc", "type": "host", "severity": "high", "status": "enabled"}]} mocker.patch.object(Client, "get_biocs", return_value=mock_reply) - args = {"name": "test_bioc", "severity": "high"} res = bioc_list_command(client, args) assert res.outputs == mock_reply["objects"] - assert "test_bioc" in res.readable_output + Client.get_biocs.assert_called_with(expected_request_data) -def test_bioc_create_command(mocker): +@pytest.mark.parametrize( + "args, expected_request_data, expected_output, expected_error", + [ + ( + { + "name": "test_bioc", + "severity": "high", + "type": "bioc_type", + "is_xql": "true", + "comment": "test_comment", + "status": "enabled", + "mitre_technique_id_and_name": "T1000", + "mitre_tactic_id_and_name": "TA0001", + "indicator": '{"field": "value"}', + }, + { + "request_data": [ + { + "rule_id": None, + "name": "test_bioc", + "severity": "SEV_040_HIGH", + "type": "bioc_type", + "is_xql": True, + "comment": "test_comment", + "status": "enabled", + "mitre_technique_id_and_name": "T1000", + "mitre_tactic_id_and_name": "TA0001", + "indicator": {"field": "value"}, + } + ] + }, + {"rule_id": "1", "readable_output": "BIOC created successfully"}, + None, + ), + ( + {"name": "test_bioc", "severity": "high", "indicator": "invalid_json"}, + None, + None, + "Unable to parse 'indicator'. Please use the JSON format.", + ), + ], +) +def test_bioc_create_command(mocker, args, expected_request_data, expected_output, expected_error): """ Given: - Arguments for creating a BIOC. @@ -2824,14 +2895,33 @@ def test_bioc_create_command(mocker): mock_reply = {"added_objects": [{"id": "1", "status": "BIOC created successfully"}]} mocker.patch.object(Client, "insert_or_update_biocs", return_value=mock_reply) - args = {"name": "test_bioc", "severity": "high", "indicator": '{"field": "value"}'} - res = bioc_create_command(client, args) - - assert res.outputs == {"rule_id": "1"} - assert res.readable_output == "BIOC created successfully" + if expected_error: + with pytest.raises(DemistoException) as e: + bioc_create_command(client, args) + assert expected_error in str(e.value) + else: + res = bioc_create_command(client, args) + assert res.outputs == {"rule_id": expected_output["rule_id"]} + assert res.readable_output == expected_output["readable_output"] + Client.insert_or_update_biocs.assert_called_with(expected_request_data) -def test_bioc_update_command(mocker): +@pytest.mark.parametrize( + "mock_reply, expected_output, expected_readable_output", + [ + ( + {"updated_objects": [{"id": "1", "status": "BIOC updated successfully"}]}, + {"rule_id": "1"}, + "BIOC updated successfully", + ), + ( + {"updated_objects": []}, + {}, + "No BIOCs updated.", + ), + ], +) +def test_bioc_update_command(mocker, mock_reply, expected_output, expected_readable_output): """ Given: - Arguments for updating a BIOC. @@ -2843,17 +2933,24 @@ def test_bioc_update_command(mocker): from CortexXDRIR import Client, bioc_update_command client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) - mock_reply = {"updated_objects": [{"id": "1", "status": "BIOC updated successfully"}]} mocker.patch.object(Client, "insert_or_update_biocs", return_value=mock_reply) args = {"rule_id": "1", "name": "test_bioc", "severity": "high"} res = bioc_update_command(client, args) - assert res.outputs == {"rule_id": "1"} - assert res.readable_output == "BIOC updated successfully" + assert res.outputs == expected_output + assert res.readable_output == expected_readable_output -def test_bioc_delete_command(mocker): +@pytest.mark.parametrize( + "mock_reply, expected_output", + [ + ({"objects": ["1"]}, "BIOC with id 1 deleted successfully."), + ({"objects": ["1", "2"]}, "BIOCs with ids 1, 2 deleted successfully."), + ({"objects": []}, "No BIOCs were found to delete."), + ], +) +def test_bioc_delete_command(mocker, mock_reply, expected_output): """ Given: - Arguments for deleting a BIOC. @@ -2865,16 +2962,42 @@ def test_bioc_delete_command(mocker): from CortexXDRIR import Client, bioc_delete_command client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) - mock_reply = {"objects": ["1"]} mocker.patch.object(Client, "delete_biocs", return_value=mock_reply) args = {"name": "test_bioc"} res = bioc_delete_command(client, args) - assert res == "BIOC with id 1 deleted successfully." + assert res == expected_output -def test_correlation_rule_list_command(mocker): +@pytest.mark.parametrize( + "args, expected_request_data", + [ + ( + {"name": "test_rule", "page": "1", "limit": "10", "extra_data": "true"}, + { + "request_data": { + "filters": [{"field": "name", "operator": "EQ", "value": "test_rule"}], + "extended_view": True, + "search_from": 10, + "search_to": 20, + } + }, + ), + ( + {"filter_json": '[{"field": "name", "operator": "EQ", "value": "test_rule"}]'}, + { + "request_data": { + "filters": [{"field": "name", "operator": "EQ", "value": "test_rule"}], + "extended_view": False, + "search_from": 0, + "search_to": 50, + } + }, + ), + ], +) +def test_correlation_rule_list_command(mocker, args, expected_request_data): """ Given: - Arguments for filtering correlation rules. @@ -2889,14 +3012,79 @@ def test_correlation_rule_list_command(mocker): mock_reply = {"objects": [{"rule_id": "1", "name": "test_rule", "description": "desc", "is_enabled": True}]} mocker.patch.object(Client, "get_correlation_rules", return_value=mock_reply) - args = {"name": "test_rule"} res = correlation_rule_list_command(client, args) assert res.outputs == mock_reply["objects"] - assert "test_rule" in res.readable_output + Client.get_correlation_rules.assert_called_with(expected_request_data) -def test_correlation_rule_create_command(mocker): +@pytest.mark.parametrize( + "args, expected_request_data, expected_output", + [ + ( + { + "name": "test_rule", + "severity": "high", + "xql_query": "dataset = xdr_data", + "is_enabled": "true", + "timezone": "UTC", + "dataset": "xdr_data", + "alert_category": "category", + "execution_mode": "real_time", + "mapping_strategy": "strategy", + "description": "desc", + "alert_name": "alert", + "alert_description": "alert_desc", + "search_window": "1h", + "schedule_linux": "* * * * *", + "schedule": "daily", + "drilldown_query_timeframe": "1h", + "investigation_query_link": "link", + "suppression_enabled": "true", + "suppression_duration": "1h", + "suppression_fields": "field1", + "user_defined_severity": "medium", + "user_defined_category": "cat", + "mitre_defs_json": '{"tactic": "test"}', + "alert_fields": '{"field": "value"}', + }, + { + "request_data": [ + { + "rule_id": None, + "name": "test_rule", + "severity": "SEV_040_HIGH", + "xql_query": "dataset = xdr_data", + "is_enabled": True, + "action": "ALERTS", + "timezone": "UTC", + "dataset": "xdr_data", + "alert_category": "CATEGORY", + "execution_mode": "REAL_TIME", + "mapping_strategy": "STRATEGY", + "description": "desc", + "alert_name": "alert", + "alert_description": "alert_desc", + "search_window": "1h", + "crontab": "* * * * *", + "simple_schedule": "daily", + "drilldown_query_timeframe": "1h", + "investigation_query_link": "link", + "suppression_enabled": True, + "suppression_duration": "1h", + "suppression_fields": "field1", + "user_defined_severity": "medium", + "user_defined_category": "cat", + "mitre_defs": {"tactic": "test"}, + "alert_fields": {"field": "value"}, + } + ] + }, + {"rule_id": "1", "readable_output": "Rule created successfully"}, + ) + ], +) +def test_correlation_rule_create_command(mocker, args, expected_request_data, expected_output): """ Given: - Arguments for creating a correlation rule. @@ -2911,14 +3099,55 @@ def test_correlation_rule_create_command(mocker): mock_reply = {"added_objects": [{"id": "1", "status": "Rule created successfully"}]} mocker.patch.object(Client, "create_or_update_correlation_rules", return_value=mock_reply) - args = {"name": "test_rule", "severity": "high", "xql_query": "dataset = xdr_data", "is_enabled": "true"} res = correlation_rule_create_command(client, args) - assert res.outputs == {"rule_id": "1"} - assert res.readable_output == "Rule created successfully" + assert res.outputs == {"rule_id": expected_output["rule_id"]} + assert res.readable_output == expected_output["readable_output"] + Client.create_or_update_correlation_rules.assert_called_with(expected_request_data) -def test_correlation_rule_update_command(mocker): +@pytest.mark.parametrize( + "args, expected_request_data, expected_output", + [ + ( + {"rule_id": "1", "name": "test_rule", "severity": "high", "is_enabled": "true"}, + { + "request_data": [ + { + "rule_id": "1", + "name": "test_rule", + "severity": "SEV_040_HIGH", + "xql_query": None, + "is_enabled": True, + "action": "ALERTS", + "timezone": None, + "dataset": None, + "alert_category": None, + "execution_mode": None, + "mapping_strategy": None, + "description": None, + "alert_name": None, + "alert_description": None, + "search_window": None, + "crontab": None, + "simple_schedule": None, + "drilldown_query_timeframe": None, + "investigation_query_link": None, + "suppression_enabled": None, + "suppression_duration": None, + "suppression_fields": None, + "user_defined_severity": None, + "user_defined_category": None, + "mitre_defs": {}, + "alert_fields": {}, + } + ] + }, + {"rule_id": "1", "readable_output": "Rule updated successfully"}, + ) + ], +) +def test_correlation_rule_update_command(mocker, args, expected_request_data, expected_output): """ Given: - Arguments for updating a correlation rule. @@ -2933,17 +3162,51 @@ def test_correlation_rule_update_command(mocker): mock_reply = {"updated_objects": [{"id": "1", "status": "Rule updated successfully"}]} mocker.patch.object(Client, "create_or_update_correlation_rules", return_value=mock_reply) - args = {"rule_id": "1", "name": "test_rule", "severity": "high", "is_enabled": "true"} res = correlation_rule_update_command(client, args) - assert res.outputs == {"rule_id": "1"} - assert res.readable_output == "Rule updated successfully" + assert res.outputs == {"rule_id": expected_output["rule_id"]} + assert res.readable_output == expected_output["readable_output"] + Client.create_or_update_correlation_rules.assert_called_with(expected_request_data) -def test_correlation_rule_delete_command(mocker): +@pytest.mark.parametrize( + "mock_reply, args, expected_output, expected_filters", + [ + ( + {"objects": ["1"], "objects_count": 1}, + {"rule_id": "1"}, + "Correlation Rule 1 was deleted.", + {"filters": [{"field": "rule_id", "operator": "EQ", "value": 1}]}, + ), + ( + {"objects": ["1", "2"], "objects_count": 2}, + {"rule_id": "1,2"}, + "Correlation Rules 1, 2 were deleted.", + { + "filters": [ + {"field": "rule_id", "operator": "EQ", "value": 1}, + {"field": "rule_id", "operator": "EQ", "value": 2}, + ] + }, + ), + ( + {"objects": [], "objects_count": 0}, + {"rule_id": "1"}, + "Could not find any correlation rules to delete.", + {"filters": [{"field": "rule_id", "operator": "EQ", "value": 1}]}, + ), + ( + {"objects": ["1"], "objects_count": 1}, + {"rule_id": "1,invalid"}, + "Correlation Rule 1 was deleted.", + {"filters": [{"field": "rule_id", "operator": "EQ", "value": 1}]}, + ), + ], +) +def test_correlation_rule_delete_command(mocker, mock_reply, args, expected_output, expected_filters): """ Given: - - Arguments for deleting a correlation rule. + - Arguments for deleting correlation rules. When: - Running correlation_rule_delete_command. Then: @@ -2952,10 +3215,91 @@ def test_correlation_rule_delete_command(mocker): from CortexXDRIR import Client, correlation_rule_delete_command client = Client(base_url=f"{XDR_URL}/public_api/v1", verify=False, timeout=120, proxy=False) - mock_reply = {"objects": ["1"], "objects_count": 1} mocker.patch.object(Client, "delete_correlation_rules", return_value=mock_reply) - args = {"rule_id": "1"} res = correlation_rule_delete_command(client, args) - assert res.readable_output == "Correlation Rule 1 was deleted." + assert res.readable_output == expected_output + Client.delete_correlation_rules.assert_called_with(expected_filters) + + +def test_create_filters_for_bioc_and_correlation_rules_all_fields(): + """ + Given: + - All possible arguments for creating filters. + When: + - Running create_filters_for_bioc_and_correlation_rules. + Then: + - Verify the returned filters list contains all expected filters. + """ + from CortexXDRIR import create_filters_for_bioc_and_correlation_rules + + args = { + "name": "test_name", + "severity": "high", + "type": "bioc_type", + "is_xql": "true", + "comment": "test_comment", + "status": "enabled", + "indicator": "test_indicator", + "mitre_technique_id_and_name": "T1000,T1001", + "mitre_tactic_id_and_name": "TA0001,TA0002", + "xql_query": "test_query", + "is_enabled": "true", + "description": "test_description", + "alert_name": "test_alert_name", + "alert_category": "test_alert_category", + "alert_description": "test_alert_description", + "alert_fields": "field1,field2", + "execution_mode": "real_time", + "search_window": "1h", + "schedule": "daily", + "timezone": "UTC", + "schedule_linux": "* * * * *", + "suppression_enabled": "true", + "suppression_duration": "1h", + "suppression_fields": "field1", + "dataset": "xdr_data", + "user_defined_severity": "medium", + "user_defined_category": "category", + "mitre_defs_json": '{"tactic": "test"}', + "investigation_query_link": "link", + "drilldown_query_timeframe": "1h", + "mapping_strategy": "strategy", + "alert_domain": "domain", + } + + filters = create_filters_for_bioc_and_correlation_rules(args) + + assert {"field": "name", "operator": "EQ", "value": "test_name"} in filters + assert {"field": "severity", "operator": "EQ", "value": "SEV_040_HIGH"} in filters + assert {"field": "type", "operator": "EQ", "value": "bioc_type"} in filters + assert {"field": "is_xql", "operator": "EQ", "value": True} in filters + assert {"field": "comment", "operator": "EQ", "value": "test_comment"} in filters + assert {"field": "status", "operator": "EQ", "value": "enabled"} in filters + assert {"field": "indicator", "operator": "EQ", "value": "test_indicator"} in filters + assert {"field": "mitre_technique_id_and_name", "operator": "EQ", "value": ["T1000", "T1001"]} in filters + assert {"field": "mitre_tactic_id_and_name", "operator": "EQ", "value": ["TA0001", "TA0002"]} in filters + assert {"field": "xql_query", "operator": "EQ", "value": "test_query"} in filters + assert {"field": "is_enabled", "operator": "EQ", "value": True} in filters + assert {"field": "description", "operator": "EQ", "value": "test_description"} in filters + assert {"field": "alert_name", "operator": "EQ", "value": "test_alert_name"} in filters + assert {"field": "alert_category", "operator": "EQ", "value": "test_alert_category"} in filters + assert {"field": "alert_description", "operator": "EQ", "value": "test_alert_description"} in filters + assert {"field": "alert_fields", "operator": "EQ", "value": ["field1", "field2"]} in filters + assert {"field": "execution_mode", "operator": "EQ", "value": "real_time"} in filters + assert {"field": "search_window", "operator": "EQ", "value": "1h"} in filters + assert {"field": "schedule", "operator": "EQ", "value": "daily"} in filters + assert {"field": "timezone", "operator": "EQ", "value": "UTC"} in filters + assert {"field": "schedule_linux", "operator": "EQ", "value": "* * * * *"} in filters + assert {"field": "suppression_enabled", "operator": "EQ", "value": True} in filters + assert {"field": "suppression_duration", "operator": "EQ", "value": "1h"} in filters + assert {"field": "suppression_fields", "operator": "EQ", "value": "field1"} in filters + assert {"field": "dataset", "operator": "EQ", "value": "xdr_data"} in filters + assert {"field": "user_defined_severity", "operator": "EQ", "value": "medium"} in filters + assert {"field": "user_defined_category", "operator": "EQ", "value": "category"} in filters + assert {"field": "mitre_defs", "operator": "EQ", "value": {"tactic": "test"}} in filters + assert {"field": "investigation_query_link", "operator": "EQ", "value": "link"} in filters + assert {"field": "drilldown_query_timeframe", "operator": "EQ", "value": "1h"} in filters + assert {"field": "mapping_strategy", "operator": "EQ", "value": "strategy"} in filters + assert {"field": "alert_domain", "operator": "EQ", "value": "domain"} in filters From 594b18b2c413d6138a34ff638091b06f66d2ef64 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Thu, 5 Feb 2026 13:47:59 +0200 Subject: [PATCH 21/27] fixed unittest --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 12 ++++++++---- .../Integrations/CortexXDRIR/CortexXDRIR_test.py | 7 ++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index af6c331d8117..20acd8555be4 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -1674,11 +1674,11 @@ def bioc_list_command(client: Client, args: Dict) -> CommandResults: extended_view = argToBoolean(args.get("extra_data", False)) request_data = assign_params( - filters=filters, extended_view=extended_view, search_from=page * page_size, search_to=(page + 1) * page_size, ) + request_data["filters"] = filters reply = client.get_biocs({"request_data": request_data}) biocs = reply.get("objects", []) readable_output = tableToMarkdown( @@ -1822,12 +1822,16 @@ def correlation_rule_list_command(client: Client, args: Dict) -> CommandResults: if filter_json := args.get("filter_json"): filters.extend(json.loads(filter_json)) + page = arg_to_number(args.get("page")) or 0 + limit = arg_to_number(args.get("limit")) or 50 + page_size = arg_to_number(args.get("page_size")) or limit + request_data = assign_params( - filters=filters, extended_view=argToBoolean(args.get("extra_data", False)), - search_from=arg_to_number(args.get("page")), - search_to=arg_to_number(args.get("limit")), + search_from=page * page_size, + search_to=(page + 1) * page_size, ) + request_data["filters"] = filters reply = client.get_correlation_rules({"request_data": request_data}) rules = reply.get("objects", []) readable_output = tableToMarkdown( diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py index 611864f386ac..adaf743b67f1 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py @@ -2808,7 +2808,6 @@ def test_replace_dots_in_keys(): {}, { "request_data": { - "filters": [], "extended_view": False, "search_from": 0, "search_to": 50, @@ -2979,8 +2978,8 @@ def test_bioc_delete_command(mocker, mock_reply, expected_output): "request_data": { "filters": [{"field": "name", "operator": "EQ", "value": "test_rule"}], "extended_view": True, - "search_from": 10, - "search_to": 20, + "search_from": 1, + "search_to": 10, } }, ), @@ -2990,8 +2989,6 @@ def test_bioc_delete_command(mocker, mock_reply, expected_output): "request_data": { "filters": [{"field": "name", "operator": "EQ", "value": "test_rule"}], "extended_view": False, - "search_from": 0, - "search_to": 50, } }, ), From f6c865bbb120e8bbf017394603fd7afd24dc94c1 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Thu, 5 Feb 2026 13:51:34 +0200 Subject: [PATCH 22/27] fixed unittest --- Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py index adaf743b67f1..e56fedd5bf9c 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py @@ -2811,6 +2811,7 @@ def test_replace_dots_in_keys(): "extended_view": False, "search_from": 0, "search_to": 50, + "filters": [], } }, ), @@ -2989,6 +2990,8 @@ def test_bioc_delete_command(mocker, mock_reply, expected_output): "request_data": { "filters": [{"field": "name", "operator": "EQ", "value": "test_rule"}], "extended_view": False, + "search_from": 1, + "search_to": 10, } }, ), From 7f2b25910b425cef51f72990fecc1da684d7da4f Mon Sep 17 00:00:00 2001 From: noydavidi Date: Thu, 5 Feb 2026 13:59:39 +0200 Subject: [PATCH 23/27] fixed some pre commit errors --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 14 +++++++++----- .../Integrations/CortexXDRIR/CortexXDRIR_test.py | 8 ++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 20acd8555be4..fbb09b040fa4 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -1711,7 +1711,7 @@ def bioc_create_or_update_helper(client: Client, args: Dict) -> dict: "rule_id": args.get("rule_id"), # required fields "name": args.get("name"), - "severity": BIOC_AND_CR_SEVERITY_MAPPING.get(args.get("severity")), + "severity": BIOC_AND_CR_SEVERITY_MAPPING.get(args.get("severity", "")), # not required but need to be null "type": args.get("type"), "is_xql": argToBoolean(args.get("is_xql")) if args.get("is_xql") else None, @@ -1742,9 +1742,13 @@ def bioc_create_command(client: Client, args: Dict) -> CommandResults: CommandResults: The command results. """ reply = bioc_create_or_update_helper(client, args) - added_objects = reply.get("added_objects") - message = added_objects[0].get("status") - outputs = {"rule_id": added_objects[0].get("id")} + added_objects: List[Dict[str, Any]] = reply.get("added_objects", []) + if added_objects: + message = added_objects[0].get("status") + outputs = {"rule_id": added_objects[0].get("id")} + else: + message = "Could not create the BIOC." + outputs = {} return CommandResults( readable_output=message, @@ -1868,7 +1872,7 @@ def correlation_rule_create_or_update_helper(client: Client, args: Dict) -> dict "name": args.get("name"), "severity": severity_mapped, "xql_query": args.get("xql_query"), - "is_enabled": argToBoolean(args.get("is_enabled")), + "is_enabled": argToBoolean(args.get("is_enabled")) if args.get("is_enabled") else None, "action": "ALERTS", "timezone": args.get("timezone"), "dataset": args.get("dataset"), diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py index e56fedd5bf9c..6c8f558aab9c 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py @@ -2979,8 +2979,8 @@ def test_bioc_delete_command(mocker, mock_reply, expected_output): "request_data": { "filters": [{"field": "name", "operator": "EQ", "value": "test_rule"}], "extended_view": True, - "search_from": 1, - "search_to": 10, + "search_from": 0, + "search_to": 50, } }, ), @@ -2990,8 +2990,8 @@ def test_bioc_delete_command(mocker, mock_reply, expected_output): "request_data": { "filters": [{"field": "name", "operator": "EQ", "value": "test_rule"}], "extended_view": False, - "search_from": 1, - "search_to": 10, + "search_from": 0, + "search_to": 50, } }, ), From 920d9ccad62eb068fe0834a77dbb9d314f3fd165 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Thu, 5 Feb 2026 14:05:59 +0200 Subject: [PATCH 24/27] fixed logic --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index fbb09b040fa4..0425edcdd749 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -1742,12 +1742,11 @@ def bioc_create_command(client: Client, args: Dict) -> CommandResults: CommandResults: The command results. """ reply = bioc_create_or_update_helper(client, args) - added_objects: List[Dict[str, Any]] = reply.get("added_objects", []) - if added_objects: + if added_objects := reply.get("added_objects", []): message = added_objects[0].get("status") outputs = {"rule_id": added_objects[0].get("id")} else: - message = "Could not create the BIOC." + message = "No BIOCs created." outputs = {} return CommandResults( @@ -1876,9 +1875,9 @@ def correlation_rule_create_or_update_helper(client: Client, args: Dict) -> dict "action": "ALERTS", "timezone": args.get("timezone"), "dataset": args.get("dataset"), - "alert_category": args.get("alert_category").upper() if args.get("alert_category") else None, - "execution_mode": args.get("execution_mode").upper() if args.get("execution_mode") else None, - "mapping_strategy": args.get("mapping_strategy").upper() if args.get("mapping_strategy") else None, + "alert_category": args.get("alert_category", "").upper() if args.get("alert_category") else None, + "execution_mode": args.get("execution_mode", "").upper() if args.get("execution_mode") else None, + "mapping_strategy": args.get("mapping_strategy", "").upper() if args.get("mapping_strategy") else None, # Use 'or None' to convert empty strings ("") into JSON null. We must put a value for those fields. "description": args.get("description") or None, "alert_name": args.get("alert_name") or None, @@ -1913,13 +1912,16 @@ def correlation_rule_create_command(client: Client, args: Dict) -> CommandResult CommandResults: The command results. """ reply = correlation_rule_create_or_update_helper(client, args) - added_objects = reply.get("added_objects")[0] if reply.get("added_objects") else {} - rule_id = added_objects.get("id") - message = added_objects.get("status") + if added_objects := reply.get("added_objects", []): + message = added_objects[0].get("status") + outputs = {"rule_id": added_objects[0].get("id")} + else: + message = "No Correlation Rules created." + outputs = {} return CommandResults( readable_output=message, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CorrelationRule", - outputs={"rule_id": rule_id}, + outputs=outputs, raw_response=reply, ) @@ -1935,14 +1937,16 @@ def correlation_rule_update_command(client: Client, args: Dict) -> CommandResult CommandResults: The command results. """ reply = correlation_rule_create_or_update_helper(client, args) - updated_objects = reply.get("updated_objects")[0] if reply.get("updated_objects") else {} - rule_id = updated_objects.get("id") - message = updated_objects.get("status") - + if updated_objects := reply.get("updated_objects"): + message = updated_objects[0].get("status") + outputs = {"rule_id": updated_objects[0].get("id")} + else: + message = "No BIOCs updated." + outputs = {} return CommandResults( readable_output=message, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CorrelationRule", - outputs={"rule_id": rule_id}, + outputs=outputs, raw_response=reply, ) From 3d19edcb69f197eea59c5129f97e2f5834e344a8 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Thu, 5 Feb 2026 14:06:40 +0200 Subject: [PATCH 25/27] fixed logic --- Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 0425edcdd749..abf099a4a483 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -1941,7 +1941,7 @@ def correlation_rule_update_command(client: Client, args: Dict) -> CommandResult message = updated_objects[0].get("status") outputs = {"rule_id": updated_objects[0].get("id")} else: - message = "No BIOCs updated." + message = "No Correlation Rules updated." outputs = {} return CommandResults( readable_output=message, From a5a3b2640bc69f3515ccc39c145490456abcaf25 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Thu, 5 Feb 2026 15:57:38 +0200 Subject: [PATCH 26/27] fixed comments from ai review --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 45 ++++++++---- .../Integrations/CortexXDRIR/CortexXDRIR.yml | 68 +++++-------------- .../CortexXDRIR/command_examples.txt | 2 +- 3 files changed, 48 insertions(+), 67 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index abf099a4a483..639aa4bdfc23 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -50,7 +50,12 @@ XDR_OPEN_STATUS_TO_XSOAR = ["under_investigation", "new"] -BIOC_AND_CR_SEVERITY_MAPPING = {"info": "SEV_010_INFO", "low": "SEV_020_LOW", "medium": "SEV_030_MEDIUM", "high": "SEV_040_HIGH"} +BIOC_AND_CR_SEVERITY_MAPPING = {"info": "SEV_010_INFO", + "low": "SEV_020_LOW", + "medium": "SEV_030_MEDIUM", + "high": "SEV_040_HIGH", + "critical": "SEV_050_CRITICAL" + } def convert_epoch_to_milli(timestamp): @@ -568,7 +573,7 @@ def update_alerts_in_xdr_request(self, alerts_ids, severity, status, comment) -> def get_biocs(self, request_data: dict): reply = self._http_request( method="POST", - url_suffix="/bioc/get/", + url_suffix="/bioc/get", json_data=request_data, ) return reply @@ -576,7 +581,7 @@ def get_biocs(self, request_data: dict): def insert_or_update_biocs(self, request_data): reply = self._http_request( method="POST", - url_suffix="/bioc/insert/", + url_suffix="/bioc/insert", json_data=request_data, ) return reply @@ -592,7 +597,7 @@ def delete_biocs(self, request_data: dict): def get_correlation_rules(self, request_data: dict): reply = self._http_request( method="POST", - url_suffix="/correlations/get/", + url_suffix="/correlations/get", json_data=request_data, ) return reply @@ -609,7 +614,7 @@ def delete_correlation_rules(self, request_data: dict): reply = self._http_request( method="POST", url_suffix="/correlations/delete", - json_data={"request_data": request_data}, + json_data=request_data, ) return reply @@ -1645,7 +1650,11 @@ def create_filters_for_bioc_and_correlation_rules(args: dict) -> list: if user_defined_category := args.get("user_defined_category"): filters.append({"field": "user_defined_category", "operator": "EQ", "value": user_defined_category}) if mitre_defs := args.get("mitre_defs_json"): - filters.append({"field": "mitre_defs", "operator": "EQ", "value": json.loads(mitre_defs)}) + try: + mitre_defs_json = json.loads(mitre_defs) + except ValueError: + raise DemistoException("Unable to parse 'mitre_defs'. Please use the JSON format.") + filters.append({"field": "mitre_defs", "operator": "EQ", "value": mitre_defs_json}) if investigation_query_link := args.get("investigation_query_link"): filters.append({"field": "investigation_query_link", "operator": "EQ", "value": investigation_query_link}) if drilldown_query_timeframe := args.get("drilldown_query_timeframe"): @@ -1691,7 +1700,7 @@ def bioc_list_command(client: Client, args: Dict) -> CommandResults: return CommandResults( readable_output=readable_output, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.BIOC", - outputs_key_field="name", + outputs_key_field="rule_id", outputs=biocs, raw_response=reply, ) @@ -1785,7 +1794,7 @@ def bioc_update_command(client: Client, args: Dict) -> CommandResults: ) -def bioc_delete_command(client: Client, args: Dict) -> str: +def bioc_delete_command(client: Client, args: Dict) -> CommandResults: """ API Docs https://docs-cortex.paloaltonetworks.com/r/Cortex-XDR-Platform-APIs/Delete-BIOCs Deletes a BIOC. @@ -1802,14 +1811,14 @@ def bioc_delete_command(client: Client, args: Dict) -> str: count = len(rule_ids) if count == 1: - return f"BIOC with id {rule_ids[0]} deleted successfully." + return CommandResults(readable_output=f"BIOC with id {rule_ids[0]} deleted successfully.") elif count > 1: ids_str = ", ".join(map(str, rule_ids)) - return f"BIOCs with ids {ids_str} deleted successfully." + return CommandResults(readable_output=f"BIOCs with ids {ids_str} deleted successfully.") else: - return "No BIOCs were found to delete." + return CommandResults(readable_output="No BIOCs were found to delete.") def correlation_rule_list_command(client: Client, args: Dict) -> CommandResults: @@ -1823,7 +1832,11 @@ def correlation_rule_list_command(client: Client, args: Dict) -> CommandResults: """ filters = create_filters_for_bioc_and_correlation_rules(args) if filter_json := args.get("filter_json"): - filters.extend(json.loads(filter_json)) + try: + filter_json = json.loads(filter_json) + filters.extend(filter_json) + except ValueError: + raise DemistoException("Unable to parse 'filter_json'. Please use the JSON format.") page = arg_to_number(args.get("page")) or 0 limit = arg_to_number(args.get("limit")) or 50 @@ -1840,14 +1853,14 @@ def correlation_rule_list_command(client: Client, args: Dict) -> CommandResults: readable_output = tableToMarkdown( name="Correlation Rules List", t=rules, - headers=["rule_id", "name", "description", "is_enabled"], + headers=["rule_id", "name", "severity", "description", "is_enabled"], removeNull=True, headerTransform=string_to_table_header, ) return CommandResults( readable_output=readable_output, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CorrelationRule", - outputs_key_field="id", + outputs_key_field="rule_id", outputs=rules, raw_response=reply, ) @@ -1921,6 +1934,7 @@ def correlation_rule_create_command(client: Client, args: Dict) -> CommandResult return CommandResults( readable_output=message, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CorrelationRule", + outputs_key_field="rule_id", outputs=outputs, raw_response=reply, ) @@ -1946,6 +1960,7 @@ def correlation_rule_update_command(client: Client, args: Dict) -> CommandResult return CommandResults( readable_output=message, outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CorrelationRule", + outputs_key_field="rule_id", outputs=outputs, raw_response=reply, ) @@ -1970,7 +1985,7 @@ def correlation_rule_delete_command(client: Client, args: Dict) -> CommandResult # If rule_id is None, "abc", or "", skip it safely continue - reply = client.delete_correlation_rules({"filters": rule_id_list}) + reply = client.delete_correlation_rules({"request_data": {"filters": rule_id_list}}) deleted_ids = reply.get("objects", []) objects_count = reply.get("objects_count") diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml index 48fad9c94593..48fc8cbe56ea 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml @@ -4143,6 +4143,7 @@ script: - low - medium - high + - critical - auto: PREDEFINED description: 'The BIOC type to filter by.' name: type @@ -4182,7 +4183,7 @@ script: - description: "The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']." isArray: true name: mitre_technique_id_and_name - - description: "The MITRE technique ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']." + - description: "The MITRE tactic ID and name. Must be in format 'ID - Name', for example: ['T1566 - Phishing']." isArray: true name: mitre_tactic_id_and_name - auto: PREDEFINED @@ -4217,13 +4218,13 @@ script: type: String - contextPath: PaloAltoNetworksXDR.BIOC.is_xql description: Whether the BIOC is XQL. - type: Bool + type: Boolean - contextPath: PaloAltoNetworksXDR.BIOC.comment description: The BIOC comment. type: String - contextPath: PaloAltoNetworksXDR.BIOC.indicator description: The BIOC indicator. - type: Unknown + type: String - arguments: - description: The BIOC name. name: name @@ -4237,10 +4238,10 @@ script: - low - medium - high + - critical - auto: PREDEFINED description: The BIOC type. name: type -# required: true predefined: - other - persistence @@ -4266,11 +4267,9 @@ script: - 'false' - description: The BIOC comment. name: comment -# required: true - auto: PREDEFINED description: The BIOC status. name: status -# required: true predefined: - enabled - disabled @@ -4290,17 +4289,8 @@ script: description: Creates a new BIOC. name: xdr-bioc-create outputs: - - contextPath: PaloAltoNetworksXDR.BIOC.name - description: BIOC name. - type: String - - contextPath: PaloAltoNetworksXDR.BIOC.type - description: BIOC type. - type: String - - contextPath: PaloAltoNetworksXDR.BIOC.severity - description: BIOC severity. - type: String - - contextPath: PaloAltoNetworksXDR.BIOC.status - description: BIOC status. + - contextPath: PaloAltoNetworksXDR.BIOC.rule_id + description: BIOC ID. type: String - arguments: - description: BIOC rule ID. @@ -4318,10 +4308,10 @@ script: - low - medium - high + - critical - auto: PREDEFINED description: BIOC type. name: type -# required: true predefined: - other - persistence @@ -4347,11 +4337,9 @@ script: - 'false' - description: BIOC comment. name: comment -# required: true - auto: PREDEFINED description: BIOC status. name: status -# required: true predefined: - enabled - disabled @@ -4371,17 +4359,8 @@ script: description: Updates an existing BIOC. name: xdr-bioc-update outputs: - - contextPath: PaloAltoNetworksXDR.BIOC.name - description: BIOC name. - type: String - - contextPath: PaloAltoNetworksXDR.BIOC.type - description: BIOC type. - type: String - - contextPath: PaloAltoNetworksXDR.BIOC.severity - description: BIOC severity. - type: String - - contextPath: PaloAltoNetworksXDR.BIOC.status - description: BIOC status. + - contextPath: PaloAltoNetworksXDR.BIOC.rule_id + description: BIOC ID. type: String - arguments: - description: BIOC name. @@ -4394,6 +4373,7 @@ script: - low - medium - high + - critical - auto: PREDEFINED description: BIOC type. name: type @@ -4444,6 +4424,7 @@ script: - low - medium - high + - critical - description: Correlation rule XQL query. name: xql_query - auto: PREDEFINED @@ -4479,7 +4460,7 @@ script: description: Returns a list of correlation rules. name: xdr-correlation-rule-list outputs: - - contextPath: PaloAltoNetworksXDR.CorrelationRule.id + - contextPath: PaloAltoNetworksXDR.CorrelationRule.rule_id description: Correlation rule ID. type: String - contextPath: PaloAltoNetworksXDR.CorrelationRule.name @@ -4504,6 +4485,7 @@ script: - low - medium - high + - critical - description: 'The correlation rule XQL query. Example: xql_query="dataset = xdr_data | limit 1"' name: xql_query required: true @@ -4578,6 +4560,7 @@ script: name: suppression_fields - description: 'The correlation rule dataset. Example: dataset=alerts' name: dataset + required: true - description: User-defined severity. name: user_defined_severity - description: User-defined category. @@ -4591,18 +4574,9 @@ script: description: Creates a new correlation rule. name: xdr-correlation-rule-create outputs: - - contextPath: PaloAltoNetworksXDR.CorrelationRule.id + - contextPath: PaloAltoNetworksXDR.CorrelationRule.rule_id description: Correlation rule ID. type: String - - contextPath: PaloAltoNetworksXDR.CorrelationRule.name - description: Correlation rule name. - type: String - - contextPath: PaloAltoNetworksXDR.CorrelationRule.description - description: Correlation rule description. - type: String - - contextPath: PaloAltoNetworksXDR.CorrelationRule.is_enabled - description: Whether the correlation rule is enabled. - type: Boolean - arguments: - description: Correlation rule ID. name: rule_id @@ -4619,6 +4593,7 @@ script: - low - medium - high + - critical - description: 'The correlation rule XQL query. Example: xql_query="dataset = xdr_data | limit 1"' name: xql_query required: true @@ -4709,15 +4684,6 @@ script: - contextPath: PaloAltoNetworksXDR.CorrelationRule.id description: Correlation rule ID. type: String - - contextPath: PaloAltoNetworksXDR.CorrelationRule.name - description: Correlation rule name. - type: String - - contextPath: PaloAltoNetworksXDR.CorrelationRule.description - description: Correlation rule description. - type: String - - contextPath: PaloAltoNetworksXDR.CorrelationRule.is_enabled - description: Whether the correlation rule is enabled. - type: Boolean - arguments: - description: Correlation rule ID. isArray: true diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/command_examples.txt b/Packs/CortexXDR/Integrations/CortexXDRIR/command_examples.txt index b42fae4c5c4d..6266f168b98d 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/command_examples.txt +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/command_examples.txt @@ -65,7 +65,7 @@ !xdr-bioc-list severity=high type=collection !xdr-bioc-create name="TestBIOC" severity=high mitre_tactic_id_and_name="['TA0002 - Execution']" mitre_technique_id_and_name="['T1059 - Command and Scripting Interpreter']" indicator="{\"runOnCGO\":true,\"investigationType\":\"FILE_EVENT\",\"investigation\":{\"FILE_EVENT\":{\"filter\":{\"AND\":[{\"SEARCH_FIELD\":\"action_file_name\",\"SEARCH_TYPE\":\"EQ\",\"SEARCH_VALUE\":\"testfile.exe\"}]}}}}" type=collection status=enabled comment="Test" !xdr-bioc-update name="TestBIOC" severity=high mitre_tactic_id_and_name="['TA0002 - Execution']" mitre_technique_id_and_name="['T1059 - Command and Scripting Interpreter']" indicator="{\"runOnCGO\":true,\"investigationType\":\"FILE_EVENT\",\"investigation\":{\"FILE_EVENT\":{\"filter\":{\"AND\":[{\"SEARCH_FIELD\":\"action_file_name\",\"SEARCH_TYPE\":\"EQ\",\"SEARCH_VALUE\":\"testfile.exe\"}]}}}}" type=collection status=enabled comment="Test" -!xdr-bioc-delete type=collection +!xdr-bioc-delete type=collection name="TestBIOC" !xdr-correlation-rule-list extra_data=true !xdr-correlation-rule-create name="CR test" severity=low alert_category=dropper execution_mode=scheduled mapping_strategy=auto xql_query="dataset = xdr_data | limit 1" dataset=alerts alert_name="Test" is_enabled=true drilldown_query_timeframe="ALERT" investigation_query_link="dataset = xdr_data | limit 1" schedule="10 minutes" schedule_linux="*/10 * * * *" search_window="1 hours" suppression_duration="1 hours" suppression_enabled=true suppression_fields="event_type" timezone="Asia/Jerusalem" !xdr-correlation-rule-update name="CR test" severity=low alert_category=dropper execution_mode=scheduled mapping_strategy=auto xql_query="dataset = xdr_data | limit 1" dataset=alerts alert_name="Test" is_enabled=true drilldown_query_timeframe="ALERT" investigation_query_link="dataset = xdr_data | limit 1" schedule="10 minutes" schedule_linux="*/10 * * * *" search_window="1 hours" suppression_duration="1 hours" suppression_enabled=true suppression_fields="event_type" timezone="Asia/Jerusalem" From b3744f978fa54dba769204907de3fa321cba5bb2 Mon Sep 17 00:00:00 2001 From: noydavidi Date: Thu, 5 Feb 2026 16:12:11 +0200 Subject: [PATCH 27/27] fixed unittests --- .../Integrations/CortexXDRIR/CortexXDRIR.py | 13 ++++++----- .../CortexXDRIR/CortexXDRIR_test.py | 22 ++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py index 639aa4bdfc23..8cb99d50e5ae 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py @@ -50,12 +50,13 @@ XDR_OPEN_STATUS_TO_XSOAR = ["under_investigation", "new"] -BIOC_AND_CR_SEVERITY_MAPPING = {"info": "SEV_010_INFO", - "low": "SEV_020_LOW", - "medium": "SEV_030_MEDIUM", - "high": "SEV_040_HIGH", - "critical": "SEV_050_CRITICAL" - } +BIOC_AND_CR_SEVERITY_MAPPING = { + "info": "SEV_010_INFO", + "low": "SEV_020_LOW", + "medium": "SEV_030_MEDIUM", + "high": "SEV_040_HIGH", + "critical": "SEV_050_CRITICAL", +} def convert_epoch_to_milli(timestamp): diff --git a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py index 6c8f558aab9c..64aefe45bab6 100644 --- a/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py +++ b/Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR_test.py @@ -2967,7 +2967,7 @@ def test_bioc_delete_command(mocker, mock_reply, expected_output): args = {"name": "test_bioc"} res = bioc_delete_command(client, args) - assert res == expected_output + assert res.readable_output == expected_output @pytest.mark.parametrize( @@ -2979,8 +2979,8 @@ def test_bioc_delete_command(mocker, mock_reply, expected_output): "request_data": { "filters": [{"field": "name", "operator": "EQ", "value": "test_rule"}], "extended_view": True, - "search_from": 0, - "search_to": 50, + "search_from": 10, + "search_to": 20, } }, ), @@ -3176,30 +3176,32 @@ def test_correlation_rule_update_command(mocker, args, expected_request_data, ex {"objects": ["1"], "objects_count": 1}, {"rule_id": "1"}, "Correlation Rule 1 was deleted.", - {"filters": [{"field": "rule_id", "operator": "EQ", "value": 1}]}, + {"request_data": {"filters": [{"field": "rule_id", "operator": "EQ", "value": 1}]}}, ), ( {"objects": ["1", "2"], "objects_count": 2}, {"rule_id": "1,2"}, "Correlation Rules 1, 2 were deleted.", { - "filters": [ - {"field": "rule_id", "operator": "EQ", "value": 1}, - {"field": "rule_id", "operator": "EQ", "value": 2}, - ] + "request_data": { + "filters": [ + {"field": "rule_id", "operator": "EQ", "value": 1}, + {"field": "rule_id", "operator": "EQ", "value": 2}, + ] + } }, ), ( {"objects": [], "objects_count": 0}, {"rule_id": "1"}, "Could not find any correlation rules to delete.", - {"filters": [{"field": "rule_id", "operator": "EQ", "value": 1}]}, + {"request_data": {"filters": [{"field": "rule_id", "operator": "EQ", "value": 1}]}}, ), ( {"objects": ["1"], "objects_count": 1}, {"rule_id": "1,invalid"}, "Correlation Rule 1 was deleted.", - {"filters": [{"field": "rule_id", "operator": "EQ", "value": 1}]}, + {"request_data": {"filters": [{"field": "rule_id", "operator": "EQ", "value": 1}]}}, ), ], )