Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,35 @@ class AssetType(str, Enum):
}


# IEEE 754 max safe integer (2^53 - 1). Integers larger than this lose
# numerical precision when parsed as floats during JSON unmarshaling in XSOAR Core.
MAX_SAFE_INT = 9007199254740991


def sanitize_large_ints(obj: Any) -> Any:
"""
Recursively finds integers larger than the JS/Go max safe integer (2^53 - 1)
and converts them to strings to prevent IEEE 754 precision loss in XSOAR Core.

ThreatConnect object IDs can exceed the IEEE 754 safe integer limit
(9,007,199,254,740,991), which causes rounding drift when parsed as floats
during JSON unmarshaling in XSOAR Core. Casting such integers to strings
preserves numerical fidelity so downstream commands can reference the
correct ThreatConnect object IDs.
"""
if isinstance(obj, bool):
# bool is a subclass of int in Python, preserve as-is
return obj
if isinstance(obj, int):
return str(obj) if obj > MAX_SAFE_INT else obj
Comment thread
lironcohen272 marked this conversation as resolved.
if isinstance(obj, dict):
return {k: sanitize_large_ints(v) for k, v in obj.items()}
if isinstance(obj, list):
return [sanitize_large_ints(i) for i in obj]

return obj


class Client(BaseClient):
def __init__(self, api_id: str, api_secret: str, base_url: str, verify: bool = True, proxy: bool = False):
super().__init__(base_url=base_url, proxy=proxy, verify=verify)
Expand All @@ -75,6 +104,10 @@ def make_request(
response = self._http_request(
method=method.value, url_suffix=url_suffix, data=payload, resp_type=responseType, params=params, headers=headers
)

if responseType == "json":
return sanitize_large_ints(response)

return response

def create_header(self, url_suffix: str, method: Method) -> dict:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ configuration:
type: 8
required: false
section: Collect
- display: Incidents Fetch Interval
name: incidentFetchInterval
defaultvalue: '1'
type: 19
required: false
section: Collect
- additionalinfo: Free text box to add comma-separated tags to filter the fetched incidents by.
display: Tags filter for the fetch
name: tags
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -879,3 +879,132 @@ def test_tc_get_indicator_command_by_id(mocker):
call_kwargs = get_indicators_mock.call_args[1]
assert call_kwargs.get("indicator_id") == "99999"
assert call_kwargs.get("summary") == ""


class TestSanitizeLargeInts:
Comment thread
lironcohen272 marked this conversation as resolved.
"""Tests for the sanitize_large_ints helper function.

ThreatConnect object IDs can exceed 2^53 - 1 (9,007,199,254,740,991),
causing IEEE 754 precision loss when parsed as floats in XSOAR Core.
The helper converts such integers to strings while leaving smaller
integers and non-integer values untouched.
"""

def test_small_int_unchanged(self):
"""
Given: An integer at or below the max safe integer.
When: sanitize_large_ints is called.
Then: The value is returned unchanged as an int.
"""
assert sanitize_large_ints(0) == 0
assert sanitize_large_ints(1) == 1
assert sanitize_large_ints(MAX_SAFE_INT) == MAX_SAFE_INT
assert isinstance(sanitize_large_ints(MAX_SAFE_INT), int)

def test_large_int_stringified(self):
"""
Given: An integer greater than the max safe integer.
When: sanitize_large_ints is called.
Then: The value is converted to a string preserving all digits.
"""
large_id = 13510798884070691
result = sanitize_large_ints(large_id)
assert result == "13510798884070691"
assert isinstance(result, str)

def test_bool_preserved(self):
"""
Given: Boolean values (which subclass int in Python).
When: sanitize_large_ints is called.
Then: Booleans are returned unchanged, not converted to int/str.
"""
assert sanitize_large_ints(True) is True
assert sanitize_large_ints(False) is False

def test_non_int_types_unchanged(self):
"""
Given: Non-integer values (str, float, None).
When: sanitize_large_ints is called.
Then: Values are returned unchanged.
"""
assert sanitize_large_ints("abc") == "abc"
assert sanitize_large_ints(1.5) == 1.5
assert sanitize_large_ints(None) is None

def test_nested_dict(self):
"""
Given: A nested dict containing large integer IDs.
When: sanitize_large_ints is called.
Then: All large ints are stringified recursively while other
values remain unchanged.
"""
payload = {
"data": {
"id": 13510798884070691,
"name": "test-group",
"confidence": 75,
"nested": {"indicatorId": 13510798884679648},
}
}
result = sanitize_large_ints(payload)
assert result["data"]["id"] == "13510798884070691"
assert result["data"]["name"] == "test-group"
assert result["data"]["confidence"] == 75
assert result["data"]["nested"]["indicatorId"] == "13510798884679648"

def test_list_of_objects(self):
Comment thread
lironcohen272 marked this conversation as resolved.
"""
Given: A list containing dicts with large integer IDs.
When: sanitize_large_ints is called.
Then: All large ints inside list elements are stringified.
"""
payload = [
{"id": 13510798884679647, "rating": 3},
{"id": 13510798884679645, "rating": 4},
{"id": 12345, "rating": 5},
]
result = sanitize_large_ints(payload)
assert result[0]["id"] == "13510798884679647"
assert result[1]["id"] == "13510798884679645"
assert result[2]["id"] == 12345
assert result[0]["rating"] == 3

def test_list_of_mixed_types(self):
"""
Given: A list containing mixed types (large int, small int, str, float,
bool, None, nested dict, nested list).
When: sanitize_large_ints is called.
Then: Only ints exceeding MAX_SAFE_INT are stringified; all other
types are returned unchanged, and nested containers are
processed recursively.
"""
payload = [
13510798884679647,
12345,
"abc",
1.5,
True,
False,
None,
{"id": 13510798884679648, "name": "nested"},
[13510798884679649, 10],
]
result = sanitize_large_ints(payload)
assert result[0] == "13510798884679647"
assert result[1] == 12345
assert result[2] == "abc"
assert result[3] == 1.5
assert result[4] is True
assert result[5] is False
assert result[6] is None
assert result[7] == {"id": "13510798884679648", "name": "nested"}
assert result[8] == ["13510798884679649", 10]

def test_empty_containers(self):
"""
Given: Empty dict and list inputs.
When: sanitize_large_ints is called.
Then: Empty containers are returned unchanged.
"""
assert sanitize_large_ints({}) == {}
assert sanitize_large_ints([]) == []
6 changes: 6 additions & 0 deletions Packs/ThreatConnect/ReleaseNotes/3_1_24.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

#### Integrations

##### ThreatConnect v3

- Fixed an issue where large **ThreatConnect** object IDs could lose numerical precision when processed by Cortex XSOAR, resulting in incorrect IDs being used by downstream commands. Large integer values are now returned as strings to preserve their accuracy.
2 changes: 1 addition & 1 deletion Packs/ThreatConnect/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "ThreatConnect",
"description": "Threat intelligence platform.",
"support": "xsoar",
"currentVersion": "3.1.23",
"currentVersion": "3.1.24",
"author": "Cortex XSOAR",
"url": "https://www.paloaltonetworks.com/cortex",
"email": "",
Expand Down
Loading