Skip to content

Commit f327b60

Browse files
almog2296Content Bot
andauthored
Block ip command xdr core (demisto#40559)
* First Commit * Small Fixes * Commit * Added the following functions: core_block_ip_command, block_ip_request, wait_for_block_ip_status, get_block_ip_status and the support for the command core-block-ip in the yml file * Small Changes * Small Changes * Small Changes * Small Changes * Changed to using polling command + added docs and tests * Empty commit to trigger CI * Small Docs Changes * Small Docs Changes * Small Fixes * Fix yml file * Fix yml file * Fix tests * Fix readme * Fix readme * Updated *demisto/google-cloud-storage version * Fixes after review * Fixes after review * Small change * Added error messages foreach error code * Fixed Readme File * Check Valid IP address * add to command description default of duration is 300 * Add note to readme on XDR agent version * Return error if ip address is invalid * Bump pack from version Core to 3.4.8. * Raise instead of reutrn_error * Empty commit * Empty commit --------- Co-authored-by: Content Bot <bot@demisto.com>
1 parent 8687a05 commit f327b60

7 files changed

Lines changed: 662 additions & 3 deletions

File tree

Packs/Core/Integrations/CortexCoreIR/CortexCoreIR.py

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@
3838
"endpoint_name",
3939
"endpoint_status",
4040
]
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+
}
4152

4253

4354
class Client(CoreClient):
@@ -87,6 +98,105 @@ def create_indicator_rule_request(self, request_data: Union[dict, str], suffix:
8798
)
8899
return reply
89100

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+
90200
def get_contributing_event_by_alert_id(self, alert_id: int):
91201
"""_summary_
92202
@@ -583,6 +693,118 @@ def core_get_contributing_event_command(client: Client, args: Dict) -> CommandRe
583693
)
584694

585695

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+
586808
def main(): # pragma: no cover
587809
"""
588810
Executes an integration command
@@ -976,6 +1198,9 @@ def main(): # pragma: no cover
9761198
elif command == "core-get-contributing-event":
9771199
return_results(core_get_contributing_event_command(client, args))
9781200

1201+
elif command == "core-block-ip":
1202+
return_results(core_block_ip_command(args, client))
1203+
9791204
elif command in PREVALENCE_COMMANDS:
9801205
return_results(handle_prevalence_command(client, command, args))
9811206

Packs/Core/Integrations/CortexCoreIR/CortexCoreIR.yml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4286,11 +4286,30 @@ script:
42864286
- contextPath: Core.ContributingEvent.events
42874287
description: The contributing events.
42884288
type: Unknown
4289+
- arguments:
4290+
- description: List of agent IDs that support the operation.
4291+
name: endpoint_list
4292+
required: true
4293+
- description: List of IPv6 or IPv4 addresses to be added to the blocklist.
4294+
name: addresses
4295+
required: true
4296+
- description: Number of minutes to block (Max 518,400). The default is 300.
4297+
name: duration
4298+
- description: The blocked IP address lists required for polling that are not visible to the user.
4299+
name: blocked_list
4300+
hidden: true
4301+
polling: true
4302+
name: core-block-ip
4303+
description: Block malicious or suspicious IP addresses.
4304+
outputs:
4305+
- contextPath: Core.ip_block_results
4306+
description: List of Dictionaries, each containing ip_address, end_point, and reason (status and error message).
4307+
type: List
42894308
runonce: false
42904309
script: '-'
42914310
subtype: python3
42924311
type: python
4293-
dockerimage: demisto/google-cloud-storage:1.0.0.3708421
4312+
dockerimage: demisto/google-cloud-storage:1.0.0.4088897
42944313
tests:
42954314
- No tests
42964315
supportsquickactions: true

0 commit comments

Comments
 (0)