|
38 | 38 | "endpoint_name", |
39 | 39 | "endpoint_status", |
40 | 40 | ] |
| 41 | +ERROR_CODE_MAP = { |
| 42 | + -199: "IP_BLOCK_DISABLED_BY_POLICY", |
| 43 | + -198: "INVALID_IP_ADDRESS", |
| 44 | + -197: "IP_ADDRESS_ALREADY_BLOCKED", |
| 45 | + -196: "IP_ADDRESS_WHITELISTED", |
| 46 | + -195: "IP_ADDRESS_NOT_BLOCKED", |
| 47 | + -194: "IP_ADDRESS_NOT_BLOCKED_BUT_WHITELISTED", |
| 48 | + -193: "IP_IS_LOOPBACK", |
| 49 | + -192: "IPV6_BLOCKING_IS_DISABLED", |
| 50 | + -191: "IP_IS_LOCAL_ADDRESS", |
| 51 | +} |
41 | 52 |
|
42 | 53 |
|
43 | 54 | class Client(CoreClient): |
@@ -87,6 +98,105 @@ def create_indicator_rule_request(self, request_data: Union[dict, str], suffix: |
87 | 98 | ) |
88 | 99 | return reply |
89 | 100 |
|
| 101 | + def _is_endpoint_connected(self, endpoint_id: str) -> bool: |
| 102 | + """ |
| 103 | + Helper method to check if an endpoint is connected |
| 104 | + """ |
| 105 | + endpoint_status = self.get_endpoints(endpoint_id_list=[endpoint_id], status="connected") |
| 106 | + return bool(endpoint_status) |
| 107 | + |
| 108 | + def block_ip_request(self, endpoint_id: str, ip_list: list[str], duration: int) -> list[dict[str, Any]]: |
| 109 | + """ |
| 110 | + Block one or more IPs on a given endpoint and collect action IDs. |
| 111 | + If endpoint disconnected/not exists the group id will be None. |
| 112 | + Args: |
| 113 | + endpoint_id (str): ID of the endpoint to apply the block. |
| 114 | + ip_list (list[str]): IP addresses to block. |
| 115 | + duration (int): Block duration in seconds. |
| 116 | +
|
| 117 | + Returns: |
| 118 | + list[dict]: A list of action records, each containing: |
| 119 | + - ip_address (str): The blocked IP. |
| 120 | + - endpoint_id (str): The endpoint where the block was applied. |
| 121 | + - group_id (str): ID of the block action for status polling. |
| 122 | + """ |
| 123 | + results = [] |
| 124 | + if not self._is_endpoint_connected(endpoint_id): |
| 125 | + demisto.debug(f"Cannot block ip list. Endpoint {endpoint_id} is not connected.") |
| 126 | + return [{"ip_address": ip_address, "group_id": None, "endpoint_id": endpoint_id} for ip_address in ip_list] |
| 127 | + |
| 128 | + for ip_address in ip_list: |
| 129 | + demisto.debug(f"Blocking ip address: {ip_address}") |
| 130 | + response = self._http_request( |
| 131 | + method="POST", |
| 132 | + headers=self._headers, |
| 133 | + url_suffix="/endpoints/block_ip", |
| 134 | + json_data={ |
| 135 | + "request_data": { |
| 136 | + "addresses": [ip_address], |
| 137 | + "endpoint_id": endpoint_id, |
| 138 | + "direction": "both", |
| 139 | + "duration": duration, |
| 140 | + } |
| 141 | + }, |
| 142 | + ) |
| 143 | + group_id = response.get("reply", {}).get("group_action_id") |
| 144 | + demisto.debug(f"Block request for {ip_address} returned with group_id {group_id}") |
| 145 | + results.append( |
| 146 | + { |
| 147 | + "ip_address": ip_address, |
| 148 | + "group_id": group_id, |
| 149 | + "endpoint_id": endpoint_id, |
| 150 | + } |
| 151 | + ) |
| 152 | + |
| 153 | + return results |
| 154 | + |
| 155 | + def fetch_block_status(self, group_id: int, endpoint_id: str) -> tuple[str, str]: |
| 156 | + """ |
| 157 | + Check for status of blocking ip action. |
| 158 | +
|
| 159 | + Args: |
| 160 | + group_id (int): The group id returned from the block request. |
| 161 | + endpoint_id (str): The ID of the endpoint whose block status is being checked. |
| 162 | +
|
| 163 | + Returns: |
| 164 | + tuple[str, str]: |
| 165 | + - status: The returned status from the api. |
| 166 | + - message: The returned error text. |
| 167 | + """ |
| 168 | + if not self._is_endpoint_connected(endpoint_id) or not group_id: |
| 169 | + demisto.debug(f"Cannot fetch status. Endpoint {endpoint_id} is not connected.") |
| 170 | + return "Failure", "Endpoint Disconnected" |
| 171 | + |
| 172 | + if group_id == "INVALID_IP": |
| 173 | + return "Failure", "INVALID_IP" |
| 174 | + |
| 175 | + reply = self.action_status_get(group_id) |
| 176 | + status = reply.get("data", {}).get(endpoint_id) |
| 177 | + error_reasons = reply.get("errorReasons", {}) |
| 178 | + if status == "FAILED": |
| 179 | + reason = error_reasons.get(endpoint_id, {}) |
| 180 | + text = reason.get("errorText") |
| 181 | + |
| 182 | + if not text and reason.get("errorData"): |
| 183 | + try: |
| 184 | + payload = json.loads(reason["errorData"]) |
| 185 | + text = payload.get("errorText") |
| 186 | + except (ValueError, TypeError): |
| 187 | + text = reason["errorData"] |
| 188 | + |
| 189 | + match = re.search(r"error code\s*(-?\d+)", text or "") |
| 190 | + error_number = int(match.group(1)) if match else 0 |
| 191 | + |
| 192 | + demisto.debug(f"Error number {error_number}") |
| 193 | + return "Failure", ERROR_CODE_MAP.get(error_number) or text or "Unknown error" |
| 194 | + |
| 195 | + if status == "COMPLETED_SUCCESSFULLY": |
| 196 | + return "Success", "" |
| 197 | + |
| 198 | + return status or "Unknown", "" |
| 199 | + |
90 | 200 | def get_contributing_event_by_alert_id(self, alert_id: int): |
91 | 201 | """_summary_ |
92 | 202 |
|
@@ -583,6 +693,118 @@ def core_get_contributing_event_command(client: Client, args: Dict) -> CommandRe |
583 | 693 | ) |
584 | 694 |
|
585 | 695 |
|
| 696 | +def polling_block_ip_status(args, client) -> PollResult: |
| 697 | + """ |
| 698 | + Check action status for each endpoint id and ip address. |
| 699 | + Due limitation of the polling each time will check all combinations. |
| 700 | + Will stop polling when all statuses of the requests are Success/Failure. |
| 701 | +
|
| 702 | + Args: |
| 703 | + args (dict): |
| 704 | + ip_list list[str]: IPs to block. |
| 705 | + endpoint_list list[str]: Endpoint IDs. |
| 706 | + duration (int, optional): Block time in seconds (default: 300). |
| 707 | + blocked_list list[str]: Action IDs to poll. |
| 708 | + client (Client): Integration client. |
| 709 | + """ |
| 710 | + polling_queue = argToList(args.get("blocked_list", [])) |
| 711 | + demisto.debug(f"polling queue length:{len(polling_queue)}") |
| 712 | + |
| 713 | + results = [] |
| 714 | + for polled_action in polling_queue: |
| 715 | + demisto.debug( |
| 716 | + f"Polled action: endpoint={polled_action['endpoint_id']}, " |
| 717 | + f"group={polled_action['group_id']}, address={polled_action['ip_address']}" |
| 718 | + ) |
| 719 | + status, message = client.fetch_block_status(polled_action["group_id"], polled_action["endpoint_id"]) |
| 720 | + demisto.debug(f"polled action status:{status}, with message:{message}") |
| 721 | + if status == "Success": |
| 722 | + results.append( |
| 723 | + {"ip_address": polled_action["ip_address"], "endpoint_id": polled_action["endpoint_id"], "reason": "Success"} |
| 724 | + ) |
| 725 | + |
| 726 | + elif status == "Failure": |
| 727 | + results.append( |
| 728 | + { |
| 729 | + "ip_address": polled_action["ip_address"], |
| 730 | + "endpoint_id": polled_action["endpoint_id"], |
| 731 | + "reason": f"{status}: {message}", |
| 732 | + } |
| 733 | + ) |
| 734 | + |
| 735 | + else: |
| 736 | + demisto.debug("Polling continue") |
| 737 | + return PollResult( |
| 738 | + response=None, |
| 739 | + partial_result=CommandResults(readable_output="Blocking in progress..."), |
| 740 | + continue_to_poll=True, |
| 741 | + args_for_next_run=args, |
| 742 | + ) |
| 743 | + |
| 744 | + response = CommandResults( |
| 745 | + readable_output=tableToMarkdown( |
| 746 | + name="Results", |
| 747 | + t=results, |
| 748 | + ), |
| 749 | + outputs_prefix="Core.ip_block_results", |
| 750 | + outputs=results, |
| 751 | + ) |
| 752 | + |
| 753 | + return PollResult(response=response, continue_to_poll=False, args_for_next_run=args) |
| 754 | + |
| 755 | + |
| 756 | +@polling_function("core-block-ip", interval=10, timeout=60, requires_polling_arg=False) |
| 757 | +def core_block_ip_command(args: dict, client: Client) -> PollResult: |
| 758 | + """ |
| 759 | + Send block IP requests for each IP address on each endpoint. |
| 760 | + Polls status of the requests until all status are Success/Failure or until timeout. |
| 761 | +
|
| 762 | + Args: |
| 763 | + args (dict): |
| 764 | + addresses list[str]: IPs to block. |
| 765 | + endpoint_list list[str]: Endpoint IDs. |
| 766 | + duration (int, optional): Block time in seconds (default: 300). |
| 767 | + blocked_list list[dict]: list of dicts each holds 3 fields: ip_address, group_id, endpoint_id |
| 768 | + for polling status of requests only. |
| 769 | + client (Client): client. |
| 770 | +
|
| 771 | + Returns: |
| 772 | + PollResult: Schedules or returns poll status/result. |
| 773 | + """ |
| 774 | + if not args.get("blocked_list"): |
| 775 | + # First call when no block ip request has done |
| 776 | + ip_list = argToList(args.get("addresses", [])) |
| 777 | + endpoint_list = argToList(args.get("endpoint_list", [])) |
| 778 | + duration = arg_to_number(args.get("duration")) or 300 |
| 779 | + |
| 780 | + if duration <= 0 or duration >= 518400: |
| 781 | + raise DemistoException("Duration must be greater than 0 and less than 518,400 minutes (approx 12 months).") |
| 782 | + |
| 783 | + is_ip_list_valid(ip_list) |
| 784 | + |
| 785 | + blocked_list = [] |
| 786 | + |
| 787 | + for endpoint_id in endpoint_list: |
| 788 | + blocked_list.extend(client.block_ip_request(endpoint_id, ip_list, duration)) |
| 789 | + args_for_next_run = {"blocked_list": blocked_list, **args} |
| 790 | + return polling_block_ip_status(args_for_next_run, client) |
| 791 | + else: |
| 792 | + # all other calls after the block ip requests sent |
| 793 | + return polling_block_ip_status(args, client) |
| 794 | + |
| 795 | + |
| 796 | +def is_ip_list_valid(ip_list: list[str]): |
| 797 | + """ |
| 798 | + Validates all the ip addresses. |
| 799 | + Return error in case one of the inputs is invalid. |
| 800 | + Args: |
| 801 | + ip_list (list[str]): list of ip address to check. |
| 802 | + """ |
| 803 | + for ip_address in ip_list: |
| 804 | + if not is_ip_valid(ip_address) and not is_ipv6_valid(ip_address): |
| 805 | + raise DemistoException(f"ip address {ip_address} is invalid") |
| 806 | + |
| 807 | + |
586 | 808 | def main(): # pragma: no cover |
587 | 809 | """ |
588 | 810 | Executes an integration command |
@@ -976,6 +1198,9 @@ def main(): # pragma: no cover |
976 | 1198 | elif command == "core-get-contributing-event": |
977 | 1199 | return_results(core_get_contributing_event_command(client, args)) |
978 | 1200 |
|
| 1201 | + elif command == "core-block-ip": |
| 1202 | + return_results(core_block_ip_command(args, client)) |
| 1203 | + |
979 | 1204 | elif command in PREVALENCE_COMMANDS: |
980 | 1205 | return_results(handle_prevalence_command(client, command, args)) |
981 | 1206 |
|
|
0 commit comments