Skip to content

Commit 1c2be46

Browse files
barryyosi-panwxsoar-bot
authored andcommitted
custom close reason support (demisto#35038)
1 parent 16e6f38 commit 1c2be46

File tree

15 files changed

+237
-87
lines changed

15 files changed

+237
-87
lines changed

Packs/ApiModules/Scripts/CoreIRApiModule/CoreIRApiModule.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2968,23 +2968,23 @@ def resolve_xdr_close_reason(xsoar_close_reason: str) -> str:
29682968
"""
29692969
# Initially setting the close reason according to the default mapping.
29702970
xdr_close_reason = XSOAR_RESOLVED_STATUS_TO_XDR.get(xsoar_close_reason, 'resolved_other')
2971+
29712972
# Reading custom XSOAR->XDR close-reason mapping.
29722973
custom_xsoar_to_xdr_close_reason_mapping = comma_separated_mapping_to_dict(
29732974
demisto.params().get("custom_xsoar_to_xdr_close_reason_mapping")
29742975
)
29752976

29762977
# Overriding default close-reason mapping if there exists a custom one.
29772978
if xsoar_close_reason in custom_xsoar_to_xdr_close_reason_mapping:
2978-
xdr_close_reason_candidate = custom_xsoar_to_xdr_close_reason_mapping[xsoar_close_reason]
2979+
xdr_close_reason_candidate = custom_xsoar_to_xdr_close_reason_mapping.get(xsoar_close_reason)
29792980
# Transforming resolved close-reason into snake_case format with known prefix to match XDR status format.
2980-
demisto.debug(
2981-
f"resolve_xdr_close_reason XSOAR->XDR custom close-reason exists, using {xsoar_close_reason}={xdr_close_reason}")
29822981
xdr_close_reason_candidate = "resolved_" + "_".join(xdr_close_reason_candidate.lower().split(" "))
2983-
29842982
if xdr_close_reason_candidate not in XDR_RESOLVED_STATUS_TO_XSOAR:
29852983
demisto.debug("Warning: Provided XDR close-reason does not exist. Using default XDR close-reason mapping. ")
29862984
else:
29872985
xdr_close_reason = xdr_close_reason_candidate
2986+
demisto.debug(
2987+
f"resolve_xdr_close_reason XSOAR->XDR custom close-reason exists, using {xsoar_close_reason}={xdr_close_reason}")
29882988
else:
29892989
demisto.debug(f"resolve_xdr_close_reason using default mapping {xsoar_close_reason}={xdr_close_reason}")
29902990

Packs/ApiModules/Scripts/CoreIRApiModule/CoreIRApiModule.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ comment: Common Core IR Client, provides generic Infrastructure.
1212
scripttarget: 0
1313
dependson: {}
1414
timeout: 0s
15-
dockerimage: demisto/python3:3.10.14.99865
15+
dockerimage: demisto/python3:3.11.9.101916
1616
fromversion: 5.0.0
1717
tests:
1818
- No tests (auto formatted)

Packs/Base/ReleaseNotes/1_34_27.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
#### Scripts
3+
4+
##### CommonServerPython
5+
6+
Added a functionality to read server configuration.

Packs/Base/Scripts/CommonServerPython/CommonServerPython.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12021,6 +12021,36 @@ def is_time_sensitive():
1202112021
return hasattr(demisto, 'isTimeSensitive') and demisto.isTimeSensitive()
1202212022

1202312023

12024+
def parse_json_string(json_string):
12025+
"""
12026+
Parse a JSON string into a Python dictionary.
12027+
12028+
:type json_string: ``str``
12029+
:param json_string: The JSON string to be parsed.
12030+
12031+
:rtype: ``dict``
12032+
:return: A Python dictionary representing the parsed JSON data.
12033+
"""
12034+
try:
12035+
data = json.loads(json_string)
12036+
return data
12037+
except json.JSONDecodeError as error: # type: ignore[attr-defined]
12038+
demisto.error("Error decoding JSON: {error}".format(error=error))
12039+
return {}
12040+
12041+
12042+
def get_server_config():
12043+
"""
12044+
Retrieves XSOAR server configuration.
12045+
12046+
:rtype: ``dict``
12047+
:return: The XSOAR server configuration.
12048+
"""
12049+
response = demisto.internalHttpRequest(method='GET', uri='/system/config')
12050+
body = parse_json_string(response.get('body'))
12051+
server_config = body.get('sysConf', {})
12052+
return server_config
12053+
1202412054
from DemistoClassApiModule import * # type:ignore [no-redef] # noqa:E402
1202512055

1202612056

Packs/Base/Scripts/CommonServerPython/CommonServerPython_test.py

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,21 @@
1818

1919
import CommonServerPython
2020
import demistomock as demisto
21-
from CommonServerPython import xml2json, json2xml, entryTypes, formats, tableToMarkdown, underscoreToCamelCase, \
22-
flattenCell, date_to_timestamp, datetime, timedelta, camelize, pascalToSpace, argToList, \
23-
remove_nulls_from_dictionary, is_error, get_error, hash_djb2, fileResult, is_ip_valid, get_demisto_version, \
24-
IntegrationLogger, parse_date_string, IS_PY3, PY_VER_MINOR, DebugLogger, b64_encode, parse_date_range, \
25-
return_outputs, is_filename_valid, convert_dict_values_bytes_to_str, \
26-
argToBoolean, ipv4Regex, ipv4cidrRegex, ipv6cidrRegex, urlRegex, ipv6Regex, domainRegex, batch, FeedIndicatorType, \
27-
encode_string_results, safe_load_json, remove_empty_elements, aws_table_to_markdown, is_demisto_version_ge, \
28-
appendContext, auto_detect_indicator_type, handle_proxy, get_demisto_version_as_str, get_x_content_info_headers, \
29-
url_to_clickable_markdown, WarningsHandler, DemistoException, SmartGetDict, JsonTransformer, \
30-
remove_duplicates_from_list_arg, DBotScoreType, DBotScoreReliability, Common, send_events_to_xsiam, ExecutionMetrics, \
31-
response_to_context, is_integration_command_execution, is_xsiam_or_xsoar_saas, is_xsoar, is_xsoar_on_prem, \
32-
is_xsoar_hosted, is_xsoar_saas, is_xsiam, send_data_to_xsiam, censor_request_logs, censor_request_logs, safe_sleep
21+
from CommonServerPython import (xml2json, json2xml, entryTypes, formats, tableToMarkdown, underscoreToCamelCase,
22+
flattenCell, date_to_timestamp, datetime, timedelta, camelize, pascalToSpace, argToList,
23+
remove_nulls_from_dictionary, is_error, get_error, hash_djb2, fileResult, is_ip_valid,
24+
get_demisto_version, IntegrationLogger, parse_date_string, IS_PY3, PY_VER_MINOR, DebugLogger,
25+
b64_encode, parse_date_range, return_outputs, is_filename_valid, convert_dict_values_bytes_to_str,
26+
argToBoolean, ipv4Regex, ipv4cidrRegex, ipv6cidrRegex, urlRegex, ipv6Regex, domainRegex, batch,
27+
FeedIndicatorType, encode_string_results, safe_load_json, remove_empty_elements,
28+
aws_table_to_markdown, is_demisto_version_ge, appendContext, auto_detect_indicator_type,
29+
handle_proxy, get_demisto_version_as_str, get_x_content_info_headers, url_to_clickable_markdown,
30+
WarningsHandler, DemistoException, SmartGetDict, JsonTransformer, remove_duplicates_from_list_arg,
31+
DBotScoreType, DBotScoreReliability, Common, send_events_to_xsiam, ExecutionMetrics,
32+
response_to_context, is_integration_command_execution, is_xsiam_or_xsoar_saas, is_xsoar,
33+
is_xsoar_on_prem, is_xsoar_hosted, is_xsoar_saas, is_xsiam, send_data_to_xsiam,
34+
censor_request_logs, censor_request_logs, safe_sleep, get_server_config
35+
)
3336

3437
EVENTS_LOG_ERROR = \
3538
"""Error sending new events into XSIAM.
@@ -9769,3 +9772,50 @@ def test_sleep_mocked_time(mocker):
97699772

97709773
# Verify sleep duration based on mocked time difference
97719774
assert sleep_mocker.call_count == 2
9775+
9776+
9777+
def test_get_server_config(mocker):
9778+
mock_response = {
9779+
'body': '{"sysConf":{"incident.closereasons":"CustomReason1, CustomReason 2, Foo","versn":40},"defaultMap":{}}\n',
9780+
'headers': {
9781+
'Content-Length': ['104'],
9782+
'X-Xss-Protection': ['1; mode=block'],
9783+
'X-Content-Type-Options': ['nosniff'],
9784+
'Strict-Transport-Security': ['max-age=10886400000000000; includeSubDomains'],
9785+
'Vary': ['Accept-Encoding'],
9786+
'Server-Timing': ['7'],
9787+
'Date': ['Wed, 03 Jul 2010 09:11:35 GMT'],
9788+
'X-Frame-Options': ['DENY'],
9789+
'Content-Type': ['application/json']
9790+
},
9791+
'status': '200 OK',
9792+
'statusCode': 200
9793+
}
9794+
9795+
mocker.patch.object(demisto, 'internalHttpRequest', return_value=mock_response)
9796+
server_config = get_server_config()
9797+
assert server_config == {'incident.closereasons': 'CustomReason1, CustomReason 2, Foo', 'versn': 40}
9798+
9799+
9800+
def test_get_server_config_fail(mocker):
9801+
mock_response = {
9802+
'body': 'NOT A VALID JSON',
9803+
'headers': {
9804+
'Content-Length': ['104'],
9805+
'X-Xss-Protection': ['1; mode=block'],
9806+
'X-Content-Type-Options': ['nosniff'],
9807+
'Strict-Transport-Security': ['max-age=10886400000000000; includeSubDomains'],
9808+
'Vary': ['Accept-Encoding'],
9809+
'Server-Timing': ['7'],
9810+
'Date': ['Wed, 03 Jul 2010 09:11:35 GMT'],
9811+
'X-Frame-Options': ['DENY'],
9812+
'Content-Type': ['application/json']
9813+
},
9814+
'status': '200 OK',
9815+
'statusCode': 200
9816+
}
9817+
9818+
mocker.patch.object(demisto, 'internalHttpRequest', return_value=mock_response)
9819+
mocked_error = mocker.patch.object(demisto, 'error')
9820+
assert get_server_config() == {}
9821+
assert mocked_error.call_args[0][0] == 'Error decoding JSON: Expecting value: line 1 column 1 (char 0)'

Packs/Base/pack_metadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "Base",
33
"description": "The base pack for Cortex XSOAR.",
44
"support": "xsoar",
5-
"currentVersion": "1.34.26",
5+
"currentVersion": "1.34.27",
66
"author": "Cortex XSOAR",
77
"serverMinVersion": "6.0.0",
88
"url": "https://www.paloaltonetworks.com/cortex",

Packs/Core/ReleaseNotes/3_0_50.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
#### Integrations
3+
4+
##### Indicators detection
5+
6+
Updated the CoreIRApiModule with support for custom XSOAR close-reasons in XSOAR-XDR close-reason mapping.
7+
8+
##### Investigation & Response
9+
10+
Updated the CoreIRApiModule with support for custom XSOAR close-reasons in XSOAR-XDR close-reason mapping.
11+

Packs/Core/pack_metadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "Core - Investigation and Response",
33
"description": "Automates incident response",
44
"support": "xsoar",
5-
"currentVersion": "3.0.49",
5+
"currentVersion": "3.0.50",
66
"author": "Cortex XSOAR",
77
"url": "https://www.paloaltonetworks.com/cortex",
88
"email": "",

Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py

Lines changed: 64 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,64 @@ def filter_and_save_unseen_incident(incidents: List, limit: int, number_of_alrea
130130
return filtered_incidents
131131

132132

133+
def get_xsoar_close_reasons():
134+
"""
135+
Get the default XSOAR close-reasons in addition to custom close-reasons from server configuration.
136+
"""
137+
default_xsoar_close_reasons = list(XSOAR_RESOLVED_STATUS_TO_XDR.keys())
138+
custom_close_reasons = []
139+
try:
140+
server_config = get_server_config()
141+
demisto.debug(f'get_xsoar_close_reasons server-config: {str(server_config)}')
142+
if server_config:
143+
custom_close_reasons: list = argToList(server_config.get('incident.closereasons', ''))
144+
except Exception as e:
145+
demisto.error(f"Could not get server configuration: {e}")
146+
return default_xsoar_close_reasons + custom_close_reasons
147+
148+
149+
def validate_custom_close_reasons_mapping(mapping: str, direction: str):
150+
""" Check validity of provided custom close-reason mappings. """
151+
152+
xdr_statuses = [status.replace("resolved_", "").replace("_", " ").title() for status in XDR_RESOLVED_STATUS_TO_XSOAR]
153+
xsoar_statuses = get_xsoar_close_reasons()
154+
155+
exception_message = ('Improper custom mapping ({direction}) provided: "{key_or_value}" is not a valid Cortex '
156+
'{xsoar_or_xdr} close-reason. Valid Cortex {xsoar_or_xdr} close-reasons are: {statuses}')
157+
158+
def to_xdr_status(status):
159+
return "resolved_" + "_".join(status.lower().split(" "))
160+
161+
custom_mapping = comma_separated_mapping_to_dict(mapping)
162+
163+
valid_key = valid_value = True # If no mapping was provided.
164+
165+
for key, value in custom_mapping.items():
166+
if direction == XSOAR_TO_XDR:
167+
xdr_close_reason = to_xdr_status(value)
168+
valid_key = key in xsoar_statuses
169+
valid_value = xdr_close_reason in XDR_RESOLVED_STATUS_TO_XSOAR
170+
elif direction == XDR_TO_XSOAR:
171+
xdr_close_reason = to_xdr_status(key)
172+
valid_key = xdr_close_reason in XDR_RESOLVED_STATUS_TO_XSOAR
173+
valid_value = value in xsoar_statuses
174+
175+
if not valid_key:
176+
raise DemistoException(
177+
exception_message.format(direction=direction,
178+
key_or_value=key,
179+
xsoar_or_xdr="XSOAR" if direction == XSOAR_TO_XDR else "XDR",
180+
statuses=xsoar_statuses
181+
if direction == XSOAR_TO_XDR else xdr_statuses))
182+
elif not valid_value:
183+
raise DemistoException(
184+
exception_message.format(direction=direction,
185+
key_or_value=value,
186+
xsoar_or_xdr="XDR" if direction == XSOAR_TO_XDR else "XSOAR",
187+
statuses=xdr_statuses
188+
if direction == XSOAR_TO_XDR else xsoar_statuses))
189+
190+
133191
class Client(CoreClient):
134192
def __init__(self, base_url, proxy, verify, timeout, params=None):
135193
if not params:
@@ -157,54 +215,12 @@ def test_module(self, first_fetch_time):
157215
raise
158216

159217
# XSOAR -> XDR
160-
self.validate_custom_mapping(mapping=self._params.get("custom_xsoar_to_xdr_close_reason_mapping"),
161-
direction=XSOAR_TO_XDR)
218+
validate_custom_close_reasons_mapping(mapping=self._params.get("custom_xsoar_to_xdr_close_reason_mapping"),
219+
direction=XSOAR_TO_XDR)
162220

163221
# XDR -> XSOAR
164-
self.validate_custom_mapping(mapping=self._params.get("custom_xdr_to_xsoar_close_reason_mapping"),
165-
direction=XDR_TO_XSOAR)
166-
167-
def validate_custom_mapping(self, mapping: str, direction: str):
168-
""" Check validity of provided custom close-reason mappings. """
169-
170-
xdr_statuses_to_xsoar = [status.replace("resolved_", "").replace("_", " ").title()
171-
for status in XDR_RESOLVED_STATUS_TO_XSOAR]
172-
xsoar_statuses_to_xdr = list(XSOAR_RESOLVED_STATUS_TO_XDR.keys())
173-
174-
exception_message = ('Improper custom mapping ({direction}) provided: "{key_or_value}" is not a valid Cortex '
175-
'{xsoar_or_xdr} close-reason. Valid Cortex {xsoar_or_xdr} close-reasons are: {statuses}')
176-
177-
def to_xdr_status(status):
178-
return "resolved_" + "_".join(status.lower().split(" "))
179-
180-
custom_mapping = comma_separated_mapping_to_dict(mapping)
181-
182-
valid_key = valid_value = True # If no mapping was provided.
183-
184-
for key, value in custom_mapping.items():
185-
if direction == XSOAR_TO_XDR:
186-
xdr_close_reason = to_xdr_status(value)
187-
valid_key = key in XSOAR_RESOLVED_STATUS_TO_XDR
188-
valid_value = xdr_close_reason in XDR_RESOLVED_STATUS_TO_XSOAR
189-
elif direction == XDR_TO_XSOAR:
190-
xdr_close_reason = to_xdr_status(key)
191-
valid_key = xdr_close_reason in XDR_RESOLVED_STATUS_TO_XSOAR
192-
valid_value = value in XSOAR_RESOLVED_STATUS_TO_XDR
193-
194-
if not valid_key:
195-
raise DemistoException(
196-
exception_message.format(direction=direction,
197-
key_or_value=key,
198-
xsoar_or_xdr="XSOAR" if direction == XSOAR_TO_XDR else "XDR",
199-
statuses=xsoar_statuses_to_xdr
200-
if direction == XSOAR_TO_XDR else xdr_statuses_to_xsoar))
201-
elif not valid_value:
202-
raise DemistoException(
203-
exception_message.format(direction=direction,
204-
key_or_value=value,
205-
xsoar_or_xdr="XDR" if direction == XSOAR_TO_XDR else "XSOAR",
206-
statuses=xdr_statuses_to_xsoar
207-
if direction == XSOAR_TO_XDR else xsoar_statuses_to_xdr))
222+
validate_custom_close_reasons_mapping(mapping=self._params.get("custom_xdr_to_xsoar_close_reason_mapping"),
223+
direction=XDR_TO_XSOAR)
208224

209225
def handle_fetch_starred_incidents(self, limit: int, page_number: int, request_data: dict) -> List:
210226
"""
@@ -789,6 +805,7 @@ def resolve_xsoar_close_reason(xdr_close_reason: str):
789805
:param xdr_close_reason: XDR raw status/close reason e.g. 'resolved_false_positive'.
790806
:return: XSOAR close reason.
791807
"""
808+
possible_xsoar_close_reasons = get_xsoar_close_reasons()
792809

793810
# Check if incoming XDR close-reason has a non-default mapping to XSOAR close-reason.
794811
if demisto.params().get("custom_xdr_to_xsoar_close_reason_mapping"):
@@ -802,7 +819,7 @@ def resolve_xsoar_close_reason(xdr_close_reason: str):
802819
xdr_close_reason.replace("resolved_", "").replace("_", " ").title()
803820
)
804821
xsoar_close_reason = custom_xdr_to_xsoar_close_reason_mapping.get(title_cased_xdr_close_reason)
805-
if xsoar_close_reason in XSOAR_RESOLVED_STATUS_TO_XDR:
822+
if xsoar_close_reason in possible_xsoar_close_reasons:
806823
demisto.debug(
807824
f"XDR->XSOAR custom close-reason exists, using {xdr_close_reason}={xsoar_close_reason}"
808825
)

Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3547,7 +3547,7 @@ script:
35473547
Update one or more alerts with the provided arguments.
35483548
Required license: Cortex XDR Prevent, Cortex XDR Pro per Endpoint, or Cortex XDR Pro per GB.
35493549
name: xdr-update-alert
3550-
dockerimage: demisto/python3:3.10.14.99865
3550+
dockerimage: demisto/python3:3.11.9.101916
35513551
isfetch: true
35523552
isfetch:xpanse: false
35533553
script: ''

0 commit comments

Comments
 (0)