Skip to content

Commit 3739c65

Browse files
Nbensalmon/xsup 56190 falcon fetch bug (#41381)
Fixed an issue where fetching detection entities could fail if the number of detection IDs exceeded the API’s maximum per request.
1 parent 69df8fd commit 3739c65

File tree

5 files changed

+2613
-12
lines changed

5 files changed

+2613
-12
lines changed

Packs/CrowdStrikeFalcon/Integrations/CrowdStrikeFalcon/CrowdStrikeFalcon.py

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,9 @@
6969
FETCH_TIME = "now" if demisto.command() == "fetch-events" else PARAMS.get("fetch_time", "3 days")
7070

7171
MAX_FETCH_SIZE = 10000
72-
MAX_FETCH_DETECTION_PER_API_CALL = 10000
73-
MAX_FETCH_INCIDENT_PER_API_CALL = 500
72+
MAX_FETCH_DETECTION_PER_API_CALL = 10000 # fetch limit for get ids call - detections
73+
MAX_FETCH_DETECTION_PER_API_CALL_ENTITY = 1000 # fetch limit for get entities call - detections
74+
MAX_FETCH_INCIDENT_PER_API_CALL = 500 # fetch limit for get ids call - incidents
7475

7576
BYTE_CREDS = f"{CLIENT_ID}:{SECRET}".encode()
7677

@@ -1708,13 +1709,31 @@ def get_detections_entities(detections_ids: list):
17081709
:param detections_ids: IDs of the requested detections.
17091710
:return: Response json of the get detection entities endpoint (detection objects)
17101711
"""
1711-
ids_json = {"ids": detections_ids} if LEGACY_VERSION else {"composite_ids": detections_ids}
1712+
if not detections_ids:
1713+
return detections_ids
1714+
1715+
combined_resources = []
1716+
17121717
url = "/detects/entities/summaries/GET/v1" if LEGACY_VERSION else "/alerts/entities/alerts/v2"
1713-
demisto.debug(f"Getting detections entities from {url} with {ids_json=}. {LEGACY_VERSION=}")
1714-
if detections_ids:
1718+
1719+
# Iterate through the detections_ids list in chunks of 1000 (According to API documentation).
1720+
for i in range(0, len(detections_ids), MAX_FETCH_DETECTION_PER_API_CALL_ENTITY):
1721+
batch_ids = detections_ids[i : i + MAX_FETCH_DETECTION_PER_API_CALL_ENTITY]
1722+
1723+
ids_json = {"ids": batch_ids} if LEGACY_VERSION else {"composite_ids": batch_ids}
1724+
demisto.debug(
1725+
f"Getting detections entities from {url} with {ids_json=} " f"with batch_ids len {len(batch_ids)}. {LEGACY_VERSION=}"
1726+
)
1727+
1728+
# Make the API call with the current batch.
17151729
response = http_request("POST", url, data=json.dumps(ids_json))
1716-
return response
1717-
return detections_ids
1730+
1731+
if "resources" in response:
1732+
# Combine the resources from each response.
1733+
combined_resources.extend(response["resources"])
1734+
1735+
# Return the combined result.
1736+
return {"resources": combined_resources}
17181737

17191738

17201739
def get_incidents_ids(
@@ -1791,11 +1810,27 @@ def get_detection_entities(incidents_ids: list):
17911810
:return: The response.
17921811
:rtype ``dict``
17931812
"""
1813+
combined_resources = []
1814+
17941815
url_endpoint_version = "v1" if LEGACY_VERSION else "v2"
1795-
ids_json = {"ids": incidents_ids} if LEGACY_VERSION else {"composite_ids": incidents_ids}
1796-
demisto.debug(f"In get_detection_entities: Getting detection entities from\
1797-
{url_endpoint_version} with {ids_json=}. {LEGACY_VERSION=}")
1798-
return http_request("POST", f"/alerts/entities/alerts/{url_endpoint_version}", data=json.dumps(ids_json))
1816+
url = f"/alerts/entities/alerts/{url_endpoint_version}"
1817+
1818+
for i in range(0, len(incidents_ids), MAX_FETCH_DETECTION_PER_API_CALL_ENTITY):
1819+
batch_ids = incidents_ids[i : i + MAX_FETCH_DETECTION_PER_API_CALL_ENTITY]
1820+
1821+
ids_json = {"ids": batch_ids} if LEGACY_VERSION else {"composite_ids": batch_ids}
1822+
demisto.debug(f"In get_detection_entities: Getting detection entities from\
1823+
{url_endpoint_version} with {ids_json=} and with batch_ids len {len(batch_ids)} . {LEGACY_VERSION=}")
1824+
1825+
# Make the API call with the current batch.
1826+
raw_res = http_request("POST", url, data=json.dumps(ids_json))
1827+
1828+
if "resources" in raw_res:
1829+
# Combine the resources from each response.
1830+
combined_resources.extend(raw_res["resources"])
1831+
1832+
# Return the combined result.
1833+
return {"resources": combined_resources}
17991834

18001835

18011836
def get_users(offset: int, limit: int, query_filter: str | None = None) -> dict:

Packs/CrowdStrikeFalcon/Integrations/CrowdStrikeFalcon/CrowdStrikeFalcon_test.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4451,6 +4451,62 @@ def test_get_remote_detection_data_for_multiple_types(mocker, detection_type, in
44514451
}
44524452

44534453

4454+
@pytest.mark.parametrize(
4455+
"function_name, num_ids, expected_calls",
4456+
[
4457+
# These are two different functions - calling both of them to verify the call architecture applies to both.
4458+
("get_detections_entities", 0, 0), # Edge case: no IDs
4459+
("get_detections_entities", 500, 1), # Less than the limit
4460+
("get_detections_entities", 1000, 1), # Exactly the limit
4461+
("get_detections_entities", 1001, 2), # More than the limit (specific request)
4462+
("get_detections_entities", 2500, 3), # More than the limit (many calls)
4463+
("get_detection_entities", 0, 0),
4464+
("get_detection_entities", 500, 1),
4465+
("get_detection_entities", 1000, 1),
4466+
("get_detection_entities", 1001, 2),
4467+
("get_detection_entities", 2500, 3),
4468+
],
4469+
)
4470+
def test_get_detections_entities_batches_requests(mocker, function_name, num_ids, expected_calls):
4471+
"""
4472+
Given
4473+
- Number of ID's to fetch from the CrowdStrike Falcon Entity API.
4474+
When
4475+
- Running fetch detections entities functions with the ID's list
4476+
Then
4477+
- Return the number of calls to http_request based on the number of IDs provided.
4478+
- Return the number of resources based on the number of IDs provided.
4479+
"""
4480+
import CrowdStrikeFalcon
4481+
4482+
mock_http_request = mocker.patch.object(CrowdStrikeFalcon, "http_request")
4483+
4484+
# Configure a side effect to return responses with the correct number of resources
4485+
def side_effect(method, url, data):
4486+
data_dict = json.loads(data)
4487+
ids_in_batch = data_dict.get("composite_ids") or data_dict.get("ids")
4488+
return {"meta": {"trace_id": "test_trace"}, "resources": [{"id": i} for i in ids_in_batch]}
4489+
4490+
mock_http_request.side_effect = side_effect
4491+
4492+
# Get the function to test dynamically
4493+
function_to_test = getattr(CrowdStrikeFalcon, function_name)
4494+
4495+
# Load and slice the IDs list from the JSON file
4496+
id_list_full = load_json("./test_data/mock_detections_id_list.json").get("Ids", {})[:num_ids]
4497+
detections_ids = id_list_full[:num_ids]
4498+
4499+
# Call the function with the mock IDs
4500+
result = function_to_test(detections_ids)
4501+
4502+
# Check that the number of http_request calls matches the expectation
4503+
assert mock_http_request.call_count == expected_calls
4504+
4505+
# Check that the number of resources returned matches the number of IDs
4506+
if "resources" in result:
4507+
assert len(result["resources"]) == num_ids
4508+
4509+
44544510
@pytest.mark.parametrize("updated_object, entry_content, close_incident", input_data.set_xsoar_incident_entries_args)
44554511
def test_set_xsoar_entries__incident(mocker, updated_object, entry_content, close_incident):
44564512
"""

0 commit comments

Comments
 (0)