Skip to content

Commit

Permalink
custom close reason support (demisto#35038)
Browse files Browse the repository at this point in the history
  • Loading branch information
barryyosi-panw authored Jul 9, 2024
1 parent 2b0cf5f commit fc3b955
Show file tree
Hide file tree
Showing 15 changed files with 237 additions and 87 deletions.
8 changes: 4 additions & 4 deletions Packs/ApiModules/Scripts/CoreIRApiModule/CoreIRApiModule.py
Original file line number Diff line number Diff line change
Expand Up @@ -2968,23 +2968,23 @@ def resolve_xdr_close_reason(xsoar_close_reason: str) -> str:
"""
# Initially setting the close reason according to the default mapping.
xdr_close_reason = XSOAR_RESOLVED_STATUS_TO_XDR.get(xsoar_close_reason, 'resolved_other')

# Reading custom XSOAR->XDR close-reason mapping.
custom_xsoar_to_xdr_close_reason_mapping = comma_separated_mapping_to_dict(
demisto.params().get("custom_xsoar_to_xdr_close_reason_mapping")
)

# Overriding default close-reason mapping if there exists a custom one.
if xsoar_close_reason in custom_xsoar_to_xdr_close_reason_mapping:
xdr_close_reason_candidate = custom_xsoar_to_xdr_close_reason_mapping[xsoar_close_reason]
xdr_close_reason_candidate = custom_xsoar_to_xdr_close_reason_mapping.get(xsoar_close_reason)
# Transforming resolved close-reason into snake_case format with known prefix to match XDR status format.
demisto.debug(
f"resolve_xdr_close_reason XSOAR->XDR custom close-reason exists, using {xsoar_close_reason}={xdr_close_reason}")
xdr_close_reason_candidate = "resolved_" + "_".join(xdr_close_reason_candidate.lower().split(" "))

if xdr_close_reason_candidate not in XDR_RESOLVED_STATUS_TO_XSOAR:
demisto.debug("Warning: Provided XDR close-reason does not exist. Using default XDR close-reason mapping. ")
else:
xdr_close_reason = xdr_close_reason_candidate
demisto.debug(
f"resolve_xdr_close_reason XSOAR->XDR custom close-reason exists, using {xsoar_close_reason}={xdr_close_reason}")
else:
demisto.debug(f"resolve_xdr_close_reason using default mapping {xsoar_close_reason}={xdr_close_reason}")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ comment: Common Core IR Client, provides generic Infrastructure.
scripttarget: 0
dependson: {}
timeout: 0s
dockerimage: demisto/python3:3.10.14.99865
dockerimage: demisto/python3:3.11.9.101916
fromversion: 5.0.0
tests:
- No tests (auto formatted)
6 changes: 6 additions & 0 deletions Packs/Base/ReleaseNotes/1_34_27.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

#### Scripts

##### CommonServerPython

Added a functionality to read server configuration.
30 changes: 30 additions & 0 deletions Packs/Base/Scripts/CommonServerPython/CommonServerPython.py
Original file line number Diff line number Diff line change
Expand Up @@ -12021,6 +12021,36 @@ def is_time_sensitive():
return hasattr(demisto, 'isTimeSensitive') and demisto.isTimeSensitive()


def parse_json_string(json_string):
"""
Parse a JSON string into a Python dictionary.
:type json_string: ``str``
:param json_string: The JSON string to be parsed.
:rtype: ``dict``
:return: A Python dictionary representing the parsed JSON data.
"""
try:
data = json.loads(json_string)
return data
except json.JSONDecodeError as error: # type: ignore[attr-defined]
demisto.error("Error decoding JSON: {error}".format(error=error))
return {}


def get_server_config():
"""
Retrieves XSOAR server configuration.
:rtype: ``dict``
:return: The XSOAR server configuration.
"""
response = demisto.internalHttpRequest(method='GET', uri='/system/config')
body = parse_json_string(response.get('body'))
server_config = body.get('sysConf', {})
return server_config

from DemistoClassApiModule import * # type:ignore [no-redef] # noqa:E402


Expand Down
74 changes: 62 additions & 12 deletions Packs/Base/Scripts/CommonServerPython/CommonServerPython_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,21 @@

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

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

# Verify sleep duration based on mocked time difference
assert sleep_mocker.call_count == 2


def test_get_server_config(mocker):
mock_response = {
'body': '{"sysConf":{"incident.closereasons":"CustomReason1, CustomReason 2, Foo","versn":40},"defaultMap":{}}\n',
'headers': {
'Content-Length': ['104'],
'X-Xss-Protection': ['1; mode=block'],
'X-Content-Type-Options': ['nosniff'],
'Strict-Transport-Security': ['max-age=10886400000000000; includeSubDomains'],
'Vary': ['Accept-Encoding'],
'Server-Timing': ['7'],
'Date': ['Wed, 03 Jul 2010 09:11:35 GMT'],
'X-Frame-Options': ['DENY'],
'Content-Type': ['application/json']
},
'status': '200 OK',
'statusCode': 200
}

mocker.patch.object(demisto, 'internalHttpRequest', return_value=mock_response)
server_config = get_server_config()
assert server_config == {'incident.closereasons': 'CustomReason1, CustomReason 2, Foo', 'versn': 40}


def test_get_server_config_fail(mocker):
mock_response = {
'body': 'NOT A VALID JSON',
'headers': {
'Content-Length': ['104'],
'X-Xss-Protection': ['1; mode=block'],
'X-Content-Type-Options': ['nosniff'],
'Strict-Transport-Security': ['max-age=10886400000000000; includeSubDomains'],
'Vary': ['Accept-Encoding'],
'Server-Timing': ['7'],
'Date': ['Wed, 03 Jul 2010 09:11:35 GMT'],
'X-Frame-Options': ['DENY'],
'Content-Type': ['application/json']
},
'status': '200 OK',
'statusCode': 200
}

mocker.patch.object(demisto, 'internalHttpRequest', return_value=mock_response)
mocked_error = mocker.patch.object(demisto, 'error')
assert get_server_config() == {}
assert mocked_error.call_args[0][0] == 'Error decoding JSON: Expecting value: line 1 column 1 (char 0)'
2 changes: 1 addition & 1 deletion Packs/Base/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Base",
"description": "The base pack for Cortex XSOAR.",
"support": "xsoar",
"currentVersion": "1.34.26",
"currentVersion": "1.34.27",
"author": "Cortex XSOAR",
"serverMinVersion": "6.0.0",
"url": "https://www.paloaltonetworks.com/cortex",
Expand Down
11 changes: 11 additions & 0 deletions Packs/Core/ReleaseNotes/3_0_50.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

#### Integrations

##### Indicators detection

Updated the CoreIRApiModule with support for custom XSOAR close-reasons in XSOAR-XDR close-reason mapping.

##### Investigation & Response

Updated the CoreIRApiModule with support for custom XSOAR close-reasons in XSOAR-XDR close-reason mapping.

2 changes: 1 addition & 1 deletion Packs/Core/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Core - Investigation and Response",
"description": "Automates incident response",
"support": "xsoar",
"currentVersion": "3.0.49",
"currentVersion": "3.0.50",
"author": "Cortex XSOAR",
"url": "https://www.paloaltonetworks.com/cortex",
"email": "",
Expand Down
111 changes: 64 additions & 47 deletions Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,64 @@ def filter_and_save_unseen_incident(incidents: List, limit: int, number_of_alrea
return filtered_incidents


def get_xsoar_close_reasons():
"""
Get the default XSOAR close-reasons in addition to custom close-reasons from server configuration.
"""
default_xsoar_close_reasons = list(XSOAR_RESOLVED_STATUS_TO_XDR.keys())
custom_close_reasons = []
try:
server_config = get_server_config()
demisto.debug(f'get_xsoar_close_reasons server-config: {str(server_config)}')
if server_config:
custom_close_reasons: list = argToList(server_config.get('incident.closereasons', ''))
except Exception as e:
demisto.error(f"Could not get server configuration: {e}")
return default_xsoar_close_reasons + custom_close_reasons


def validate_custom_close_reasons_mapping(mapping: str, direction: str):
""" Check validity of provided custom close-reason mappings. """

xdr_statuses = [status.replace("resolved_", "").replace("_", " ").title() for status in XDR_RESOLVED_STATUS_TO_XSOAR]
xsoar_statuses = get_xsoar_close_reasons()

exception_message = ('Improper custom mapping ({direction}) provided: "{key_or_value}" is not a valid Cortex '
'{xsoar_or_xdr} close-reason. Valid Cortex {xsoar_or_xdr} close-reasons are: {statuses}')

def to_xdr_status(status):
return "resolved_" + "_".join(status.lower().split(" "))

custom_mapping = comma_separated_mapping_to_dict(mapping)

valid_key = valid_value = True # If no mapping was provided.

for key, value in custom_mapping.items():
if direction == XSOAR_TO_XDR:
xdr_close_reason = to_xdr_status(value)
valid_key = key in xsoar_statuses
valid_value = xdr_close_reason in XDR_RESOLVED_STATUS_TO_XSOAR
elif direction == XDR_TO_XSOAR:
xdr_close_reason = to_xdr_status(key)
valid_key = xdr_close_reason in XDR_RESOLVED_STATUS_TO_XSOAR
valid_value = value in xsoar_statuses

if not valid_key:
raise DemistoException(
exception_message.format(direction=direction,
key_or_value=key,
xsoar_or_xdr="XSOAR" if direction == XSOAR_TO_XDR else "XDR",
statuses=xsoar_statuses
if direction == XSOAR_TO_XDR else xdr_statuses))
elif not valid_value:
raise DemistoException(
exception_message.format(direction=direction,
key_or_value=value,
xsoar_or_xdr="XDR" if direction == XSOAR_TO_XDR else "XSOAR",
statuses=xdr_statuses
if direction == XSOAR_TO_XDR else xsoar_statuses))


class Client(CoreClient):
def __init__(self, base_url, proxy, verify, timeout, params=None):
if not params:
Expand Down Expand Up @@ -157,54 +215,12 @@ def test_module(self, first_fetch_time):
raise

# XSOAR -> XDR
self.validate_custom_mapping(mapping=self._params.get("custom_xsoar_to_xdr_close_reason_mapping"),
direction=XSOAR_TO_XDR)
validate_custom_close_reasons_mapping(mapping=self._params.get("custom_xsoar_to_xdr_close_reason_mapping"),
direction=XSOAR_TO_XDR)

# XDR -> XSOAR
self.validate_custom_mapping(mapping=self._params.get("custom_xdr_to_xsoar_close_reason_mapping"),
direction=XDR_TO_XSOAR)

def validate_custom_mapping(self, mapping: str, direction: str):
""" Check validity of provided custom close-reason mappings. """

xdr_statuses_to_xsoar = [status.replace("resolved_", "").replace("_", " ").title()
for status in XDR_RESOLVED_STATUS_TO_XSOAR]
xsoar_statuses_to_xdr = list(XSOAR_RESOLVED_STATUS_TO_XDR.keys())

exception_message = ('Improper custom mapping ({direction}) provided: "{key_or_value}" is not a valid Cortex '
'{xsoar_or_xdr} close-reason. Valid Cortex {xsoar_or_xdr} close-reasons are: {statuses}')

def to_xdr_status(status):
return "resolved_" + "_".join(status.lower().split(" "))

custom_mapping = comma_separated_mapping_to_dict(mapping)

valid_key = valid_value = True # If no mapping was provided.

for key, value in custom_mapping.items():
if direction == XSOAR_TO_XDR:
xdr_close_reason = to_xdr_status(value)
valid_key = key in XSOAR_RESOLVED_STATUS_TO_XDR
valid_value = xdr_close_reason in XDR_RESOLVED_STATUS_TO_XSOAR
elif direction == XDR_TO_XSOAR:
xdr_close_reason = to_xdr_status(key)
valid_key = xdr_close_reason in XDR_RESOLVED_STATUS_TO_XSOAR
valid_value = value in XSOAR_RESOLVED_STATUS_TO_XDR

if not valid_key:
raise DemistoException(
exception_message.format(direction=direction,
key_or_value=key,
xsoar_or_xdr="XSOAR" if direction == XSOAR_TO_XDR else "XDR",
statuses=xsoar_statuses_to_xdr
if direction == XSOAR_TO_XDR else xdr_statuses_to_xsoar))
elif not valid_value:
raise DemistoException(
exception_message.format(direction=direction,
key_or_value=value,
xsoar_or_xdr="XDR" if direction == XSOAR_TO_XDR else "XSOAR",
statuses=xdr_statuses_to_xsoar
if direction == XSOAR_TO_XDR else xsoar_statuses_to_xdr))
validate_custom_close_reasons_mapping(mapping=self._params.get("custom_xdr_to_xsoar_close_reason_mapping"),
direction=XDR_TO_XSOAR)

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

# Check if incoming XDR close-reason has a non-default mapping to XSOAR close-reason.
if demisto.params().get("custom_xdr_to_xsoar_close_reason_mapping"):
Expand All @@ -802,7 +819,7 @@ def resolve_xsoar_close_reason(xdr_close_reason: str):
xdr_close_reason.replace("resolved_", "").replace("_", " ").title()
)
xsoar_close_reason = custom_xdr_to_xsoar_close_reason_mapping.get(title_cased_xdr_close_reason)
if xsoar_close_reason in XSOAR_RESOLVED_STATUS_TO_XDR:
if xsoar_close_reason in possible_xsoar_close_reasons:
demisto.debug(
f"XDR->XSOAR custom close-reason exists, using {xdr_close_reason}={xsoar_close_reason}"
)
Expand Down
2 changes: 1 addition & 1 deletion Packs/CortexXDR/Integrations/CortexXDRIR/CortexXDRIR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3547,7 +3547,7 @@ script:
Update one or more alerts with the provided arguments.
Required license: Cortex XDR Prevent, Cortex XDR Pro per Endpoint, or Cortex XDR Pro per GB.
name: xdr-update-alert
dockerimage: demisto/python3:3.10.14.99865
dockerimage: demisto/python3:3.11.9.101916
isfetch: true
isfetch:xpanse: false
script: ''
Expand Down
Loading

0 comments on commit fc3b955

Please sign in to comment.