# -*- coding: utf-8 -*-
import copy
import gzip
import json
import time
import os
import re
import sys
import urllib
import uuid
import warnings
try:
from typing import Optional, List, Any, Dict
except ImportError:
Optional = List = Any = Dict = None
try:
from unittest.mock import MagicMock
except ImportError:
from mock import MagicMock
import dateparser
from freezegun import freeze_time
import pytz
import requests
import requests_mock
import pytest
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, arg_to_bool_or_none, 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, 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, resolve_should_push_events, send_data_to_xsiam,
censor_request_logs, safe_sleep, get_server_config, b64_decode,
get_engine_base_url, is_integration_instance_running_on_engine, find_and_remove_sensitive_text, stringEscapeMD,
execute_polling_command, QuickActionPreview, MirrorObject, get_pack_version, ExecutionTimeout, is_ip_address_internal,
safe_pickle_loads, UnsafePickleError
)
EVENTS_LOG_ERROR = \
"""Error sending new events into XSIAM.
Parameters used:
\tURL: https://api-url
\tHeaders: {{
"authorization": "TOKEN",
"format": "json",
"product": "some product",
"vendor": "some vendor",
"content-encoding": "gzip",
"collector-name": "test_brand",
"instance-name": "test_integration_instance",
"final-reporting-device": "www.test_url.com",
"collector-type": "events"
}}
Response status code: {status_code}
Error received:
\t{error_received}"""
ASSETS_LOG_ERROR = \
"""Error sending new assets into XSIAM.
Parameters used:
\tURL: https://api-url
\tHeaders: {{
"authorization": "TOKEN",
"format": "json",
"product": "some product",
"vendor": "some vendor",
"content-encoding": "gzip",
"collector-name": "test_brand",
"instance-name": "test_integration_instance",
"final-reporting-device": "www.test_url.com",
"collector-type": "assets",
"snapshot-id": "123000test_integration_instance",
"total-items-count": "2"
}}
Response status code: {status_code}
Error received:
\t{error_received}"""
try:
from StringIO import StringIO
except ImportError:
# Python 3
from io import StringIO # noqa
INFO = {'b': 1,
'a': {
'safd': 3,
'b': [
{'c': {'d': 432}, 'd': 2},
{'c': {'f': 1}},
{'b': 1234},
{'c': {'d': 4567}},
{'c': {'d': 11}},
{'c': {'d': u'asdf'}}],
'c': {'d': 10},
}
}
@pytest.fixture()
def clear_version_cache():
"""
Clear the version cache at end of the test (in case we mocked demisto.serverVersion)
"""
yield
get_demisto_version._version = None
@pytest.fixture(autouse=True)
def handle_calling_context(mocker):
mocker.patch.object(CommonServerPython, 'get_integration_name', return_value='Test')
def test_xml():
import json
xml = b"100foo200goo" \
b""
jsonExpected = '{"work": {"employee": [{"id": "100", "name": "foo"}, {"id": "200", "name": "goo"}]}}'
jsonActual = xml2json(xml)
assert jsonActual == jsonExpected, "expected\n" + jsonExpected + "\n to equal \n" + jsonActual
jsonDict = json.loads(jsonActual)
assert jsonDict['work']['employee'][0]['id'] == "100", 'id of first employee must be 100'
assert jsonDict['work']['employee'][1]['name'] == "goo", 'name of second employee must be goo'
xmlActual = json2xml(jsonActual)
assert xmlActual == xml, "expected:\n{}\nto equal:\n{}".format(xml, xmlActual)
def toEntry(table):
return {
'Type': entryTypes['note'],
'Contents': table,
'ContentsFormat': formats['table'],
'ReadableContentsFormat': formats['markdown'],
'HumanReadable': table
}
def test_is_ip_valid():
valid_ip_v6 = "FE80:0000:0000:0000:0202:B3FF:FE1E:8329"
valid_ip_v6_b = "FE80::0202:B3FF:FE1E:8329"
invalid_ip_v6 = "KKKK:0000:0000:0000:0202:B3FF:FE1E:8329"
valid_ip_v4 = "10.10.10.10"
invalid_ip_v4 = "10.10.10.9999"
invalid_not_ip_with_ip_structure = "1.1.1.1.1.1.1.1.1.1.1.1.1.1.1"
not_ip = "Demisto"
assert not is_ip_valid(valid_ip_v6)
assert is_ip_valid(valid_ip_v6, True)
assert is_ip_valid(valid_ip_v6_b, True)
assert not is_ip_valid(invalid_ip_v6, True)
assert not is_ip_valid(not_ip, True)
assert is_ip_valid(valid_ip_v4)
assert not is_ip_valid(invalid_ip_v4)
assert not is_ip_valid(invalid_not_ip_with_ip_structure)
DATA = [
{
'header_1': 'a1',
'header_2': 'b1',
'header_3': 'c1'
},
{
'header_1': 'a2',
'header_2': 'b2',
'header_3': 'c2'
},
{
'header_1': 'a3',
'header_2': 'b3',
'header_3': 'c3'
}
]
TABLE_TO_MARKDOWN_ONLY_DATA_PACK = [
(
DATA,
'''### tableToMarkdown test
|header_1|header_2|header_3|
|---|---|---|
| a1 | b1 | c1 |
| a2 | b2 | c2 |
| a3 | b3 | c3 |
'''
),
(
[
{
'header_1|with_pipe': 'a1',
'header_2': 'b1',
},
{
'header_1|with_pipe': 'a2',
'header_2': 'b2',
}
],
'''### tableToMarkdown test
|header_1\\|with_pipe|header_2|
|---|---|
| a1 | b1 |
| a2 | b2 |
'''
)
]
DATA_WITH_URLS = [(
[
{
'header_1': 'a1',
'url1': 'b1',
'url2': 'c1'
},
{
'header_1': 'a2',
'url1': 'b2',
'url2': 'c2'
},
{
'header_1': 'a3',
'url1': 'b3',
'url2': 'c3'
}
],
'''### tableToMarkdown test
|header_1|url1|url2|
|---|---|---|
| a1 | [b1](b1) | [c1](c1) |
| a2 | [b2](b2) | [c2](c2) |
| a3 | [b3](b3) | [c3](c3) |
'''
)]
COMPLEX_DATA_WITH_URLS = [(
[
{'data':
{'id': '1',
'result':
{'files':
[
{
'filename': 'name',
'size': 0,
'url': 'url'
}
]
},
'links': ['link']
}
},
{'data':
{'id': '2',
'result':
{'files':
[
{
'filename': 'name',
'size': 0,
'url': 'url'
}
]
},
'links': ['link']
}
}
],
[
{'data':
{'id': '1',
'result':
{'files':
[
{
'filename': 'name',
'size': 0,
'url': '[url](url)'
}
]
},
'links': ['[link](link)']
}
},
{'data':
{'id': '2',
'result':
{'files':
[
{
'filename': 'name',
'size': 0,
'url': '[url](url)'
}
]
},
'links': ['[link](link)']
}
}
])]
@pytest.mark.skipif(not IS_PY3, reason='test not supported in py2')
def test_is_ip_address_internal():
"""
Given:
- A set of valid IP addresses including both private and public IPs.
When:
- Calling is_ip_address_internal on each IP address.
Then:
- Assert that the function returns True for private/internal IPs.
- Assert that the function returns False for public/external IPs.
"""
assert is_ip_address_internal('10.0.0.1') == True
assert is_ip_address_internal('127.0.0.1') == True
assert is_ip_address_internal('8.8.8.8') == False # Google DNS
assert is_ip_address_internal('1.1.1.1') == False # Cloudflare DNS
class TestTableToMarkdown:
@pytest.mark.parametrize('data, expected_table', TABLE_TO_MARKDOWN_ONLY_DATA_PACK)
def test_sanity(self, data, expected_table):
"""
Given:
- list of objects.
When:
- calling tableToMarkdown.
Then:
- return a valid table.
"""
table = tableToMarkdown('tableToMarkdown test', data)
assert table == expected_table
@staticmethod
def test_header_transform_underscoreToCamelCase():
"""
Given:
- list of objects.
- an header transformer.
When:
- calling tableToMarkdown.
Then:
- return a valid table with updated headers.
"""
# header transform
table = tableToMarkdown('tableToMarkdown test with headerTransform', DATA,
headerTransform=underscoreToCamelCase)
expected_table = (
'### tableToMarkdown test with headerTransform\n'
'|Header1|Header2|Header3|\n'
'|---|---|---|\n'
'| a1 | b1 | c1 |\n'
'| a2 | b2 | c2 |\n'
'| a3 | b3 | c3 |\n'
)
assert table == expected_table
@staticmethod
def test_multiline():
"""
Given:
- list of objects.
- some values contains a new line and the "|" sign.
When:
- calling tableToMarkdown.
Then:
- return a valid table with "br" tags instead of new lines and escaped pipe sign.
"""
data = copy.deepcopy(DATA)
for i, d in enumerate(data):
d['header_2'] = 'b%d.1\nb%d.2' % (i + 1, i + 1,)
d['header_3'] = 'c%d|1' % (i + 1,)
table = tableToMarkdown('tableToMarkdown test with multiline', data)
expected_table = (
'### tableToMarkdown test with multiline\n'
'|header_1|header_2|header_3|\n'
'|---|---|---|\n'
'| a1 | b1.1
b1.2 | c1\|1 |\n'
'| a2 | b2.1
b2.2 | c2\|1 |\n'
'| a3 | b3.1
b3.2 | c3\|1 |\n'
)
assert table == expected_table
@staticmethod
def test_url():
"""
Given:
- list of objects.
- some values contain a URL.
- some values are missing.
When:
- calling tableToMarkdown.
Then:
- return a valid table.
"""
data = copy.deepcopy(DATA)
for d in data:
d['header_2'] = None
d['header_3'] = '[url](https:\\demisto.com)'
table_url_missing_info = tableToMarkdown('tableToMarkdown test with url and missing info', data)
expected_table_url_missing_info = (
'### tableToMarkdown test with url and missing info\n'
'|header_1|header_2|header_3|\n'
'|---|---|---|\n'
'| a1 | | [url](https:\demisto.com) |\n'
'| a2 | | [url](https:\demisto.com) |\n'
'| a3 | | [url](https:\demisto.com) |\n'
)
assert table_url_missing_info == expected_table_url_missing_info
@staticmethod
def test_single_column():
"""
Given:
- list of objects.
- a single header.
When:
- calling tableToMarkdown.
Then:
- return a valid column style table.
"""
# single column table
table_single_column = tableToMarkdown('tableToMarkdown test with single column', DATA, ['header_1'])
expected_table_single_column = (
'### tableToMarkdown test with single column\n'
'|header_1|\n'
'|---|\n'
'| a1 |\n'
'| a2 |\n'
'| a3 |\n'
)
assert table_single_column == expected_table_single_column
@staticmethod
def test_list_values():
"""
Given:
- list of objects.
- some values are lists.
When:
- calling tableToMarkdown.
Then:
- return a valid table where the list values are comma-separated and each item in a new line.
"""
# list values
data = copy.deepcopy(DATA)
for i, d in enumerate(data):
d['header_3'] = [i + 1, 'second item']
d['header_2'] = 'hi'
table_list_field = tableToMarkdown('tableToMarkdown test with list field', data)
expected_table_list_field = (
'### tableToMarkdown test with list field\n'
'|header_1|header_2|header_3|\n'
'|---|---|---|\n'
'| a1 | hi | 1,
second item |\n'
'| a2 | hi | 2,
second item |\n'
'| a3 | hi | 3,
second item |\n'
)
assert table_list_field == expected_table_list_field
@staticmethod
def test_empty_fields():
"""
Given:
- list of objects.
- all values are empty.
When:
- calling tableToMarkdown with removeNull=false.
- calling tableToMarkdown with removeNull=true.
Then:
- return an empty table.
- return a "no results" message.
"""
data = [
{
'a': None,
'b': None,
'c': None,
} for _ in range(3)
]
table_all_none = tableToMarkdown('tableToMarkdown test with all none fields', data)
expected_table_all_none = (
'### tableToMarkdown test with all none fields\n'
'|a|b|c|\n'
'|---|---|---|\n'
'| | | |\n'
'| | | |\n'
'| | | |\n'
)
assert table_all_none == expected_table_all_none
# all fields are empty - removed
table_all_none2 = tableToMarkdown('tableToMarkdown test with all none fields2', data, removeNull=True)
expected_table_all_none2 = '''### tableToMarkdown test with all none fields2
**No entries.**
'''
assert table_all_none2 == expected_table_all_none2
@staticmethod
def test_header_not_on_first_object():
"""
Given:
- list of objects
- list of headers with header that doesn't appear in the first object.
When:
- calling tableToMarkdown.
Then:
- return a valid table with the extra header.
"""
# header not on first object
data = copy.deepcopy(DATA)
data[1]['extra_header'] = 'sample'
table_extra_header = tableToMarkdown('tableToMarkdown test with extra header', data,
headers=['header_1', 'header_2', 'extra_header'])
expected_table_extra_header = (
'### tableToMarkdown test with extra header\n'
'|header_1|header_2|extra_header|\n'
'|---|---|---|\n'
'| a1 | b1 | |\n'
'| a2 | b2 | sample |\n'
'| a3 | b3 | |\n'
)
assert table_extra_header == expected_table_extra_header
@staticmethod
def test_no_header():
"""
Given:
- list of objects.
- a list with non-existing headers.
When:
- calling tableToMarkdown.
Then:
- return a "no result" message.
"""
# no header
table_no_headers = tableToMarkdown('tableToMarkdown test with no headers', DATA,
headers=['no', 'header', 'found'], removeNull=True)
expected_table_no_headers = (
'### tableToMarkdown test with no headers\n'
'**No entries.**\n'
)
assert table_no_headers == expected_table_no_headers
@staticmethod
def test_dict_value():
"""
Given:
- list of objects.
- some values are lists.
When:
- calling tableToMarkdown.
Then:
- return a valid table.
"""
# dict value
data = copy.deepcopy(DATA)
data[1]['extra_header'] = {'sample': 'qwerty', 'sample2': '`asdf'}
table_dict_record = tableToMarkdown('tableToMarkdown test with dict record', data,
headers=['header_1', 'header_2', 'extra_header'])
expected_dict_record = (
'### tableToMarkdown test with dict record\n'
'|header_1|header_2|extra_header|\n'
'|---|---|---|\n'
'| a1 | b1 | |\n'
'| a2 | b2 | sample: qwerty
sample2: \\`asdf |\n'
'| a3 | b3 | |\n'
)
assert table_dict_record == expected_dict_record
@staticmethod
def test_string_header():
"""
Given:
- list of objects.
- a single header as a string.
When:
- calling tableToMarkdown.
Then:
- return a valid table.
"""
# string header (instead of list)
table_string_header = tableToMarkdown('tableToMarkdown string header', DATA, 'header_1')
expected_string_header_tbl = (
'### tableToMarkdown string header\n'
'|header_1|\n'
'|---|\n'
'| a1 |\n'
'| a2 |\n'
'| a3 |\n'
)
assert table_string_header == expected_string_header_tbl
@staticmethod
def test_list_of_strings_instead_of_dict():
"""
Given:
- list of strings.
- a single header as a list.
When:
- calling tableToMarkdown.
Then:
- return a valid table.
"""
# list of string values instead of list of dict objects
table_string_array = tableToMarkdown('tableToMarkdown test with string array', ['foo', 'bar', 'katz'],
['header_1'])
expected_string_array_tbl = (
'### tableToMarkdown test with string array\n'
'|header_1|\n'
'|---|\n'
'| foo |\n'
'| bar |\n'
'| katz |\n'
)
assert table_string_array == expected_string_array_tbl
@staticmethod
def test_list_of_strings_instead_of_dict_and_string_header():
"""
Given:
- list of strings.
- a single header as a string.
When:
- calling tableToMarkdown.
Then:
- return a valid table.
"""
# combination: string header + string values list
table_string_array_string_header = tableToMarkdown('tableToMarkdown test with string array and string header',
['foo', 'bar', 'katz'], 'header_1')
expected_string_array_string_header_tbl = (
'### tableToMarkdown test with string array and string header\n'
'|header_1|\n'
'|---|\n'
'| foo |\n'
'| bar |\n'
'| katz |\n'
)
assert table_string_array_string_header == expected_string_array_string_header_tbl
@staticmethod
def test_single_key_dict():
# combination: string header + string values list
table_single_key_dict = tableToMarkdown('tableToMarkdown test with single key dict',
{'single': ['Arthur', 'Blob', 'Cactus']})
expected_single_key_dict_tbl = (
'### tableToMarkdown test with single key dict\n'
'|single|\n'
'|---|\n'
'| Arthur |\n'
'| Blob |\n'
'| Cactus |\n'
)
assert table_single_key_dict == expected_single_key_dict_tbl
t = {"dummy_key1": {"dummy_key2": "dummy_value"}}
table_single_key_dict_nested = tableToMarkdown('TABLE 1', t)
expected_single_key_dict_nested_tbl = (
'### TABLE 1\n'
'|dummy_key1|\n'
'|---|\n'
'| dummy_key2: dummy_value |\n'
)
assert table_single_key_dict_nested == expected_single_key_dict_nested_tbl
@staticmethod
def test_dict_with_special_character():
"""
When:
- calling tableToMarkdown.
Given:
- list of objects.
- some values contain special characters.
Then:
- return a valid table.
"""
data = {
'header_1': u'foo',
'header_2': [u'\xe2.rtf']
}
table_with_character = tableToMarkdown('tableToMarkdown test with special character', data)
expected_string_with_special_character = '''### tableToMarkdown test with special character
|header_1|header_2|
|---|---|
| foo | â.rtf |
'''
assert table_with_character == expected_string_with_special_character
@staticmethod
def test_title_with_special_character():
"""
When:
- calling tableToMarkdown.
Given:
- a title with a special character.
Then:
- return a valid table.
"""
data = {
'header_1': u'foo'
}
table_with_character = tableToMarkdown('tableToMarkdown test with special character Ù', data)
expected_string_with_special_character = (
'### tableToMarkdown test with special character Ù\n'
'|header_1|\n'
'|---|\n'
'| foo |\n'
)
assert table_with_character == expected_string_with_special_character
@pytest.mark.parametrize('data, expected_table', DATA_WITH_URLS)
def test_clickable_url(self, data, expected_table):
"""
Given:
- list of objects.
- some values are URLs.
When:
- calling tableToMarkdown.
Then:
- return a valid table with clickable URLs.
"""
table = tableToMarkdown('tableToMarkdown test', data, url_keys=['url1', 'url2'])
assert table == expected_table
@staticmethod
def test_keep_headers_list():
"""
Given:
- list of objects.
When:
- calling tableToMarkdown.
Then:
- return a valid table.
- the given headers list is not modified.
"""
headers = ['header_1', 'header_2']
data = {
'header_1': 'foo',
}
table = tableToMarkdown('tableToMarkdown test', data, removeNull=True, headers=headers)
assert 'header_2' not in table
assert headers == ['header_1', 'header_2']
# Test fails locally because expected time is in UTC
@staticmethod
def test_date_fields_param():
"""
Given:
- List of objects with date fields in epoch format.
When:
- Calling tableToMarkdown with the given date fields.
Then:
- Return the date data in the markdown table in human-readable format.
"""
data = [
{
"docker_image": "demisto/python3",
"create_time": '1631521313466'
},
{
"docker_image": "demisto/python2",
"create_time": 1631521521466
}
]
table = tableToMarkdown('tableToMarkdown test', data, headers=["docker_image", "create_time"],
date_fields=['create_time'])
expected_md_table = '''### tableToMarkdown test
|docker_image|create_time|
|---|---|
| demisto/python3 | 2021-09-13 08:21:53 |
| demisto/python2 | 2021-09-13 08:25:21 |
'''
assert table == expected_md_table
@staticmethod
def test_with_json_transformers_default():
"""
Given:
- Nested json table.
When:
- Calling tableToMarkdown with `is_auto_transform_json` set to True.
Then:
- Parse the json table to the default format which supports nesting.
"""
with open('test_data/nested_data_example.json') as f:
nested_data_example = json.load(f)
table = tableToMarkdown("tableToMarkdown test", nested_data_example,
headers=['name', 'changelog', 'nested'],
is_auto_json_transform=True)
if IS_PY3:
expected_table = """### tableToMarkdown test
|name|changelog|nested|
|---|---|---|
| Active Directory Query | **1.0.4**:
***path***:
***releaseNotes***:
#### Integrations
##### Active Directory Query v2
Fixed an issue where the ***ad-get-user*** command caused performance issues because the *limit* argument was not defined.
***displayName***: 1.0.4 - R124496
***released***: 2020-09-23T17:43:26Z
**1.0.5**:
***path***:
***releaseNotes***:
#### Integrations
##### Active Directory Query v2
- Fixed several typos.
- Updated the Docker image to: *demisto/ldap:1.0.0.11282*.
***displayName***: 1.0.5 - 132259
***released***: 2020-10-01T17:48:31Z
**1.0.6**:
***path***:
***releaseNotes***:
#### Integrations
##### Active Directory Query v2
- Fixed an issue where the DN parameter within query in the ***search-computer*** command was incorrect.
- Updated the Docker image to *demisto/ldap:1.0.0.12410*.
***displayName***: 1.0.6 - 151676
***released***: 2020-10-19T14:35:15Z | **item1**:
***a***: 1
***b***: 2
***c***: 3
***d***: 4 |
"""
else:
expected_table = u"""### tableToMarkdown test
|name|changelog|nested|
|---|---|---|
| Active Directory Query | **1.0.4**:
***path***:
***releaseNotes***:
#### Integrations
##### Active Directory Query v2
Fixed an issue where the ***ad-get-user*** command caused performance issues because the *limit* argument was not defined.
***displayName***: 1.0.4 - R124496
***released***: 2020-09-23T17:43:26Z
**1.0.5**:
***path***:
***releaseNotes***:
#### Integrations
##### Active Directory Query v2
- Fixed several typos.
- Updated the Docker image to: *demisto/ldap:1.0.0.11282*.
***displayName***: 1.0.5 - 132259
***released***: 2020-10-01T17:48:31Z
**1.0.6**:
***path***:
***releaseNotes***:
#### Integrations
##### Active Directory Query v2
- Fixed an issue where the DN parameter within query in the ***search-computer*** command was incorrect.
- Updated the Docker image to *demisto/ldap:1.0.0.12410*.
***displayName***: 1.0.6 - 151676
***released***: 2020-10-19T14:35:15Z | **item1**:
***a***: 1
***c***: 3
***b***: 2
***d***: 4 |
"""
assert table == expected_table
@staticmethod
def test_with_json_transformer_simple():
with open('test_data/simple_data_example.json') as f:
simple_data_example = json.load(f)
name_transformer = JsonTransformer(keys=['first', 'second'], is_nested=False)
json_transformer_mapping = {'name': name_transformer}
table = tableToMarkdown("tableToMarkdown test", simple_data_example,
json_transform_mapping=json_transformer_mapping)
if IS_PY3:
expected_table = """### tableToMarkdown test
|name|value|
|---|---|
| **first**:
***a***: val
***second***: b | val1 |
| **first**:
***a***: val2
***second***: d | val2 |
"""
else:
expected_table = u"""### tableToMarkdown test
|name|value|
|---|---|
| ***second***: b
**first**:
***a***: val | val1 |
| ***second***: d
**first**:
***a***: val2 | val2 |
"""
assert expected_table == table
@staticmethod
def test_with_json_transformer_nested():
"""
Given:
- Nested json table.
When:
- Calling tableToMarkdown with JsonTransformer with only `keys` given.
Then:
- The header key which is transformed will parsed with the relevant keys.
"""
with open('test_data/nested_data_example.json') as f:
nested_data_example = json.load(f)
changelog_transformer = JsonTransformer(keys=['releaseNotes', 'released'], is_nested=True)
table_json_transformer = {'changelog': changelog_transformer}
table = tableToMarkdown("tableToMarkdown test", nested_data_example, headers=['name', 'changelog'],
json_transform_mapping=table_json_transformer)
expected_table = """### tableToMarkdown test
|name|changelog|
|---|---|
| Active Directory Query | **1.0.4**:
***releaseNotes***:
#### Integrations
##### Active Directory Query v2
Fixed an issue where the ***ad-get-user*** command caused performance issues because the *limit* argument was not defined.
***released***: 2020-09-23T17:43:26Z
**1.0.5**:
***releaseNotes***:
#### Integrations
##### Active Directory Query v2
- Fixed several typos.
- Updated the Docker image to: *demisto/ldap:1.0.0.11282*.
***released***: 2020-10-01T17:48:31Z
**1.0.6**:
***releaseNotes***:
#### Integrations
##### Active Directory Query v2
- Fixed an issue where the DN parameter within query in the ***search-computer*** command was incorrect.
- Updated the Docker image to *demisto/ldap:1.0.0.12410*.
***released***: 2020-10-19T14:35:15Z |
"""
assert expected_table == table
@staticmethod
def test_with_json_transformer_nested_complex():
"""
Given:
- Double nested json table.
When:
- Calling tableToMarkdown with JsonTransformer with only `keys_lst` given and `is_nested` set to True.
Then:
- The header key which is transformed will parsed with the relevant keys.
"""
with open('test_data/complex_nested_data_example.json') as f:
complex_nested_data_example = json.load(f)
changelog_transformer = JsonTransformer(keys=['releaseNotes', 'c'], is_nested=True)
table_json_transformer = {'changelog': changelog_transformer}
table = tableToMarkdown('tableToMarkdown test', complex_nested_data_example, headers=['name', 'changelog'],
json_transform_mapping=table_json_transformer)
expected_table = """### tableToMarkdown test
|name|changelog|
|---|---|
| Active Directory Query | **1.0.4**:
**path**:
**a**:
**b**:
***c***: we should see this value
***releaseNotes***:
#### Integrations
##### Active Directory Query v2
Fixed an issue where the ***ad-get-user*** command caused performance issues because the *limit* argument was not defined.
**1.0.5**:
**path**:
**a**:
**b**:
***c***: we should see this value
***releaseNotes***:
#### Integrations
##### Active Directory Query v2
- Fixed several typos.
- Updated the Docker image to: *demisto/ldap:1.0.0.11282*.
**1.0.6**:
**path**:
**a**:
**b**:
***c***: we should see this value
***releaseNotes***:
#### Integrations
##### Active Directory Query v2
- Fixed an issue where the DN parameter within query in the ***search-computer*** command was incorrect.
- Updated the Docker image to *demisto/ldap:1.0.0.12410*.
|
"""
assert expected_table == table
@staticmethod
def test_with_json_transformer_func():
"""
Given:
- Double nested json table.
When:
- Calling tableToMarkdown with JsonTransformer set to custom function.
Then:
- The table constructed with the transforming function.
"""
def changelog_to_str(json_input):
return ', '.join(json_input.keys())
with open('test_data/nested_data_example.json') as f:
nested_data_example = json.load(f)
changelog_transformer = JsonTransformer(func=changelog_to_str)
table_json_transformer = {'changelog': changelog_transformer}
table = tableToMarkdown("tableToMarkdown test", nested_data_example, headers=['name', 'changelog'],
json_transform_mapping=table_json_transformer)
expected_table = """### tableToMarkdown test
|name|changelog|
|---|---|
| Active Directory Query | 1.0.4, 1.0.5, 1.0.6 |
"""
assert expected_table == table
@staticmethod
def test_with_json_transform_list():
"""
Given:
- Nested json table with a list.
When:
- Calling tableToMarkdown with `is_auto_json_transform=True`.
Then:
- Create a markdown table with the list
"""
with open('test_data/nested_data_in_list.json') as f:
data_with_list = json.load(f)
table = tableToMarkdown("tableToMarkdown test", data_with_list, is_auto_json_transform=True)
if IS_PY3:
expected_table = """### tableToMarkdown test
|Commands|Creation time|Hostname|Machine Action Id|MachineId|Status|
|---|---|---|---|---|---|
| **-** ***startTime***: null
***endTime***: 2022-02-17T08:22:33.823Z
***commandStatus***: Completed
**errors**:
***values***: error1, error2, error3
**command**:
***type***: GetFile
**params**:
**-** ***key***: Path
***value***: test.txt
**-** ***startTime***: null
***endTime***: 2022-02-17T08:22:33.823Z
***commandStatus***: Completed
**errors**:
***values***:
**command**:
***type***: GetFile
**params**:
**-** ***key***: Path
***value***: test222.txt | 2022-02-17T08:20:02.6180466Z | desktop-s2455r9 | 5b38733b-ed80-47be-b892-f2ffb52593fd | f70f9fe6b29cd9511652434919c6530618f06606 | Succeeded |
"""
else:
expected_table = u"""### tableToMarkdown test
|Commands|Creation time|Hostname|Machine Action Id|MachineId|Status|
|---|---|---|---|---|---|
| **-** **command**:
**params**:
**-** ***value***: test.txt
***key***: Path
***type***: GetFile
***endTime***: 2022-02-17T08:22:33.823Z
***commandStatus***: Completed
**errors**:
***values***: error1, error2, error3
***startTime***: null
**-** **command**:
**params**:
**-** ***value***: test222.txt
***key***: Path
***type***: GetFile
***endTime***: 2022-02-17T08:22:33.823Z
***commandStatus***: Completed
**errors**:
***values***:
***startTime***: null | 2022-02-17T08:20:02.6180466Z | desktop-s2455r9 | 5b38733b-ed80-47be-b892-f2ffb52593fd | f70f9fe6b29cd9511652434919c6530618f06606 | Succeeded |
"""
assert expected_table == table
@staticmethod
def test_with_json_transform_list_keys():
"""
Given:
- Nested json table with a list.
When:
- Calling tableToMarkdown with `is_auto_json_transform=True`.
Then:
- Create a markdown table with the list only with given keys
"""
with open('test_data/nested_data_in_list.json') as f:
data_with_list = json.load(f)
table = tableToMarkdown("tableToMarkdown test", data_with_list,
json_transform_mapping={'Commands': JsonTransformer(keys=('commandStatus', 'command'))})
if IS_PY3:
expected_table = """### tableToMarkdown test
|Commands|Creation time|Hostname|Machine Action Id|MachineId|Status|
|---|---|---|---|---|---|
| **-** ***commandStatus***: Completed
**command**:
***type***: GetFile
**params**:
**-** ***key***: Path
***value***: test.txt
**-** ***commandStatus***: Completed
**command**:
***type***: GetFile
**params**:
**-** ***key***: Path
***value***: test222.txt | 2022-02-17T08:20:02.6180466Z | desktop-s2455r9 | 5b38733b-ed80-47be-b892-f2ffb52593fd | f70f9fe6b29cd9511652434919c6530618f06606 | Succeeded |
"""
else:
expected_table = u"""### tableToMarkdown test
|Commands|Creation time|Hostname|Machine Action Id|MachineId|Status|
|---|---|---|---|---|---|
| **-** **command**:
**params**:
**-** ***value***: test.txt
***key***: Path
***type***: GetFile
***commandStatus***: Completed
**-** **command**:
**params**:
**-** ***value***: test222.txt
***key***: Path
***type***: GetFile
***commandStatus***: Completed | 2022-02-17T08:20:02.6180466Z | desktop-s2455r9 | 5b38733b-ed80-47be-b892-f2ffb52593fd | f70f9fe6b29cd9511652434919c6530618f06606 | Succeeded |
"""
assert expected_table == table
@staticmethod
def test_no_given_headers_and_sort_headers():
"""
Given:
- A list of dictionaries.
When:
- Calling tableToMarkdown with no given headers and sort_headers=True by default.
Then:
- Validate that the table is sorted by the keys.
"""
data = [{'c': 1, 'b': 2, 'a': 3}, {'c': 4, 'b': 5, 'a': 6}]
table = tableToMarkdown("tableToMarkdown test", data)
assert table == ('### tableToMarkdown test\n'
'|a|b|c|\n|---|---|---|\n'
'| 3 | 2 | 1 |\n'
'| 6 | 5 | 4 |\n')
@staticmethod
def test_no_given_headers_and_sort_headers_false():
"""
Given:
- A list of dictionaries.
When:
- Calling tableToMarkdown with no given headers and sort_headers=False.
Then:
- Python 3: Validate that the table is not sorted by the keys.
- Python 2: Validate that the table is sorted by the keys.
"""
data = [{'c': 1, 'b': 2, 'a': 3}, {'c': 4, 'b': 5, 'a': 6}]
table = tableToMarkdown("tableToMarkdown test", data, sort_headers=False)
if IS_PY3:
expected_table_unsorted = ('### tableToMarkdown test\n'
'|c|b|a|\n|---|---|---|\n'
'| 1 | 2 | 3 |\n'
'| 4 | 5 | 6 |\n')
assert table == expected_table_unsorted
else: # in python 2 sort_headers=False is not working
expected_table_sorted = ('### tableToMarkdown test\n'
'|a|b|c|\n|---|---|---|\n'
'| 3 | 2 | 1 |\n'
'| 6 | 5 | 4 |\n')
assert table == expected_table_sorted
@staticmethod
def test_list_integers():
"""
Given:
- A dictionary with a list of integers .
When:
- Calling tableToMarkdown with is_auto_json_transform=True.
Then:
Validate that the script ran successfully.
"""
data = {'key': 'value', 'listtest': [1, 2, 3, 4]}
table = tableToMarkdown("tableToMarkdown test", data, sort_headers=False, is_auto_json_transform=True)
assert table
@pytest.mark.parametrize('data, expected_data', COMPLEX_DATA_WITH_URLS)
def test_url_to_clickable_markdown(data, expected_data):
table = url_to_clickable_markdown(data, url_keys=['url', 'links'])
assert table == expected_data
def test_flatten_cell():
# sanity
utf8_to_flatten = b'abcdefghijklmnopqrstuvwxyz1234567890!'.decode('utf8')
flatten_text = flattenCell(utf8_to_flatten)
expected_string = 'abcdefghijklmnopqrstuvwxyz1234567890!'
assert flatten_text == expected_string
# list of uft8 and string to flatten
str_a = b'abcdefghijklmnopqrstuvwxyz1234567890!'
utf8_b = str_a.decode('utf8')
list_to_flatten = [str_a, utf8_b]
flatten_text2 = flattenCell(list_to_flatten)
expected_flatten_string = 'abcdefghijklmnopqrstuvwxyz1234567890!,\nabcdefghijklmnopqrstuvwxyz1234567890!'
assert flatten_text2 == expected_flatten_string
# special character test
special_char = u'会'
list_of_special = [special_char, special_char]
flattenCell(list_of_special)
flattenCell(special_char)
# dictionary test
dict_to_flatten = {'first': u'会'}
expected_flatten_dict = u'{\n "first": "\u4f1a"\n}'
assert flattenCell(dict_to_flatten) == expected_flatten_dict
# datetime test
datetime_value = datetime(2019, 9, 17, 6, 16, 39)
dict_to_flatten = {'date': datetime_value}
expected_flatten_dict = '{\n "date": "2019-09-17 06:16:39"\n}'
assert flattenCell(dict_to_flatten) == expected_flatten_dict
def test_hash_djb2():
assert hash_djb2("test") == 2090756197, "Invalid value of hash_djb2"
def test_camelize():
non_camalized = [{'chookity_bop': 'asdasd'}, {'ab_c': 'd e', 'fgh_ijk': 'lm', 'nop': 'qr_st'}]
expected_output_upper_camel = [{'ChookityBop': 'asdasd'}, {'AbC': 'd e', 'Nop': 'qr_st', 'FghIjk': 'lm'}]
expected_output_lower_camel = [{'chookityBop': 'asdasd'}, {'abC': 'd e', 'nop': 'qr_st', 'fghIjk': 'lm'}]
assert camelize(non_camalized, '_') == expected_output_upper_camel
assert camelize(non_camalized, '_', upper_camel=True) == expected_output_upper_camel
assert camelize(non_camalized, '_', upper_camel=False) == expected_output_lower_camel
non_camalized2 = {'ab_c': 'd e', 'fgh_ijk': 'lm', 'nop': 'qr_st'}
expected_output2_upper_camel = {'AbC': 'd e', 'Nop': 'qr_st', 'FghIjk': 'lm'}
expected_output2_lower_camel = {'abC': 'd e', 'nop': 'qr_st', 'fghIjk': 'lm'}
assert camelize(non_camalized2, '_') == expected_output2_upper_camel
assert camelize(non_camalized2, '_', upper_camel=True) == expected_output2_upper_camel
assert camelize(non_camalized2, '_', upper_camel=False) == expected_output2_lower_camel
def test_camelize_string():
from CommonServerPython import camelize_string
non_camalized = ['chookity_bop', 'ab_c', 'fgh_ijk', 'nop']
expected_output_upper_camel = ['ChookityBop', 'AbC', 'FghIjk', 'Nop']
expected_output_lower_camel = ['chookityBop', 'abC', 'fghIjk', 'nop']
for i in range(len(non_camalized)):
assert camelize_string(non_camalized[i], '_') == expected_output_upper_camel[i]
assert camelize_string(non_camalized[i], '_', upper_camel=True) == expected_output_upper_camel[i]
assert camelize_string(non_camalized[i], '_', upper_camel=False) == expected_output_lower_camel[i]
def test_underscoreToCamelCase():
from CommonServerPython import underscoreToCamelCase
non_camalized = ['chookity_bop', 'ab_c', 'fgh_ijk', 'nop']
expected_output_upper_camel = ['ChookityBop', 'AbC', 'FghIjk', 'Nop']
expected_output_lower_camel = ['chookityBop', 'abC', 'fghIjk', 'nop']
for i in range(len(non_camalized)):
assert underscoreToCamelCase(non_camalized[i]) == expected_output_upper_camel[i]
assert underscoreToCamelCase(non_camalized[i], upper_camel=True) == expected_output_upper_camel[i]
assert underscoreToCamelCase(non_camalized[i], upper_camel=False) == expected_output_lower_camel[i]
# Note this test will fail when run locally (in pycharm/vscode) as it assumes the machine (docker image) has UTC timezone set
def test_date_to_timestamp():
assert date_to_timestamp('2018-11-06T08:56:41') == 1541494601000
assert date_to_timestamp(datetime.strptime('2018-11-06T08:56:41', "%Y-%m-%dT%H:%M:%S")) == 1541494601000
PASCAL_TO_SPACE_USE_CASES = [
('Validate', 'Validate'),
('validate', 'Validate'),
('TCP', 'TCP'),
('eventType', 'Event Type'),
('eventID', 'Event ID'),
('eventId', 'Event Id'),
('IPAddress', 'IP Address'),
('isDisabled', 'Is Disabled'),
('device-group', 'Device - Group'),
]
@pytest.mark.parametrize('s, expected', PASCAL_TO_SPACE_USE_CASES)
def test_pascalToSpace(s, expected):
assert pascalToSpace(s) == expected, 'Error on {} != {}'.format(pascalToSpace(s), expected)
def test_safe_load_json():
valid_json_str = '{"foo": "bar"}'
expected_valid_json_result = {u'foo': u'bar'}
assert expected_valid_json_result == safe_load_json(valid_json_str)
def test_remove_empty_elements():
test_dict = {
"foo": "bar",
"baz": {},
"empty": [],
"nested_dict": {
"empty_list": [],
"hummus": "pita"
},
"nested_list": {
"more_empty_list": []
}
}
expected_result = {
"foo": "bar",
"nested_dict": {
"hummus": "pita"
}
}
assert expected_result == remove_empty_elements(test_dict)
@pytest.mark.parametrize('header,raw_input,expected_output', [
('AWS DynamoDB DescribeBackup', {
'BackupDescription': {
"Foo": "Bar",
"Baz": "Bang",
"TestKey": "TestValue"
}
}, '''### AWS DynamoDB DescribeBackup\n|Baz|Foo|TestKey|\n|---|---|---|\n| Bang | Bar | TestValue |\n'''),
('Empty Results', {'key': []}, '### Empty Results\n**No entries.**\n')
])
def test_aws_table_to_markdown(header, raw_input, expected_output):
"""
Given
- A header and a dict with two levels
- A header and a dict with one key pointing to an empty list
When
- Creating a markdown table using the aws_table_to_markdown function
Ensure
- The header appears as a markdown header and the dictionary is translated to a markdown table
- The header appears as a markdown header and "No entries" text appears instead of a markdown table"
"""
assert aws_table_to_markdown(raw_input, header) == expected_output
def test_argToList(mocker):
expected = ['a', 'b', 'c']
test1 = ['a', 'b', 'c']
test2 = 'a,b,c'
test3 = '["a","b","c"]'
test4 = 'a;b;c'
test5 = 1
test6 = '1'
test7 = True
test8 = [1, 2, 3]
test9 = "[test.com]"
results = [argToList(test1), argToList(test2), argToList(test2, ','), argToList(test3), argToList(test4, ';')]
for result in results:
assert expected == result, 'argToList test failed, {} is not equal to {}'.format(str(result), str(expected))
mocker.patch.object(demisto, 'debug', return_value=None)
assert argToList(test5) == [1]
assert argToList(test5, transform=str) == ['1']
assert argToList(test6) == ['1']
assert argToList(test7) == [True]
assert argToList(test8, transform=str) == ['1', '2', '3']
assert argToList(test9) == ["[test.com]"]
@pytest.mark.parametrize('args, field, expected_output', [
({'ids': "1,2,3"}, 'ids', ["1", "2", "3"]),
({'ids': "1,2,1"}, 'ids', ["1", "2"]),
({'ids': ""}, 'ids', []),
({'ids': ""}, 'name', []),
])
def test_remove_duplicates_from_list_arg(args, field, expected_output):
assert len(remove_duplicates_from_list_arg(args, field)) == len(expected_output)
def test_remove_nulls():
temp_dictionary = {"a": "b", "c": 4, "e": [], "f": {}, "g": None, "h": "", "i": [1], "k": ()}
expected_dictionary = {"a": "b", "c": 4, "i": [1]}
remove_nulls_from_dictionary(temp_dictionary)
assert expected_dictionary == temp_dictionary, \
"remove_nulls_from_dictionary test failed, {} is not equal to {}".format(str(temp_dictionary),
str(expected_dictionary))
def test_is_error_true():
execute_command_results = [
{
"Type": entryTypes["error"],
"ContentsFormat": formats["text"],
"Contents": "this is error message"
}
]
assert is_error(execute_command_results)
def test_is_error_none():
assert not is_error(None)
def test_is_error_single_entry():
execute_command_results = {
"Type": entryTypes["error"],
"ContentsFormat": formats["text"],
"Contents": "this is error message"
}
assert is_error(execute_command_results)
def test_is_error_false():
execute_command_results = [
{
"Type": entryTypes["note"],
"ContentsFormat": formats["text"],
"Contents": "this is regular note"
}
]
assert not is_error(execute_command_results)
def test_not_error_entry():
execute_command_results = "invalid command results as string"
assert not is_error(execute_command_results)
def test_get_error():
execute_command_results = [
{
"Type": entryTypes["error"],
"ContentsFormat": formats["text"],
"Contents": "this is error message"
}
]
error = get_error(execute_command_results)
assert error == "this is error message"
def test_get_error_single_entry():
execute_command_results = {
"Type": entryTypes["error"],
"ContentsFormat": formats["text"],
"Contents": "this is error message"
}
error = get_error(execute_command_results)
assert error == "this is error message"
def test_get_error_need_raise_error_on_non_error_input():
execute_command_results = [
{
"Type": entryTypes["note"],
"ContentsFormat": formats["text"],
"Contents": "this is not an error"
}
]
try:
get_error(execute_command_results)
except ValueError as exception:
assert "execute_command_result has no error entry. before using get_error use is_error" in str(exception)
return
assert False
@pytest.mark.parametrize('data,data_expected,filename', [
("this is a test", b"this is a test", "test.txt"),
("this is a test", b"this is a test", "../../../test.txt"),
(u"עברית", u"עברית".encode('utf-8'), "test.txt"),
(b"binary data\x15\x00", b"binary data\x15\x00", "test.txt"),
]) # noqa: E124
def test_fileResult(mocker, request, data, data_expected, filename):
file_id = str(uuid.uuid4())
mocker.patch.object(demisto, 'uniqueFile', return_value="fileresult")
mocker.patch.object(demisto, 'investigation', return_value={'id': file_id})
file_name = "{}_fileresult".format(file_id)
def cleanup():
try:
os.remove(file_name)
except OSError:
pass
request.addfinalizer(cleanup)
res = fileResult(filename, data)
assert res['File'] == "test.txt"
with open(file_name, 'rb') as f:
assert f.read() == data_expected
# Error that always returns a unicode string to it's str representation
class SpecialErr(Exception):
def __str__(self):
return u"מיוחד"
def test_logger():
from CommonServerPython import LOG
LOG(u'€')
LOG(Exception(u'€'))
LOG(SpecialErr(12))
def test_logger_write(mocker):
mocker.patch.object(demisto, 'params', return_value={
'credentials': {'password': 'my_password'},
})
mocker.patch.object(demisto, 'info')
ilog = IntegrationLogger()
ilog.write("This is a test with my_password")
ilog.print_log()
# assert that the print doesn't contain my_password
# call_args is tuple (args list, kwargs). we only need the args
args = demisto.info.call_args[0]
assert 'This is a test' in args[0]
assert 'my_password' not in args[0]
assert '' in args[0]
def test_logger_init_key_name(mocker):
mocker.patch.object(demisto, 'params', return_value={
'key': {'password': 'my_password'},
'secret': 'my_secret'
})
mocker.patch.object(demisto, 'info')
ilog = IntegrationLogger()
ilog.write("This is a test with my_password and my_secret")
ilog.print_log()
# assert that the print doesn't contain my_password
# call_args is tuple (args list, kwargs). we only need the args
args = demisto.info.call_args[0]
assert 'This is a test' in args[0]
assert 'my_password' not in args[0]
assert 'my_secret' not in args[0]
assert '' in args[0]
def test_logger_replace_strs(mocker):
mocker.patch.object(demisto, 'params', return_value={
'apikey': 'my_apikey',
})
ilog = IntegrationLogger()
ilog.add_replace_strs('special_str', 'ZAQ!@#$%&*', '') # also check that empty string is not added by mistake
ilog('my_apikey is special_str and b64: ' + b64_encode('my_apikey'))
ilog('special chars like ZAQ!@#$%&* should be replaced even when url-encoded like ZAQ%21%40%23%24%25%26%2A')
assert ('' not in ilog.replace_strs)
assert ilog.messages[0] == ' is and b64: '
assert ilog.messages[1] == \
'special chars like should be replaced even when url-encoded like '
TEST_SSH_KEY_ESC = '-----BEGIN OPENSSH PRIVATE KEY-----\\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFw' \
'AAAAdzc2gtcn\\n-----END OPENSSH PRIVATE KEY-----'
TEST_SSH_KEY = '-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFw' \
'AAAAdzc2gtcn\n-----END OPENSSH PRIVATE KEY-----'
TEST_PASS_JSON_CHARS = 'json_chars'
SENSITIVE_PARAM = {
'app': None,
'authentication': {
'credential': '',
'credentials': {
'id': '',
'locked': False,
'modified': '0001-01-01T00: 00: 00Z',
'name': '',
'password': 'cred_pass',
'sortValues': None,
'sshkey': TEST_SSH_KEY,
'sshkeyEsc': TEST_SSH_KEY_ESC,
'sshkeyPass': 'ssh_key_secret_pass',
'user': '',
'vaultInstanceId': '',
'version': 0,
'workgroup': ''
},
'identifier': 'admin',
'password': 'ident_pass',
'passwordChanged': False
},
'password': TEST_PASS_JSON_CHARS + '\\"',
}
def test_logger_replace_strs_credentials(mocker):
mocker.patch.object(demisto, 'params', return_value=SENSITIVE_PARAM)
basic_auth = b64_encode(
'{}:{}'.format(SENSITIVE_PARAM['authentication']['identifier'], SENSITIVE_PARAM['authentication']['password']))
ilog = IntegrationLogger()
# log some secrets
ilog('my cred pass: cred_pass. my ssh key: ssh_key_secret. my ssh key: {}.'
'my ssh key: {}. my ssh pass: ssh_key_secret_pass. ident: ident_pass.'
' basic auth: {}'.format(TEST_SSH_KEY, TEST_SSH_KEY_ESC, basic_auth))
for s in ('cred_pass', TEST_SSH_KEY, TEST_SSH_KEY_ESC, 'ssh_key_secret_pass', 'ident_pass', basic_auth):
assert s not in ilog.messages[0]
def test_debug_logger_replace_strs(mocker):
mocker.patch.object(demisto, 'params', return_value=SENSITIVE_PARAM)
debug_logger = DebugLogger()
debug_logger.int_logger.set_buffering(True)
debug_logger.log_start_debug()
msg = debug_logger.int_logger.messages[0]
assert 'debug-mode started' in msg
assert 'Params:' in msg
for s in ('cred_pass', 'ssh_key_secret', 'ssh_key_secret_pass', 'ident_pass', TEST_SSH_KEY,
TEST_SSH_KEY_ESC, TEST_PASS_JSON_CHARS):
assert s not in msg
def test_add_sensitive_log_strs(mocker):
"""
Given:
- Debug mode command
When
- Adding sensitive strings to the log
Then
- Ensure that both LOG and _requests_logger mask the sensitive str
"""
sensitive_str = '%%This_is_API_key%%'
from CommonServerPython import add_sensitive_log_strs, LOG
mocker.patch('CommonServerPython._requests_logger', DebugLogger())
CommonServerPython._requests_logger.log_start_debug()
add_sensitive_log_strs(sensitive_str)
assert sensitive_str not in LOG(sensitive_str)
assert sensitive_str not in CommonServerPython._requests_logger.int_logger(sensitive_str)
def test_build_curl_post_noproxy():
"""
Given:
- HTTP client log messages of POST query
- Proxy is not used and insecure is not checked
When
- Building curl query
Then
- Ensure curl is generated as expected
"""
ilog = IntegrationLogger()
ilog.build_curl("send: b'POST /api HTTP/1.1\\r\\n"
"Host: demisto.com\\r\\n"
"User-Agent: python-requests/2.25.0\\r\\n"
"Accept-Encoding: gzip, deflate\r\n"
"Accept: */*\\r\\n"
"Connection: keep-alive\\r\\n"
"Authorization: TOKEN\\r\\n"
"Content-Length: 57\\r\\n"
"Content-Type: application/json\\r\\n\\r\\n'")
ilog.build_curl("send: b'{\"data\": \"value\"}'")
assert ilog.curl == [
'curl -X POST https://demisto.com/api -H "Authorization: TOKEN" -H "Content-Type: application/json" '
'--noproxy "*" -d \'{"data": "value"}\''
]
def test_build_curl_post_xml():
"""
Given:
- HTTP client log messages of POST query with XML body
- Proxy is not used and insecure is not checked
When
- Building curl query
Then
- Ensure curl is generated as expected
"""
ilog = IntegrationLogger()
ilog.build_curl("send: b'POST /api HTTP/1.1\\r\\n"
"Host: demisto.com\\r\\n"
"User-Agent: python-requests/2.25.0\\r\\n"
"Accept-Encoding: gzip, deflate\r\n"
"Accept: */*\\r\\n"
"Connection: keep-alive\\r\\n"
"Authorization: TOKEN\\r\\n"
"Content-Length: 57\\r\\n"
"Content-Type: application/json\\r\\n\\r\\n'")
ilog.build_curl("send: b''")
assert ilog.curl == [
'curl -X POST https://demisto.com/api -H "Authorization: TOKEN" -H "Content-Type: application/json" '
'--noproxy "*" -d \'\''
]
def test_build_curl_get_withproxy(mocker):
"""
Given:
- HTTP client log messages of GET query
- Proxy used and insecure checked
When
- Building curl query
Then
- Ensure curl is generated as expected
"""
mocker.patch.object(demisto, 'params', return_value={
'proxy': True,
'insecure': True
})
os.environ['https_proxy'] = 'http://proxy'
ilog = IntegrationLogger()
ilog.build_curl("send: b'GET /api HTTP/1.1\\r\\n"
"Host: demisto.com\\r\\n"
"User-Agent: python-requests/2.25.0\\r\\n"
"Accept-Encoding: gzip, deflate\r\n"
"Accept: */*\\r\\n"
"Connection: keep-alive\\r\\n"
"Authorization: TOKEN\\r\\n"
"Content-Length: 57\\r\\n"
"Content-Type: application/json\\r\\n\\r\\n'")
ilog.build_curl("send: b'{\"data\": \"value\"}'")
assert ilog.curl == [
'curl -X GET https://demisto.com/api -H "Authorization: TOKEN" -H "Content-Type: application/json" '
'--proxy http://proxy -k -d \'{"data": "value"}\''
]
def test_build_curl_multiple_queries():
"""
Given:
- HTTP client log messages of POST and GET queries
- Proxy is not used and insecure is not checked
When
- Building curl query
Then
- Ensure two curl queries are generated as expected
"""
ilog = IntegrationLogger()
ilog.build_curl("send: b'POST /api/post HTTP/1.1\\r\\n"
"Host: demisto.com\\r\\n"
"User-Agent: python-requests/2.25.0\\r\\n"
"Accept-Encoding: gzip, deflate\r\n"
"Accept: */*\\r\\n"
"Connection: keep-alive\\r\\n"
"Authorization: TOKEN\\r\\n"
"Content-Length: 57\\r\\n"
"Content-Type: application/json\\r\\n\\r\\n'")
ilog.build_curl("send: b'{\"postdata\": \"value\"}'")
ilog.build_curl("send: b'GET /api/get HTTP/1.1\\r\\n"
"Host: demisto.com\\r\\n"
"User-Agent: python-requests/2.25.0\\r\\n"
"Accept-Encoding: gzip, deflate\r\n"
"Accept: */*\\r\\n"
"Connection: keep-alive\\r\\n"
"Authorization: TOKEN\\r\\n"
"Content-Length: 57\\r\\n"
"Content-Type: application/json\\r\\n\\r\\n'")
ilog.build_curl("send: b'{\"getdata\": \"value\"}'")
assert ilog.curl == [
'curl -X POST https://demisto.com/api/post -H "Authorization: TOKEN" -H "Content-Type: application/json" '
'--noproxy "*" -d \'{"postdata": "value"}\'',
'curl -X GET https://demisto.com/api/get -H "Authorization: TOKEN" -H "Content-Type: application/json" '
'--noproxy "*" -d \'{"getdata": "value"}\''
]
def test_is_mac_address():
from CommonServerPython import is_mac_address
mac_address_false = 'AA:BB:CC:00:11'
mac_address_true = 'AA:BB:CC:00:11:22'
assert (is_mac_address(mac_address_false) is False)
assert (is_mac_address(mac_address_true))
def test_return_error_truncated_message(mocker):
"""
Given
- invalid error message due to longer than max length (50,000)
When
- return_error function is called
Then
- Return a truncated message that contains clarification about the truncation
"""
from CommonServerPython import return_error, MAX_ERROR_MESSAGE_LENGTH
err_msg = "1" * (MAX_ERROR_MESSAGE_LENGTH + 1)
results = mocker.spy(demisto, 'results')
mocker.patch.object(sys, 'exit')
return_error(err_msg)
assert len(results.call_args[0][0]["Contents"]) == MAX_ERROR_MESSAGE_LENGTH + \
len("...This error body was truncated...")
assert "This error body was truncated" in results.call_args[0][0]["Contents"]
def test_return_error_valid_message(mocker):
"""
Given
- A valid error message
When
- return_error function is called
Then
- Ensure the same message is returned
- Ensure the error message does not contain clarification about a truncation
"""
from CommonServerPython import return_error, MAX_ERROR_MESSAGE_LENGTH
err_msg = "1" * int(MAX_ERROR_MESSAGE_LENGTH * 0.9)
results = mocker.spy(demisto, 'results')
mocker.patch.object(sys, 'exit')
return_error(err_msg)
assert len(results.call_args[0][0]["Contents"]) == len(err_msg)
assert "This error body was truncated" not in results.call_args[0][0]["Contents"]
def test_return_error_command(mocker):
from CommonServerPython import return_error
err_msg = "Testing unicode Ё"
outputs = {'output': 'error'}
expected_error = {
'Type': entryTypes['error'],
'ContentsFormat': formats['text'],
'Contents': err_msg,
"EntryContext": outputs
}
# Test command that is not fetch-incidents
mocker.patch.object(demisto, 'command', return_value="test-command")
mocker.patch.object(sys, 'exit')
mocker.spy(demisto, 'results')
return_error(err_msg, '', outputs)
assert str(demisto.results.call_args) == "call({})".format(expected_error)
def test_return_error_fetch_incidents(mocker):
from CommonServerPython import return_error
err_msg = "Testing unicode Ё"
# Test fetch-incidents
mocker.patch.object(demisto, 'command', return_value="fetch-incidents")
returned_error = False
try:
return_error(err_msg)
except Exception as e:
returned_error = True
assert str(e) == err_msg
assert returned_error
def test_return_error_fetch_credentials(mocker):
from CommonServerPython import return_error
err_msg = "Testing unicode Ё"
# Test fetch-credentials
mocker.patch.object(demisto, 'command', return_value="fetch-credentials")
returned_error = False
try:
return_error(err_msg)
except Exception as e:
returned_error = True
assert str(e) == err_msg
assert returned_error
def test_return_error_fetch_indicators(mocker):
from CommonServerPython import return_error
err_msg = "Testing unicode Ё"
# Test fetch-indicators
mocker.patch.object(demisto, 'command', return_value="fetch-indicators")
returned_error = False
try:
return_error(err_msg)
except Exception as e:
returned_error = True
assert str(e) == err_msg
assert returned_error
def test_return_error_long_running_execution(mocker):
from CommonServerPython import return_error
err_msg = "Testing unicode Ё"
# Test long-running-execution
mocker.patch.object(demisto, 'command', return_value="long-running-execution")
returned_error = False
try:
return_error(err_msg)
except Exception as e:
returned_error = True
assert str(e) == err_msg
assert returned_error
def test_return_error_script(mocker, monkeypatch):
from CommonServerPython import return_error
mocker.patch.object(sys, 'exit')
mocker.spy(demisto, 'results')
monkeypatch.delattr(demisto, 'command')
err_msg = "Testing unicode Ё"
outputs = {'output': 'error'}
expected_error = {
'Type': entryTypes['error'],
'ContentsFormat': formats['text'],
'Contents': err_msg,
"EntryContext": outputs
}
assert not hasattr(demisto, 'command')
return_error(err_msg, '', outputs)
assert str(demisto.results.call_args) == "call({})".format(expected_error)
def test_exception_in_return_error(mocker):
from CommonServerPython import return_error, IntegrationLogger
expected = {'EntryContext': None, 'Type': 4, 'ContentsFormat': 'text', 'Contents': 'Message'}
mocker.patch.object(demisto, 'results')
mocker.patch.object(IntegrationLogger, '__call__', return_value='Message')
with pytest.raises(SystemExit, match='0'):
return_error("Message", error=ValueError("Error!"))
results = demisto.results.call_args[0][0]
assert expected == results
# IntegrationLogger = LOG (2 times if exception supplied)
assert IntegrationLogger.__call__.call_count == 2
def test_return_error_get_modified_remote_data(mocker):
from CommonServerPython import return_error
mocker.patch.object(demisto, 'command', return_value='get-modified-remote-data')
mocker.patch.object(demisto, 'results')
err_msg = 'Test Error'
with pytest.raises(SystemExit):
return_error(err_msg)
assert demisto.results.call_args[0][0]['Contents'] == 'skip update. error: ' + err_msg
def test_return_error_get_modified_remote_data_not_implemented(mocker):
from CommonServerPython import return_error
mocker.patch.object(demisto, 'command', return_value='get-modified-remote-data')
mocker.patch.object(demisto, 'results')
err_msg = 'Test Error'
with pytest.raises(SystemExit):
try:
raise NotImplementedError('Command not implemented')
except:
return_error(err_msg)
assert demisto.results.call_args[0][0]['Contents'] == err_msg
# ── is_caller_agentix tests ──────────────────────────────────────────
def test_is_caller_agentix_true(mocker):
"""
Given
- demisto.caller() returns 'agentix'
When
- is_caller_agentix is called
Then
- It returns True
"""
from CommonServerPython import is_caller_agentix, AGENTIX_CALLER
mocker.patch.object(demisto, 'caller', create=True, return_value=AGENTIX_CALLER)
assert is_caller_agentix() is True
def test_is_caller_agentix_false_other_caller(mocker):
"""
Given
- demisto.caller() returns a non-agentix value
When
- is_caller_agentix is called
Then
- It returns False
"""
from CommonServerPython import is_caller_agentix
mocker.patch.object(demisto, 'caller', create=True, return_value='playbook')
assert is_caller_agentix() is False
def test_is_caller_agentix_no_caller_method(mocker):
"""
Given
- demisto has no 'caller' attribute (older server)
When
- is_caller_agentix is called
Then
- It returns False without raising
"""
from CommonServerPython import is_caller_agentix
if hasattr(demisto, 'caller'):
mocker.patch.object(demisto, 'caller', create=True, side_effect=AttributeError)
assert is_caller_agentix() is False
# ── CortexError.build_message tests ─────────────────────────────────
def test_content_error_build_message_missing_arg():
"""
Given
- A CortexMissingArgError built with an argument name
When
- build_message is called
Then
- It returns the unified, auto-generated message
"""
from CommonServerPython import CortexMissingArgError, RetryGuidance
err = CortexMissingArgError('time')
expected = "Required argument 'time' was not provided. " + RetryGuidance.hint(RetryGuidance.RETRY_AFTER_FIX)
assert err.build_message() == expected
assert str(err) == expected
# Retry guidance is exposed via details / ExtendedPayload.
assert err.details['retry_guidance'] == RetryGuidance.RETRY_AFTER_FIX
assert err.details['retryable'] is True
def test_content_error_build_message_missing_arg_require_one():
"""
Given
- A CortexMissingArgError built with a list of argument names (default require_one)
When
- build_message is called
Then
- It returns an "at least one of" message with a retry-after-fix hint
"""
from CommonServerPython import CortexMissingArgError, CortexErrorCode, RetryGuidance
err = CortexMissingArgError(['endpoint_id', 'endpoint_ip', 'endpoint_hostname'])
assert err.build_message() == (
"At least one of the following arguments must be provided: "
"'endpoint_id', 'endpoint_ip', 'endpoint_hostname'. "
+ RetryGuidance.hint(RetryGuidance.RETRY_AFTER_FIX)
)
assert err.error_code == CortexErrorCode.MISSING_ARGUMENT
assert err.details['arguments'] == ['endpoint_id', 'endpoint_ip', 'endpoint_hostname']
assert err.details['require_one'] is True
def test_content_error_build_message_missing_arg_require_all():
"""
Given
- A CortexMissingArgError built with a list of names and require_one=False
When
- build_message is called
Then
- It returns an "all required" message
"""
from CommonServerPython import CortexMissingArgError, RetryGuidance
err = CortexMissingArgError(['user_id', 'token'], require_one=False)
assert err.build_message() == (
"The following required arguments were not provided: 'user_id', 'token'. "
+ RetryGuidance.hint(RetryGuidance.RETRY_AFTER_FIX)
)
assert err.details['require_one'] is False
def test_content_error_build_message_invalid_arg():
from CommonServerPython import CortexInvalidArgError
err = CortexInvalidArgError('mode', value='fast', allowed_values=['slow', 'medium'])
msg = err.build_message()
assert "Invalid value for argument 'mode'" in msg
assert "got 'fast'" in msg
assert "Allowed values: slow, medium" in msg
def test_content_error_build_message_resource_not_found():
from CommonServerPython import CortexResourceNotFoundError, RetryGuidance
fix_hint = RetryGuidance.hint(RetryGuidance.RETRY_AFTER_FIX)
err = CortexResourceNotFoundError('endpoint', identifier='abc-123')
assert err.build_message() == "endpoint 'abc-123' not found " + fix_hint
err_no_id = CortexResourceNotFoundError('incident')
assert err_no_id.build_message() == "incident not found " + fix_hint
def test_content_error_build_message_custom_overrides_auto():
"""
Given
- A CortexMissingArgError with an explicit custom message
When
- build_message is called
Then
- The custom message is returned instead of the auto-generated one
"""
from CommonServerPython import CortexMissingArgError
err = CortexMissingArgError('time', override_message='Please provide the time argument.')
assert err.build_message() == 'Please provide the time argument.'
def test_content_error_build_message_conflicting_args_mutually_exclusive():
"""
Given
- A CortexConflictingArgsError with mutually exclusive arguments
When
- build_message is called
Then
- The message explains the conflict and the valid resolution
"""
from CommonServerPython import CortexConflictingArgsError
err = CortexConflictingArgsError(arguments=['start_time', 'last_n_days'])
msg = err.build_message()
assert "Conflicting arguments: 'start_time', 'last_n_days'." in msg
assert "mutually exclusive" in msg
assert "Provide only one of: 'start_time', 'last_n_days'." in msg
def test_content_error_build_message_conflicting_args_reason_resolution():
from CommonServerPython import CortexConflictingArgsError
err = CortexConflictingArgsError(
arguments=['a', 'b'],
reason='a requires b to be empty.',
resolution='Provide a without b, or b without a.',
)
msg = err.build_message()
assert "Conflicting arguments: 'a', 'b'." in msg
assert 'a requires b to be empty.' in msg
assert 'Provide a without b, or b without a.' in msg
def test_content_error_build_message_conflicting_args_custom_message():
from CommonServerPython import CortexConflictingArgsError
err = CortexConflictingArgsError('Totally custom conflict text', arguments=['a', 'b'])
assert err.build_message() == 'Totally custom conflict text'
def test_content_error_build_message_api_error_with_status():
from CommonServerPython import CortexExternalApiError
err = CortexExternalApiError(status_code=500)
msg = err.build_message()
assert 'external API' in msg
assert '(HTTP 500)' in msg
def test_content_error_build_message_auth_default():
from CommonServerPython import CortexAuthError, CortexErrorCode, RetryGuidance
err = CortexAuthError()
assert err.build_message() == (
"Authentication failed. Check your credentials or API key. "
+ RetryGuidance.hint(RetryGuidance.NOT_RETRYABLE)
)
assert err.error_code == CortexErrorCode.AUTH_ERROR
assert err.details['retry_guidance'] == RetryGuidance.NOT_RETRYABLE
assert err.details['retryable'] is False
def test_content_error_build_message_rate_limit_retry_after():
from CommonServerPython import CortexRateLimitError
err = CortexRateLimitError(retry_after=30)
msg = err.build_message()
assert "rate limit exceeded" in msg
assert "Retry after 30 seconds." in msg
assert err.details.get('retry_after_seconds') == 30
def test_content_error_build_message_timeout_and_connection_defaults():
from CommonServerPython import CortexTimeoutError, CortexConnectionError, RetryGuidance
later_hint = RetryGuidance.hint(RetryGuidance.RETRY_LATER)
assert CortexTimeoutError().build_message() == "The request timed out. Please try again. " + later_hint
assert CortexConnectionError().build_message() == \
"Unable to connect to the service. Check network connectivity. " + later_hint
assert CortexTimeoutError().details['retryable'] is True
def test_content_error_build_message_simple_defaults():
"""
Given
- The simple CortexError subclasses without an explicit message
When
- build_message is called
Then
- Each returns its category-specific default message (with retry hint where applicable)
"""
from CommonServerPython import (
CortexParseError, CortexPermissionError, CortexExecutionError, RetryGuidance,
)
not_retryable = RetryGuidance.hint(RetryGuidance.NOT_RETRYABLE)
assert CortexParseError().build_message() == \
"Failed to parse the response received from the API. " + not_retryable
assert CortexPermissionError().build_message() == \
"Permission denied. You do not have the required permissions to perform this action. " + not_retryable
# CortexExecutionError has no retry guidance -> no hint appended.
assert CortexExecutionError().build_message() == "The command or script execution failed."
def test_content_error_retry_guidance_exposed_in_details():
"""
Given
- CortexError subclasses with different retry guidance
When
- The error is constructed
Then
- details carries the retry_guidance and retryable flag for agent consumption
"""
from CommonServerPython import (
CortexInvalidArgError, CortexRateLimitError, CortexPermissionError,
CortexExecutionError, RetryGuidance,
)
invalid = CortexInvalidArgError('x', value='y')
assert invalid.details['retry_guidance'] == RetryGuidance.RETRY_AFTER_FIX
assert invalid.details['retryable'] is True
rate = CortexRateLimitError()
assert rate.details['retry_guidance'] == RetryGuidance.RETRY_LATER
assert rate.details['retryable'] is True
perm = CortexPermissionError()
assert perm.details['retry_guidance'] == RetryGuidance.NOT_RETRYABLE
assert perm.details['retryable'] is False
# No guidance -> no retry keys in details.
execution = CortexExecutionError()
assert 'retry_guidance' not in execution.details
assert 'retryable' not in execution.details
def test_content_error_api_auth_status_not_retryable():
"""
Given
- A CortexExternalApiError with a 403 status code (auto-classified as auth)
When
- The error is constructed
Then
- Its retry guidance becomes NOT_RETRYABLE
"""
from CommonServerPython import CortexExternalApiError, CortexErrorCode, RetryGuidance
err = CortexExternalApiError(status_code=403)
assert err.error_code == CortexErrorCode.AUTH_ERROR
assert err.retry_guidance == RetryGuidance.NOT_RETRYABLE
assert err.details['retryable'] is False
def test_content_error_simple_subclass_custom_message_overrides_default():
from CommonServerPython import CortexExecutionError
err = CortexExecutionError('Custom execution failure detail')
assert err.build_message() == 'Custom execution failure detail'
# ── return_error message-selection tests ─────────────────────────────
def test_return_error_agentix_uses_auto_message(mocker):
"""
Given
- Caller is Agentix and a CortexError is passed as error
When
- return_error is called with an explicit (different) message
Then
- The automatic, unified message from the CortexError is surfaced
"""
from CommonServerPython import return_error, CortexMissingArgError, RetryGuidance
mocker.patch('CommonServerPython.is_caller_agentix', return_value=True)
mocker.patch.object(demisto, 'results')
with pytest.raises(SystemExit):
return_error('A human-friendly explanation', error=CortexMissingArgError('time'))
contents = demisto.results.call_args[0][0]['Contents']
assert contents == "Required argument 'time' was not provided. " + RetryGuidance.hint(RetryGuidance.RETRY_AFTER_FIX)
def test_return_error_agentix_ignores_custom_message_uses_auto(mocker):
"""
Given
- Caller is Agentix and a CortexError is constructed WITH a custom message
When
- return_error is called (with any explicit message)
Then
- The automatic, unified message is surfaced, NOT the custom message stored on the error
"""
from CommonServerPython import return_error, CortexMissingArgError, RetryGuidance
mocker.patch('CommonServerPython.is_caller_agentix', return_value=True)
mocker.patch.object(demisto, 'results')
error = CortexMissingArgError('time', override_message='Original backward-compatible message')
# Sanity: build_message returns the custom message, auto_message ignores it
assert error.build_message() == 'Original backward-compatible message'
assert error.auto_message() == "Required argument 'time' was not provided. " + \
RetryGuidance.hint(RetryGuidance.RETRY_AFTER_FIX)
with pytest.raises(SystemExit):
return_error('A human-friendly explanation', error=error)
contents = demisto.results.call_args[0][0]['Contents']
assert contents == "Required argument 'time' was not provided. " + RetryGuidance.hint(RetryGuidance.RETRY_AFTER_FIX)
def test_return_error_non_agentix_prefers_explicit_message(mocker):
"""
Given
- Caller is not Agentix and a CortexError is passed as error
When
- return_error is called with an explicit message
Then
- The explicit message is surfaced (not the automatic one)
"""
from CommonServerPython import return_error, CortexMissingArgError
mocker.patch('CommonServerPython.is_caller_agentix', return_value=False)
mocker.patch.object(demisto, 'results')
with pytest.raises(SystemExit):
return_error('A human-friendly explanation', error=CortexMissingArgError('time'))
contents = demisto.results.call_args[0][0]['Contents']
assert contents == 'A human-friendly explanation'
def test_return_error_non_agentix_falls_back_to_auto(mocker):
"""
Given
- Caller is not Agentix, a CortexError is passed, and no explicit message
When
- return_error is called with an empty message
Then
- The automatic, unified message is surfaced
"""
from CommonServerPython import return_error, CortexMissingArgError, RetryGuidance
mocker.patch('CommonServerPython.is_caller_agentix', return_value=False)
mocker.patch.object(demisto, 'results')
with pytest.raises(SystemExit):
return_error('', error=CortexMissingArgError('time'))
contents = demisto.results.call_args[0][0]['Contents']
assert contents == "Required argument 'time' was not provided. " + RetryGuidance.hint(RetryGuidance.RETRY_AFTER_FIX)
def test_return_error_agentix_no_content_error_uses_message(mocker):
"""
Given
- Caller is Agentix but no CortexError is available
When
- return_error is called with a plain message
Then
- The explicit message is surfaced as a fallback
"""
from CommonServerPython import return_error
mocker.patch('CommonServerPython.is_caller_agentix', return_value=True)
mocker.patch.object(demisto, 'results')
with pytest.raises(SystemExit):
return_error('Plain error message')
contents = demisto.results.call_args[0][0]['Contents']
assert contents == 'Plain error message'
def test_return_error_extended_payload_includes_retry_guidance_not_retryable(mocker):
"""
Given
- A CortexError whose category is NOT retryable (e.g. auth error)
When
- return_error is called with that error
Then
- The error entry's ExtendedPayload carries the error_code and a
retryable=False flag, so an LLM agent can decide not to retry.
- The retry_guidance text is NOT included (it is already in the message).
"""
from CommonServerPython import (
return_error, CortexAuthError,
EXTENDED_PAYLOAD_ERROR_CODE_KEY, EXTENDED_PAYLOAD_RETRY_GUIDANCE_KEY,
EXTENDED_PAYLOAD_RETRYABLE_KEY,
)
mocker.patch.object(demisto, 'results')
with pytest.raises(SystemExit):
return_error('Authentication failed', error=CortexAuthError())
extended_payload = demisto.results.call_args[0][0]['ExtendedPayload']
assert extended_payload[EXTENDED_PAYLOAD_ERROR_CODE_KEY] == CortexAuthError.error_code
assert extended_payload[EXTENDED_PAYLOAD_RETRYABLE_KEY] is False
assert EXTENDED_PAYLOAD_RETRY_GUIDANCE_KEY not in extended_payload
def test_return_error_extended_payload_includes_retryable_true(mocker):
"""
Given
- A CortexError whose category IS retryable (e.g. rate limit)
When
- return_error is called with that error
Then
- The error entry's ExtendedPayload carries a retryable=True flag, and the
retry_guidance text is NOT included (it is already in the message).
"""
from CommonServerPython import (
return_error, CortexRateLimitError,
EXTENDED_PAYLOAD_RETRY_GUIDANCE_KEY, EXTENDED_PAYLOAD_RETRYABLE_KEY,
)
mocker.patch.object(demisto, 'results')
with pytest.raises(SystemExit):
return_error('Rate limited', error=CortexRateLimitError())
extended_payload = demisto.results.call_args[0][0]['ExtendedPayload']
assert extended_payload[EXTENDED_PAYLOAD_RETRYABLE_KEY] is True
assert EXTENDED_PAYLOAD_RETRY_GUIDANCE_KEY not in extended_payload
def test_content_error_api_error_appends_original_api_error_to_message():
"""
Given
- A CortexExternalApiError carrying the original API response body (bytes)
When
- The automatic message is built
Then
- The original API error body is appended (decoded cleanly) to the built
message rather than exposed as a separate field.
"""
from CommonServerPython import CortexExternalApiError
err = CortexExternalApiError(status_code=500, response_body=b'{"error": {"message": "boom"}}')
auto = err.auto_message()
assert "(HTTP 500)" in auto
assert 'Original API error: {"error": {"message": "boom"}}' in auto
# The raw bytes repr must NOT leak into the message.
assert "b'" not in auto
def test_indicator_type_by_server_version_6_2(mocker, clear_version_cache):
"""
Given
- demisto version mock set to 6.2
When
- demisto version mock set to 6.2
Then
- Return the STIX indicator type with the STIX prefix
"""
mocker.patch.object(
demisto,
'demistoVersion',
return_value={
'version': '6.2.0',
}
)
assert FeedIndicatorType.indicator_type_by_server_version("STIX Attack Pattern") == "Attack Pattern"
def test_assign_params():
from CommonServerPython import assign_params
res = assign_params(a='1', b=True, c=None, d='')
assert res == {'a': '1', 'b': True}
class TestBuildDBotEntry(object):
def test_build_dbot_entry(self):
from CommonServerPython import build_dbot_entry
res = build_dbot_entry('user@example.com', 'Email', 'Vendor', 1)
assert res == {'DBotScore': {'Indicator': 'user@example.com', 'Type': 'email', 'Vendor': 'Vendor', 'Score': 1}}
def test_build_dbot_entry_no_malicious(self):
from CommonServerPython import build_dbot_entry
res = build_dbot_entry('user@example.com', 'Email', 'Vendor', 3, build_malicious=False)
assert res == {'DBotScore': {'Indicator': 'user@example.com', 'Type': 'email', 'Vendor': 'Vendor', 'Score': 3}}
def test_build_dbot_entry_malicious(self):
from CommonServerPython import build_dbot_entry, outputPaths
res = build_dbot_entry('user@example.com', 'Email', 'Vendor', 3, 'Malicious email')
assert res == {
"DBotScore": {
"Vendor": "Vendor",
"Indicator": "user@example.com",
"Score": 3,
"Type": "email"
},
outputPaths['email']: {
"Malicious": {
"Vendor": "Vendor",
"Description": "Malicious email"
},
"Address": "user@example.com"
}
}
def test_build_malicious_dbot_entry_file(self):
from CommonServerPython import build_malicious_dbot_entry, outputPaths
res = build_malicious_dbot_entry('md5hash', 'MD5', 'Vendor', 'Google DNS')
assert res == {
outputPaths['file']:
{"Malicious": {"Vendor": "Vendor", "Description": "Google DNS"}, "MD5": "md5hash"}}
def test_build_malicious_dbot_entry(self):
from CommonServerPython import build_malicious_dbot_entry, outputPaths
res = build_malicious_dbot_entry('8.8.8.8', 'ip', 'Vendor', 'Google DNS')
assert res == {outputPaths['ip']: {
'Address': '8.8.8.8', 'Malicious': {'Vendor': 'Vendor', 'Description': 'Google DNS'}}}
def test_build_malicious_dbot_entry_wrong_indicator_type(self):
from CommonServerPython import build_malicious_dbot_entry, DemistoException
with pytest.raises(DemistoException, match='Wrong indicator type'):
build_malicious_dbot_entry('8.8.8.8', 'notindicator', 'Vendor', 'Google DNS')
def test_illegal_dbot_score(self):
from CommonServerPython import build_dbot_entry, DemistoException
with pytest.raises(DemistoException, match='illegal DBot score'):
build_dbot_entry('1', 'ip', 'Vendor', 8)
def test_illegal_indicator_type(self):
from CommonServerPython import build_dbot_entry, DemistoException
with pytest.raises(DemistoException, match='illegal indicator type'):
build_dbot_entry('1', 'NOTHING', 'Vendor', 2)
def test_file_indicators(self):
from CommonServerPython import build_dbot_entry, outputPaths
res = build_dbot_entry('md5hash', 'md5', 'Vendor', 3)
assert res == {
"DBotScore": {
"Indicator": "md5hash",
"Type": "file",
"Vendor": "Vendor",
"Score": 3
},
outputPaths['file']: {
"MD5": "md5hash",
"Malicious": {
"Vendor": "Vendor",
"Description": None
}
}
}
class TestCommandResults:
@pytest.mark.parametrize('outputs, prefix', [([], None), ([], ''), ({}, '.')])
def test_outputs_without_outputs_prefix(self, outputs, prefix):
"""
Given
- outputs as a list without output_prefix, or with a period output prefix.
When
- Returns results.
Then
- Validate a ValueError is raised.
"""
from CommonServerPython import CommandResults
with pytest.raises(ValueError, match='outputs_prefix'):
CommandResults(outputs=outputs, outputs_prefix=prefix)
def test_with_tags(self):
from CommonServerPython import CommandResults
command_results = CommandResults(tags=['tag1', 'tag2'])
assert command_results.tags == ['tag1', 'tag2']
assert command_results.to_context()['Tags'] == ['tag1', 'tag2']
@pytest.mark.parametrize('output', [True, False])
def test_with_boolean_output(self, output):
from CommonServerPython import CommandResults
command_results = CommandResults(
outputs=output,
outputs_prefix="BooleanOutput",
readable_output="Boolean Output: {}".format(output),
)
assert command_results.to_context()['Contents'] == output
def test_dbot_score_is_in_to_context_ip(self):
"""
Given
- IP indicator
When
- Creating a reputation
Then
- Validate the DBOT Score and IP output exists in entry context.
"""
from CommonServerPython import Common, DBotScoreType, CommandResults
indicator_id = '1.1.1.1'
raw_response = {'id': indicator_id}
indicator = Common.IP(
indicator_id,
dbot_score=Common.DBotScore(
indicator_id,
DBotScoreType.IP,
'VirusTotal',
score=Common.DBotScore.BAD,
malicious_description='malicious!'
)
)
entry_context = CommandResults(
indicator=indicator,
readable_output='Indicator!',
outputs={'Indicator': raw_response},
raw_response=raw_response
).to_context()['EntryContext']
assert Common.DBotScore.CONTEXT_PATH in entry_context
assert Common.IP.CONTEXT_PATH in entry_context
def test_dbot_score_is_in_to_context_file(self):
"""
Given
- File indicator
When
- Creating a reputation
Then
- Validate the DBOT Score and File output exists in entry context.
"""
from CommonServerPython import Common, DBotScoreType, CommandResults
indicator_id = '63347f5d946164a23faca26b78a91e1c'
raw_response = {'id': indicator_id}
indicator = Common.File(
md5=indicator_id,
dbot_score=Common.DBotScore(
indicator_id,
DBotScoreType.FILE,
'Indicator',
score=Common.DBotScore.BAD,
malicious_description='malicious!'
)
)
entry_context = CommandResults(
indicator=indicator,
readable_output='output!',
outputs={'Indicator': raw_response},
raw_response=raw_response
).to_context()['EntryContext']
assert Common.DBotScore.CONTEXT_PATH in entry_context
assert Common.File.CONTEXT_PATH in entry_context
def test_dbot_score_is_in_to_context_domain(self):
"""
Given
- domain indicator
When
- Creating a reputation
Then
- Validate the DBOT Score and File output exists in entry context.
"""
from CommonServerPython import Common, DBotScoreType, CommandResults
indicator_id = 'example.com'
raw_response = {'id': indicator_id}
indicator = Common.Domain(
indicator_id,
dbot_score=Common.DBotScore(
indicator_id,
DBotScoreType.DOMAIN,
'VirusTotal',
score=Common.DBotScore.BAD,
malicious_description='malicious!'
)
)
entry_context = CommandResults(
indicator=indicator,
readable_output='output!',
outputs={'Indicator': raw_response},
raw_response=raw_response
).to_context()['EntryContext']
assert Common.DBotScore.CONTEXT_PATH in entry_context
assert Common.Domain.CONTEXT_PATH in entry_context
def test_dbot_score_is_in_to_context_url(self):
"""
Given
- url indicator
When
- Creating a reputation
Then
- Validate the DBOT Score and File output exists in entry context.
"""
from CommonServerPython import Common, DBotScoreType, CommandResults
indicator_id = 'https://example.com'
raw_response = {'id': indicator_id}
indicator = Common.URL(
indicator_id,
dbot_score=Common.DBotScore(
indicator_id,
DBotScoreType.URL,
'VirusTotal',
score=Common.DBotScore.BAD,
malicious_description='malicious!'
)
)
entry_context = CommandResults(
indicator=indicator,
readable_output='output!',
outputs={'Indicator': raw_response},
raw_response=raw_response
).to_context()['EntryContext']
assert Common.DBotScore.CONTEXT_PATH in entry_context
assert Common.URL.CONTEXT_PATH in entry_context
def test_hashes_array_is_in_to_context_file(self):
"""
Given
- A File indicator.
When
- Creating a reputation with all existing hashes.
Then
- Verify that the hashes array exists in the entry context and includes all the hashes types and values.
"""
from CommonServerPython import Common, DBotScoreType, CommandResults
indicator_id = '63347f5d946164a23faca26b78a91e1c'
raw_response = {'id': indicator_id}
indicator = Common.File(
md5=indicator_id,
sha1='test_sha1',
sha256='test_sha256',
sha512='test_sha512',
ssdeep='test_ssdeep',
imphash='test_imphash',
hashes=[Common.Hash('test_type1', 'test_value1'), Common.Hash('test_type2', 'test_value2')],
dbot_score=Common.DBotScore(
indicator_id,
DBotScoreType.FILE,
'Indicator',
score=Common.DBotScore.BAD,
malicious_description='malicious!'
)
)
entry_context = CommandResults(
indicator=indicator,
readable_output='output!',
outputs={'Indicator': raw_response},
raw_response=raw_response
).to_context()['EntryContext']
expected_hashes_array = [
{'type': 'test_type1', 'value': 'test_value1'},
{'type': 'test_type2', 'value': 'test_value2'},
{'type': 'MD5', 'value': '63347f5d946164a23faca26b78a91e1c'},
{'type': 'SHA1', 'value': 'test_sha1'},
{'type': 'SHA256', 'value': 'test_sha256'},
{'type': 'SHA512', 'value': 'test_sha512'},
{'type': 'SSDeep', 'value': 'test_ssdeep'},
{'type': 'Imphash', 'value': 'test_imphash'}
]
assert entry_context[Common.File.CONTEXT_PATH][0].get('Hashes') == expected_hashes_array
def test_multiple_outputs_keys(self):
"""
Given
- File has 3 unique keys. sha256, md5 and sha1
When
- creating CommandResults with outputs_key_field=[sha1, sha256, md5]
Then
- entrycontext DT expression contains all 3 unique fields
"""
from CommonServerPython import CommandResults
files = [
{
'sha256': '111',
'sha1': '111',
'md5': '111'
},
{
'sha256': '222',
'sha1': '222',
'md5': '222'
}
]
results = CommandResults(outputs_prefix='File', outputs_key_field=['sha1', 'sha256', 'md5'], outputs=files)
assert list(results.to_context()['EntryContext'].keys())[0] == \
'File(val.sha1 && val.sha1 == obj.sha1 && val.sha256 && val.sha256 == obj.sha256 && val.md5 && val.md5 == obj.md5)'
def test_output_prefix_includes_dt(self):
"""
Given
- Returning File with only outputs_prefix which includes DT in it
- outputs key fields are not provided
When
- creating CommandResults
Then
- EntryContext key should contain only the outputs_prefix
"""
from CommonServerPython import CommandResults
files = [{"key": "value"}] # if outputs is empty list, no results are returned
results = CommandResults(outputs_prefix='File(val.sha1 == obj.sha1 && val.md5 == obj.md5)',
outputs_key_field='', outputs=files)
assert list(results.to_context()['EntryContext'].keys())[0] == \
'File(val.sha1 == obj.sha1 && val.md5 == obj.md5)'
@pytest.mark.parametrize('score, expected_readable',
[(CommonServerPython.Common.DBotScore.NONE, 'Unknown'),
(CommonServerPython.Common.DBotScore.GOOD, 'Good'),
(CommonServerPython.Common.DBotScore.SUSPICIOUS, 'Suspicious'),
(CommonServerPython.Common.DBotScore.BAD, 'Bad')])
def test_dbot_readable(self, score, expected_readable):
from CommonServerPython import Common, DBotScoreType
dbot_score = Common.DBotScore(
indicator='8.8.8.8',
integration_name='Test',
indicator_type=DBotScoreType.IP,
score=score
)
assert dbot_score.to_readable() == expected_readable
def test_dbot_readable_invalid(self):
from CommonServerPython import Common, DBotScoreType
dbot_score = Common.DBotScore(
indicator='8.8.8.8',
integration_name='Test',
indicator_type=DBotScoreType.IP,
score=0
)
dbot_score.score = 7
assert dbot_score.to_readable() == 'Undefined'
dbot_score.score = None
assert dbot_score.to_readable() == 'Undefined'
def test_readable_only_context(self):
"""
Given:
- Markdown entry to CommandResults
When:
- Returning results
Then:
- Validate HumanReadable exists
"""
from CommonServerPython import CommandResults
markdown = '## Something'
context = CommandResults(readable_output=markdown).to_context()
assert context.get('HumanReadable') == markdown
def test_empty_outputs(self):
"""
Given:
- Outputs as None
When:
- Returning results
Then:
- Validate EntryContext key value
"""
from CommonServerPython import CommandResults
res = CommandResults(
outputs_prefix='FoundIndicators',
outputs_key_field='value',
outputs=None
)
context = res.to_context()
assert {} == context.get('EntryContext')
def test_empty_list_outputs(self):
"""
Given:
- Outputs with empty list
When:
- Returning results
Then:
- Validate EntryContext key value
"""
from CommonServerPython import CommandResults
res = CommandResults(
outputs_prefix='FoundIndicators',
outputs_key_field='value',
outputs=[]
)
context = res.to_context()
assert {} == context.get('EntryContext')
def test_return_command_results(self, clear_version_cache):
from CommonServerPython import Common, CommandResults, EntryFormat, EntryType, DBotScoreType
dbot_score = Common.DBotScore(
indicator='8.8.8.8',
integration_name='Test',
indicator_type=DBotScoreType.IP,
score=Common.DBotScore.GOOD,
message='test comment'
)
ip = Common.IP(
ip='8.8.8.8',
dbot_score=dbot_score,
asn='some asn',
hostname='test.com',
geo_country=None,
geo_description=None,
geo_latitude=None,
geo_longitude=None,
positive_engines=None,
detection_engines=None
)
results = CommandResults(
outputs_key_field=None,
outputs_prefix=None,
outputs=None,
indicators=[ip]
)
assert results.to_context() == {
'Type': EntryType.NOTE,
'ContentsFormat': EntryFormat.JSON,
'Contents': None,
'HumanReadable': None,
'EntryContext': {
'IP(val.Address && val.Address == obj.Address)': [
{
'Address': '8.8.8.8',
'ASN': 'some asn',
'Hostname': 'test.com'
}
],
'DBotScore(val.Indicator && val.Indicator == obj.Indicator && '
'val.Vendor == obj.Vendor && val.Type == obj.Type)': [
{
'Indicator': '8.8.8.8',
'Vendor': 'Test',
'Score': 1,
'Type': 'ip',
'Message': 'test comment'
}
]
},
'IndicatorTimeline': [],
'Relationships': [],
'IgnoreAutoExtract': False,
'Note': False
}
def test_multiple_indicators(self, clear_version_cache):
from CommonServerPython import Common, CommandResults, EntryFormat, EntryType, DBotScoreType
dbot_score1 = Common.DBotScore(
indicator='8.8.8.8',
integration_name='Test',
indicator_type=DBotScoreType.IP,
score=Common.DBotScore.GOOD
)
ip1 = Common.IP(
ip='8.8.8.8',
dbot_score=dbot_score1,
asn='some asn',
hostname='test.com',
geo_country=None,
geo_description=None,
geo_latitude=None,
geo_longitude=None,
positive_engines=None,
detection_engines=None
)
dbot_score2 = Common.DBotScore(
indicator='5.5.5.5',
integration_name='Test',
indicator_type=DBotScoreType.IP,
score=Common.DBotScore.GOOD
)
ip2 = Common.IP(
ip='5.5.5.5',
dbot_score=dbot_score2,
asn='some asn',
hostname='test.com',
geo_country=None,
geo_description=None,
geo_latitude=None,
geo_longitude=None,
positive_engines=None,
detection_engines=None
)
results = CommandResults(
outputs_key_field=None,
outputs_prefix=None,
outputs=None,
indicators=[ip1, ip2]
)
assert results.to_context() == {
'Type': EntryType.NOTE,
'ContentsFormat': EntryFormat.JSON,
'Contents': None,
'HumanReadable': None,
'EntryContext': {
'IP(val.Address && val.Address == obj.Address)': [
{
'Address': '8.8.8.8',
'ASN': 'some asn',
'Hostname': 'test.com'
},
{
'Address': '5.5.5.5',
'ASN': 'some asn',
'Hostname': 'test.com'
}
],
'DBotScore(val.Indicator && val.Indicator == obj.Indicator && '
'val.Vendor == obj.Vendor && val.Type == obj.Type)': [
{
'Indicator': '8.8.8.8',
'Vendor': 'Test',
'Score': 1,
'Type': 'ip'
},
{
'Indicator': '5.5.5.5',
'Vendor': 'Test',
'Score': 1,
'Type': 'ip'
}
]
},
'IndicatorTimeline': [],
'Relationships': [],
'IgnoreAutoExtract': False,
'Note': False
}
def test_return_list_of_items(self, clear_version_cache):
from CommonServerPython import CommandResults, EntryFormat, EntryType
tickets = [
{
'ticket_id': 1,
'title': 'foo'
},
{
'ticket_id': 2,
'title': 'goo'
}
]
results = CommandResults(
outputs_prefix='Jira.Ticket',
outputs_key_field='ticket_id',
outputs=tickets
)
assert results.to_context() == {
'Type': EntryType.NOTE,
'ContentsFormat': EntryFormat.JSON,
'Contents': tickets,
'HumanReadable': tableToMarkdown('Results', tickets),
'EntryContext': {
'Jira.Ticket(val.ticket_id && val.ticket_id == obj.ticket_id)': tickets
},
'IndicatorTimeline': [],
'Relationships': [],
'IgnoreAutoExtract': False,
'Note': False
}
def test_return_list_of_items_the_old_way(self):
from CommonServerPython import CommandResults, EntryFormat, EntryType
tickets = [
{
'ticket_id': 1,
'title': 'foo'
},
{
'ticket_id': 2,
'title': 'goo'
}
]
results = CommandResults(
outputs_prefix=None,
outputs_key_field=None,
outputs={
'Jira.Ticket(val.ticket_id == obj.ticket_id)': tickets
},
raw_response=tickets
)
assert sorted(results.to_context()) == sorted({
'Type': EntryType.NOTE,
'ContentsFormat': EntryFormat.JSON,
'Contents': tickets,
'HumanReadable': None,
'EntryContext': {
'Jira.Ticket(val.ticket_id == obj.ticket_id)': tickets
},
'IndicatorTimeline': [],
'Relationships': [],
'IgnoreAutoExtract': False,
'Note': False
})
def test_create_dbot_score_with_invalid_score(self):
from CommonServerPython import Common, DBotScoreType
try:
Common.DBotScore(
indicator='8.8.8.8',
integration_name='Virus Total',
score=100,
indicator_type=DBotScoreType.IP
)
assert False
except TypeError:
assert True
def test_create_dbot_score_with_invalid_reliability(self):
"""
Given:
- an invalid reliability value.
When
- creating a DBotScore entry
Then
- an error should be raised
"""
from CommonServerPython import Common, DBotScoreType
try:
Common.DBotScore(
indicator='8.8.8.8',
integration_name='Virus Total',
score=0,
indicator_type=DBotScoreType.IP,
reliability='Not a reliability'
)
assert False
except TypeError:
assert True
def test_create_dbot_score_with_valid_reliability(self):
"""
Given:
- a valid reliability value
When
- creating a DBotScore entry
Then
- the proper entry is created
"""
from CommonServerPython import Common, DBotScoreType, DBotScoreReliability, CommandResults
dbot_score = Common.DBotScore(
indicator='8.8.8.8',
integration_name='Test',
score=Common.DBotScore.GOOD,
indicator_type=DBotScoreType.IP,
reliability=DBotScoreReliability.B,
)
ip = Common.IP(
ip='8.8.8.8',
dbot_score=dbot_score,
)
results = CommandResults(
indicator=ip,
)
assert results.to_context()['EntryContext'] == {
'IP(val.Address && val.Address == obj.Address)': [
{
'Address': '8.8.8.8'
}
],
'DBotScore(val.Indicator && val.Indicator == '
'obj.Indicator && val.Vendor == obj.Vendor && val.Type == obj.Type)': [
{
'Indicator': '8.8.8.8',
'Type': 'ip',
'Vendor': 'Test',
'Score': 1,
'Reliability': 'B - Usually reliable'
}
]
}
def test_indicator_timeline_with_list_of_indicators(self):
"""
Given:
- a list of an indicator
When
- creating an IndicatorTimeline object
- creating a CommandResults objects using the IndicatorTimeline object
Then
- the IndicatorTimeline receives the appropriate category and message
"""
from CommonServerPython import CommandResults, IndicatorsTimeline
indicators = ['8.8.8.8']
timeline = IndicatorsTimeline(indicators=indicators, category='test', message='message')
results = CommandResults(
outputs_prefix=None,
outputs_key_field=None,
outputs=None,
raw_response=indicators,
indicators_timeline=timeline
)
assert sorted(results.to_context().get('IndicatorTimeline')) == sorted([
{'Value': '8.8.8.8', 'Category': 'test', 'Message': 'message'}
])
def test_indicator_timeline_running_from_an_integration(self, mocker):
"""
Given:
- a list of an indicator
When
- mocking the demisto.params()
- creating an IndicatorTimeline object
- creating a CommandResults objects using the IndicatorTimeline object
Then
- the IndicatorTimeline receives the appropriate category and message
"""
from CommonServerPython import CommandResults, IndicatorsTimeline
mocker.patch.object(demisto, 'params', return_value={'insecure': True})
indicators = ['8.8.8.8']
timeline = IndicatorsTimeline(indicators=indicators)
results = CommandResults(
outputs_prefix=None,
outputs_key_field=None,
outputs=None,
raw_response=indicators,
indicators_timeline=timeline
)
assert sorted(results.to_context().get('IndicatorTimeline')) == sorted([
{'Value': '8.8.8.8', 'Category': 'Integration Update'}
])
def test_single_indicator(self, mocker):
"""
Given:
- a single indicator
When
- mocking the demisto.params()
- creating an Common.IP object
- creating a CommandResults objects using the indicator member
Then
- The CommandResults.to_context() returns single result of standard output IP and DBotScore
"""
from CommonServerPython import CommandResults, Common, DBotScoreType
mocker.patch.object(demisto, 'params', return_value={'insecure': True})
dbot_score = Common.DBotScore(
indicator='8.8.8.8',
integration_name='Test',
indicator_type=DBotScoreType.IP,
score=Common.DBotScore.GOOD
)
ip = Common.IP(
ip='8.8.8.8',
dbot_score=dbot_score
)
results = CommandResults(
indicator=ip
)
assert results.to_context()['EntryContext'] == {
'IP(val.Address && val.Address == obj.Address)': [
{
'Address': '8.8.8.8'
}
],
'DBotScore(val.Indicator && val.Indicator == '
'obj.Indicator && val.Vendor == obj.Vendor && val.Type == obj.Type)': [
{
'Indicator': '8.8.8.8',
'Type': 'ip',
'Vendor': 'Test',
'Score': 1
}
]
}
def test_single_indicator_with_indicators(self, mocker):
"""
Given:
- a single indicator and a list of indicators
When
- mocking the demisto.params()
- creating an Common.IP object
- creating a CommandResults objects using the indicator member AND indicators member
Then
- The CommandResults.__init__() should raise an ValueError with appropriate error
"""
from CommonServerPython import CommandResults, Common, DBotScoreType
mocker.patch.object(demisto, 'params', return_value={'insecure': True})
dbot_score = Common.DBotScore(
indicator='8.8.8.8',
integration_name='Virus Total',
indicator_type=DBotScoreType.IP,
score=Common.DBotScore.GOOD
)
ip = Common.IP(
ip='8.8.8.8',
dbot_score=dbot_score
)
with pytest.raises(ValueError) as e:
CommandResults(
indicator=ip,
indicators=[ip]
)
assert e.value.args[0] == 'indicators is DEPRECATED, use only indicator'
def test_indicator_with_no_auto_extract(self):
"""
Given:
- a list of an indicator
- ignore_auto_extract set to True
When
- creating a CommandResults object with an indicator
- using Ignore Auto Extract
Then
- the IgnoreAutoExtract field is set to True
"""
from CommonServerPython import CommandResults
indicators = ['8.8.8.8']
results = CommandResults(
outputs_prefix=None,
outputs_key_field=None,
outputs=None,
raw_response=indicators,
indicators_timeline=None,
ignore_auto_extract=True
)
assert results.to_context().get('IgnoreAutoExtract') is True
def test_entry_as_note(self):
"""
Given:
- mark_as_note set to True
When:
- creating a CommandResults object
Then:
- the Note field is set to True
"""
from CommonServerPython import CommandResults
results = CommandResults(
outputs_prefix='Test',
outputs_key_field='value',
outputs=None,
mark_as_note=True
)
assert results.to_context().get('Note') is True
def test_empty_readable_outputs(self):
"""
Given:
- Outputs as str
- outputs_prefix is str
- No readable_output
When:
- Returning results
Then:
- Validate generated readable_output
"""
from CommonServerPython import CommandResults
res = CommandResults(
outputs="outputs_test",
outputs_prefix="outputs_prefix_test"
)
context = res.to_context()
assert "outputs_test" == context.get('HumanReadable')
def test_replace_existing(self):
"""
Given:
- replace_existing=True
When:
- Returning an object to context that needs to override it's key on each run.
Then:
- Return an object with the DT "(true)"
"""
from CommonServerPython import CommandResults
res = CommandResults(
outputs="next_token",
outputs_prefix="Path.To.Value",
replace_existing=True
)
context = res.to_context()
assert context["EntryContext"] == {"Path.To(true)": {"Value": "next_token"}}
def test_replace_existing_not_nested(self):
"""
Given:
- replace_existing=True but outputs_prefix is not nested, i.e., does not have a period.
When:
- Returning an object to context that needs to override it's key on each run.
Then:
- Raise an errror.
"""
from CommonServerPython import CommandResults
res = CommandResults(
outputs="next_token",
outputs_prefix="PathToValue",
replace_existing=True
)
with pytest.raises(DemistoException, match='outputs_prefix must be a nested path to replace an existing key.'):
res.to_context()
def test_large_indicator_context(self):
"""
Given:
- A large indicator greater than the DEFAULT_INSIGHT_CACHE_SIZE (3072 KB).
When:
- Returning an object to context.
Then:
- Return a small cointext object with in the size of 3072 KB
"""
from CommonServerPython import CommandResults
mock_dbot_score = Common.DBotScore(
indicator='8.8.8.8',
integration_name='Test',
indicator_type=DBotScoreType.IP,
score=Common.DBotScore.GOOD
)
mock_indicator = Common.IP(ip="8.8.8.8", dbot_score=mock_dbot_score, description="a"*3072*1024)
res = CommandResults(
indicator=mock_indicator
)
assert res.to_context()['EntryContext']['IP(val.Address && val.Address == obj.Address)'][0] == {'Address': '8.8.8.8'}
def test_http_request_ssl_ciphers_insecure():
if IS_PY3 and PY_VER_MINOR >= 10:
from CommonServerPython import BaseClient
client = BaseClient('https://www.google.com', ok_codes=(200, 201), verify=False)
adapter = client._session.adapters.get('https://')
ssl_context = adapter.poolmanager.connection_pool_kw['ssl_context']
ciphers_list = ssl_context.get_ciphers()
assert len(ciphers_list) == 42
assert next(cipher for cipher in ciphers_list if cipher['name'] == 'AES128-GCM-SHA256')
else:
assert True
class TestBaseClient:
from CommonServerPython import BaseClient
text = {"status": "ok"}
client = BaseClient('http://example.com/api/v2/', ok_codes=(200, 201))
RETRIES_POSITIVE_TEST = [
'get',
'put',
'post'
]
@pytest.mark.skip(reason="Test - too long, only manual")
@pytest.mark.parametrize('method', RETRIES_POSITIVE_TEST)
def test_http_requests_with_retry_sanity(self, method):
"""
Given
- A base client
When
- Making http request call with retries configured to a number higher then 0
Then
- Ensure a successful request return response as expected
"""
url = 'http://httpbin.org/{}'.format(method)
res = self.client._http_request(method,
'',
full_url=url,
retries=1,
status_list_to_retry=[401])
assert res['url'] == url
RETRIES_NEGATIVE_TESTS_INPUT = [
('get', 400), ('get', 401), ('get', 500),
('put', 400), ('put', 401), ('put', 500),
('post', 400), ('post', 401), ('post', 500),
]
@pytest.mark.skip(reason="Test - too long, only manual")
@pytest.mark.parametrize('method, status', RETRIES_NEGATIVE_TESTS_INPUT)
def test_http_requests_with_retry_negative_sanity(self, method, status):
"""
Given
- A base client
When
- Making http request call with retries configured to a number higher then 0
Then
- An unsuccessful request returns a DemistoException regardless the bad status code.
"""
with pytest.raises(CommonServerPython.DemistoException, match='{}'.format(status)):
self.client._http_request(method,
'',
full_url='http://httpbin.org/status/{}'.format(status),
retries=3,
status_list_to_retry=[400, 401, 500])
def test_implement_retry_without_backoff_jitter_support(self, mocker):
"""
Given:
- A BaseClient instance
- The installed urllib3's Retry class does not support the `backoff_jitter`
parameter (as is the case for urllib3 < 1.26.9)
When:
- _implement_retry is called with retries > 0
Then:
- Retry is called without the backoff_jitter kwarg (no TypeError is raised)
- The https:// adapter is mounted on the session with the configured retry object
"""
from urllib3.util import Retry as RealRetry
class RetryDefaultNoJitter:
allowed_methods = frozenset(['GET'])
# No backoff_jitter attribute, simulating an older urllib3 version
mock_retry_class = mocker.MagicMock(name='Retry')
mock_retry_class.DEFAULT = RetryDefaultNoJitter
mock_retry_class.return_value = mocker.MagicMock(spec=RealRetry)
mocker.patch.object(CommonServerPython, 'Retry', mock_retry_class)
client = CommonServerPython.BaseClient('https://example.com/api/v2/', ok_codes=(200, 201))
# Should not raise TypeError even though backoff_jitter is requested
client._implement_retry(retries=3, backoff_jitter=0.5)
mock_retry_class.assert_called_once()
assert 'backoff_jitter' not in mock_retry_class.call_args.kwargs
adapter = client._session.adapters.get('https://')
assert adapter is not None
assert adapter.max_retries is mock_retry_class.return_value
def test_implement_retry_with_backoff_jitter_support(self, mocker):
"""
Given:
- A BaseClient instance
- The installed urllib3's Retry class supports the `backoff_jitter` parameter
When:
- _implement_retry is called with retries > 0 and a backoff_jitter value
Then:
- Retry is called with the backoff_jitter kwarg set to the given value
"""
from urllib3.util import Retry as RealRetry
class RetryDefaultWithJitter:
allowed_methods = frozenset(['GET'])
backoff_jitter = 0.0
mock_retry_class = mocker.MagicMock(name='Retry')
mock_retry_class.DEFAULT = RetryDefaultWithJitter
mock_retry_class.return_value = mocker.MagicMock(spec=RealRetry)
mocker.patch.object(CommonServerPython, 'Retry', mock_retry_class)
client = CommonServerPython.BaseClient('https://example.com/api/v2/', ok_codes=(200, 201))
client._implement_retry(retries=3, backoff_jitter=0.5)
mock_retry_class.assert_called_once()
assert mock_retry_class.call_args.kwargs.get('backoff_jitter') == 0.5
def test_http_request_json(self, requests_mock):
requests_mock.get('http://example.com/api/v2/event', text=json.dumps(self.text))
res = self.client._http_request('get', 'event')
assert res == self.text
def test_http_request_json_negative(self, requests_mock):
from CommonServerPython import DemistoException
text = 'notjson'
requests_mock.get('http://example.com/api/v2/event', text=text)
with pytest.raises(DemistoException, match="Failed to parse json") as exception:
self.client._http_request('get', 'event')
assert exception.value.res
assert exception.value.res.text == text
def test_http_request_text(self, requests_mock):
requests_mock.get('http://example.com/api/v2/event', text=json.dumps(self.text))
res = self.client._http_request('get', 'event', resp_type='text')
assert res == json.dumps(self.text)
def test_http_request_content(self, requests_mock):
requests_mock.get('http://example.com/api/v2/event', content=str.encode(json.dumps(self.text)))
res = self.client._http_request('get', 'event', resp_type='content')
assert json.loads(res) == self.text
def test_http_request_response(self, requests_mock):
requests_mock.get('http://example.com/api/v2/event')
res = self.client._http_request('get', 'event', resp_type='response')
assert isinstance(res, requests.Response)
def test_http_request_proxy_false(self):
from CommonServerPython import BaseClient
import requests_mock
os.environ['http_proxy'] = 'http://testproxy:8899'
os.environ['https_proxy'] = 'https://testproxy:8899'
os.environ['REQUESTS_CA_BUNDLE'] = '/test1.pem'
client = BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), proxy=False, verify=True)
with requests_mock.mock() as m:
m.get('http://example.com/api/v2/event')
res = client._http_request('get', 'event', resp_type='response')
assert m.last_request.verify == '/test1.pem'
assert not m.last_request.proxies
assert m.called is True
def test_http_request_proxy_true(self):
from CommonServerPython import BaseClient
import requests_mock
os.environ['http_proxy'] = 'http://testproxy:8899'
os.environ['https_proxy'] = 'https://testproxy:8899'
os.environ['REQUESTS_CA_BUNDLE'] = '/test1.pem'
client = BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), proxy=True, verify=True)
with requests_mock.mock() as m:
m.get('http://example.com/api/v2/event')
res = client._http_request('get', 'event', resp_type='response')
assert m.last_request.verify == '/test1.pem'
assert m.last_request.proxies == {
'http': 'http://testproxy:8899',
'https': 'https://testproxy:8899'
}
assert m.called is True
def test_http_request_proxy_without_http_prefix(self):
"""
Given
- proxy param is set to true
- proxy configs are without http/https prefix
When
- run an http get request
Then
- the request will run and will use proxy configs that will include http:// prefix.
"""
from CommonServerPython import BaseClient
import requests_mock
os.environ['http_proxy'] = 'testproxy:8899'
os.environ['https_proxy'] = 'testproxy:8899'
os.environ['REQUESTS_CA_BUNDLE'] = '/test1.pem'
client = BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), proxy=True, verify=True)
with requests_mock.mock() as m:
m.get('http://example.com/api/v2/event')
res = client._http_request('get', 'event', resp_type='response')
assert m.last_request.verify == '/test1.pem'
assert m.last_request.proxies == {
'http': 'http://testproxy:8899',
'https': 'http://testproxy:8899'
}
assert m.called is True
def test_http_request_proxy_empty_proxy(self):
"""
Given
- proxy param is set to true
- proxy configs are empty
When
- run an http get request
Then
- the request will run and will use empty proxy configs and will not add https prefixes
"""
from CommonServerPython import BaseClient
import requests_mock
os.environ['http_proxy'] = ''
os.environ['https_proxy'] = ''
os.environ['REQUESTS_CA_BUNDLE'] = '/test1.pem'
client = BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), proxy=True, verify=True)
with requests_mock.mock() as m:
m.get('http://example.com/api/v2/event')
res = client._http_request('get', 'event', resp_type='response')
assert m.last_request.verify == '/test1.pem'
assert m.last_request.proxies == {}
assert m.called is True
def test_http_request_verify_false(self):
from CommonServerPython import BaseClient
import requests_mock
os.environ['REQUESTS_CA_BUNDLE'] = '/test1.pem'
client = BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), proxy=True, verify=False)
with requests_mock.mock() as m:
m.get('http://example.com/api/v2/event')
res = client._http_request('get', 'event', resp_type='response')
assert m.last_request.verify is False
assert m.called is True
def test_http_request_not_ok(self, requests_mock):
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', status_code=500)
with pytest.raises(DemistoException, match="[500]"):
self.client._http_request('get', 'event')
def test_http_request_not_ok_but_ok(self, requests_mock):
requests_mock.get('http://example.com/api/v2/event', status_code=500)
res = self.client._http_request('get', 'event', resp_type='response', ok_codes=(500,))
assert res.status_code == 500
def test_http_request_not_ok_with_json(self, requests_mock):
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', status_code=500, content=str.encode(json.dumps(self.text)))
with pytest.raises(DemistoException, match="Error in API call"):
self.client._http_request('get', 'event')
def test_http_request_not_ok_with_json_parsing(self, requests_mock):
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', status_code=500, content=str.encode(json.dumps(self.text)))
with pytest.raises(DemistoException) as exception:
self.client._http_request('get', 'event')
message = str(exception.value)
response_json_error = json.loads(message.split('\n')[1])
assert response_json_error == self.text
def test_http_request_timeout(self, requests_mock):
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', exc=requests.exceptions.ConnectTimeout)
with pytest.raises(DemistoException, match="Connection Timeout Error"):
self.client._http_request('get', 'event')
def test_http_request_ssl_error(self, requests_mock):
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', exc=requests.exceptions.SSLError)
with pytest.raises(DemistoException, match="SSL Certificate Verification Failed"):
self.client._http_request('get', 'event', resp_type='response')
def test_http_request_ssl_error_insecure(cls, requests_mock):
requests_mock.get('http://example.com/api/v2/event', exc=requests.exceptions.SSLError('test ssl'))
client = cls.BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), verify=False)
with pytest.raises(requests.exceptions.SSLError, match="^test ssl$"):
client._http_request('get', 'event', resp_type='response')
def test_http_request_proxy_error(self, requests_mock):
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', exc=requests.exceptions.ProxyError)
with pytest.raises(DemistoException, match="Proxy Error"):
self.client._http_request('get', 'event', resp_type='response')
def test_http_request_connection_error_with_errno(self, requests_mock):
from CommonServerPython import DemistoException
err = requests.exceptions.ConnectionError()
err.errno = 104
err.strerror = "Connection reset by peer test"
requests_mock.get('http://example.com/api/v2/event', exc=err)
with pytest.raises(DemistoException, match="Error Number: \[104\]\\nMessage: Connection reset by peer test"):
self.client._http_request('get', 'event', resp_type='response')
def test_http_request_connection_error_without_errno(self, requests_mock):
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', exc=requests.exceptions.ConnectionError("Generic error"))
with pytest.raises(DemistoException, match="Generic error"):
self.client._http_request('get', 'event', resp_type='response')
def test_text_exception_parsing(self, requests_mock):
from CommonServerPython import DemistoException
reason = 'Bad Request'
text = 'additional text'
requests_mock.get('http://example.com/api/v2/event',
status_code=400,
reason=reason,
text=text)
with pytest.raises(DemistoException, match='- {}\n{}'.format(reason, text)):
self.client._http_request('get', 'event', resp_type='text')
def test_json_exception_parsing(self, requests_mock):
from CommonServerPython import DemistoException
reason = 'Bad Request'
json_response = {'error': 'additional text'}
requests_mock.get('http://example.com/api/v2/event',
status_code=400,
reason=reason,
json=json_response)
with pytest.raises(DemistoException, match='- {}\n.*{}'.format(reason, json_response["error"])):
self.client._http_request('get', 'event', resp_type='text')
def test_exception_response_json_parsing_when_ok_code_is_invalid(self, requests_mock):
from CommonServerPython import DemistoException
json_response = {'error': 'additional text'}
requests_mock.get('http://example.com/api/v2/event',
status_code=400,
json=json_response)
try:
self.client._http_request('get', 'event', ok_codes=(200,))
except DemistoException as e:
resp_json = e.res.json()
assert e.res.status_code == 400
assert resp_json.get('error') == 'additional text'
def test_exception_response_text_parsing_when_ok_code_is_invalid(self, requests_mock):
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event',
status_code=400,
text='{"error": "additional text"}')
try:
self.client._http_request('get', 'event', ok_codes=(200,))
except DemistoException as e:
resp_json = json.loads(e.res.text)
assert e.res.status_code == 400
assert resp_json.get('error') == 'additional text'
def test_http_request_timeout_default(self, requests_mock):
requests_mock.get('http://example.com/api/v2/event', text=json.dumps(self.text))
self.client._http_request('get', 'event')
assert requests_mock.last_request.timeout == self.client.REQUESTS_TIMEOUT
def test_http_request_timeout_given_func(self, requests_mock):
requests_mock.get('http://example.com/api/v2/event', text=json.dumps(self.text))
timeout = 120
self.client._http_request('get', 'event', timeout=timeout)
assert requests_mock.last_request.timeout == timeout
def test_http_request_timeout_given_class(self, requests_mock):
from CommonServerPython import BaseClient
requests_mock.get('http://example.com/api/v2/event', text=json.dumps(self.text))
timeout = 44
new_client = BaseClient('http://example.com/api/v2/', timeout=timeout)
new_client._http_request('get', 'event')
assert requests_mock.last_request.timeout == timeout
def test_http_request_timeout_environ_system(self, requests_mock, mocker):
from CommonServerPython import BaseClient
requests_mock.get('http://example.com/api/v2/event', text=json.dumps(self.text))
timeout = 10
mocker.patch.dict(os.environ, {'REQUESTS_TIMEOUT': str(timeout)})
new_client = BaseClient('http://example.com/api/v2/')
new_client._http_request('get', 'event')
assert requests_mock.last_request.timeout == timeout
def test_http_request_timeout_environ_integration(self, requests_mock, mocker):
requests_mock.get('http://example.com/api/v2/event', text=json.dumps(self.text))
timeout = 180.1
# integration name is set to Test in the fixture handle_calling_context
mocker.patch.dict(os.environ, {'REQUESTS_TIMEOUT.Test': str(timeout)})
from CommonServerPython import BaseClient
new_client = BaseClient('http://example.com/api/v2/')
new_client._http_request('get', 'event')
assert requests_mock.last_request.timeout == timeout
def test_http_request_timeout_environ_script(self, requests_mock, mocker):
requests_mock.get('http://example.com/api/v2/event', text=json.dumps(self.text))
timeout = 23.4
script_name = 'TestScript'
mocker.patch.dict(os.environ, {'REQUESTS_TIMEOUT.' + script_name: str(timeout)})
mocker.patch.dict(demisto.callingContext, {'context': {'ScriptName': script_name}})
mocker.patch.object(CommonServerPython, 'get_integration_name', return_value='')
from CommonServerPython import BaseClient
new_client = BaseClient('http://example.com/api/v2/')
new_client._http_request('get', 'event')
assert requests_mock.last_request.timeout == timeout
def test_is_valid_ok_codes_empty(self):
from requests import Response
from CommonServerPython import BaseClient
new_client = BaseClient('http://example.com/api/v2/')
response = Response()
response.status_code = 200
assert new_client._is_status_code_valid(response, None)
def test_is_valid_ok_codes_from_function(self):
from requests import Response
response = Response()
response.status_code = 200
assert self.client._is_status_code_valid(response, (200, 201))
def test_is_valid_ok_codes_from_self(self):
from requests import Response
response = Response()
response.status_code = 200
assert self.client._is_status_code_valid(response, None)
def test_is_valid_ok_codes_empty_false(self):
from requests import Response
response = Response()
response.status_code = 400
assert not self.client._is_status_code_valid(response, None)
def test_is_valid_ok_codes_from_function_false(self):
from requests import Response
response = Response()
response.status_code = 400
assert not self.client._is_status_code_valid(response, (200, 201))
def test_is_valid_ok_codes_from_self_false(self):
from requests import Response
response = Response()
response.status_code = 400
assert not self.client._is_status_code_valid(response)
def test_http_request_params_parser_none(self, requests_mock):
"""
Given
- query params with spaces without specific quote function.
When
- Calling _https_request function.
Then
- Verify the spaces in the result is as expected.
"""
mock_request = requests_mock.get('http://example.com/api/v2/', json={})
from CommonServerPython import BaseClient
mock_client = BaseClient('http://example.com/api/v2/')
mock_client._http_request('get', params={'key': 'value with spaces'})
assert mock_request.last_request.query == 'key=value+with+spaces'
def test_http_request_execution_metrics_success(cls, requests_mock):
"""
Given: A BaseClient object
When:
- Calling _http_request function with metrics
- A successful response.
Then: Verify the successful execution metrics is incremented.
"""
requests_mock.get('http://example.com/api/v2/event', text="success")
client = cls.BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), verify=False)
client._http_request('get', 'event', resp_type='response', with_metrics=True)
assert client.execution_metrics.success == 1
def test_http_request_execution_metrics_success_but_polling_in_progress(cls, requests_mock):
"""
Given: A BaseClient object
When:
- Calling _http_request function with metrics
- A successful response.
- Response is determined as polling in progress.
Then: Verify the successful execution metrics is not incremented.
"""
requests_mock.get('http://example.com/api/v2/event', text="success")
client = cls.BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), verify=False)
client.is_polling_in_progress = lambda _: True
client._http_request('get', 'event', resp_type='response', with_metrics=True)
assert client.execution_metrics.success == 0
def test_http_request_execution_metrics_timeout(cls, requests_mock):
"""
Given: A BaseClient object
When:
- Calling _http_request function with metrics
- A timeout error is returned.
Then: Verify the timeout error execution metrics is incremented.
"""
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', exc=requests.exceptions.ConnectTimeout)
client = cls.BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), verify=False)
with pytest.raises(DemistoException):
client._http_request('get', 'event', resp_type='response', with_metrics=True)
assert client.execution_metrics.timeout_error == 1
def test_http_request_execution_metrics_ssl_error(cls, requests_mock):
"""
Given: A BaseClient object
When:
- Calling _http_request function with metrics
- An SSL error is returned.
Then: Verify the ssl error execution metrics is incremented.
"""
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', exc=requests.exceptions.SSLError)
client = cls.BaseClient('http://example.com/api/v2/', ok_codes=(200, 201))
with pytest.raises(DemistoException):
client._http_request('get', 'event', resp_type='response', with_metrics=True)
assert client.execution_metrics.ssl_error == 1
def test_http_request_execution_metrics_proxy_error(cls, requests_mock):
"""
Given: A BaseClient object
When:
- Calling _http_request function with metrics
- A proxy error is returned.
Then: Verify the proxy error execution metrics is incremented.
"""
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', exc=requests.exceptions.ProxyError)
client = cls.BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), verify=False)
with pytest.raises(DemistoException):
client._http_request('get', 'event', resp_type='response', with_metrics=True)
assert client.execution_metrics.proxy_error == 1
def test_http_request_execution_metrics_connection_error(cls, requests_mock):
"""
Given: A BaseClient object
When:
- Calling _http_request function with metrics
- A connection error is returned.
Then: Verify the connection error execution metrics is incremented.
"""
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', exc=requests.exceptions.ConnectionError)
client = cls.BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), verify=False)
with pytest.raises(DemistoException):
client._http_request('get', 'event', resp_type='response', with_metrics=True)
assert client.execution_metrics.connection_error == 1
def test_http_request_execution_metrics_retry_error(cls, requests_mock):
"""
Given: A BaseClient object
When:
- Calling _http_request function with metrics
- A retry error is returned.
Then: Verify the retry error execution metrics is incremented.
"""
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', exc=requests.exceptions.RetryError)
client = cls.BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), verify=False)
with pytest.raises(DemistoException):
client._http_request('get', 'event', resp_type='response', with_metrics=True)
assert client.execution_metrics.retry_error == 1
def test_http_request_execution_metrics_auth_error(cls, requests_mock):
"""
Given: A BaseClient object
When:
- Calling _http_request function with metrics
- An auth error (401 status code) is returned.
Then: Verify the auth error execution metrics is incremented.
"""
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', status_code=401, text="err")
client = cls.BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), verify=False)
with pytest.raises(DemistoException, match="Error in API call"):
client._http_request('get', 'event', with_metrics=True)
assert client.execution_metrics.auth_error == 1
def test_http_request_execution_metrics_quota_error(cls, requests_mock):
"""
Given: A BaseClient object
When:
- Calling _http_request function with metrics
- A quota error (429 status code) is returned.
Then: Verify the quota error execution metrics is incremented.
"""
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', status_code=429, text="err")
client = cls.BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), verify=False)
with pytest.raises(DemistoException, match="Error in API call"):
client._http_request('get', 'event', with_metrics=True)
assert client.execution_metrics.quota_error == 1
def test_http_request_execution_metrics_service_error(cls, requests_mock):
"""
Given: A BaseClient object
When:
- Calling _http_request function with metrics
- A service error (500 status code) is returned.
Then: Verify the service error execution metrics is incremented.
"""
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', status_code=500, text="err")
client = cls.BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), verify=False)
with pytest.raises(DemistoException, match="Error in API call"):
client._http_request('get', 'event', with_metrics=True)
assert client.execution_metrics.service_error == 1
def test_http_request_execution_metrics_general_error(cls, requests_mock):
"""
Given: A BaseClient object
When:
- Calling _http_request function with metrics
- A general error (400 status code) is returned.
Then: Verify the general error execution metrics is incremented.
"""
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', status_code=400, text="err")
client = cls.BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), verify=False)
with pytest.raises(DemistoException, match="Error in API call"):
client._http_request('get', 'event', with_metrics=True)
assert client.execution_metrics.general_error == 1
def test_http_request_execution_metrics_not_found_error_but_ok(cls, requests_mock):
"""
Given: A BaseClient object
When:
- Calling _http_request function with metrics
- A not found error (404 status code) is returned.
- 404 is considered ok
Then: Verify the success execution metrics is incremented, and not the general error metrics.
"""
requests_mock.get('http://example.com/api/v2/event', status_code=404, text="err")
client = cls.BaseClient('http://example.com/api/v2/', ok_codes=(200, 201, 404), verify=False)
res = client._http_request('get', 'event', resp_type='response', with_metrics=True)
assert res.status_code == 404
assert client.execution_metrics.success == 1
assert client.execution_metrics.general_error == 0
def test_http_request_execution_metrics_results(cls, requests_mock, mocker):
"""
Given: A BaseClient object
When:
- Calling _http_request function with metrics
- An general error is returned
- The client object is then deleted
Then: Verify an execution metrics entry is sent to demisto.results() accordingly.
"""
from CommonServerPython import DemistoException, EntryType, ErrorTypes
requests_mock.get('http://example.com/api/v2/event', status_code=400, text="err")
demisto_results_mock = mocker.patch.object(demisto, 'results')
client = cls.BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), verify=False)
with pytest.raises(DemistoException, match="Error in API call"):
client._http_request('get', 'event', with_metrics=True)
del client
demisto_results_mock.assert_called_once
entry = demisto_results_mock.call_args[0][0]
assert entry["Type"] == EntryType.EXECUTION_METRICS
assert entry["APIExecutionMetrics"] == [{
"Type": ErrorTypes.GENERAL_ERROR,
"APICallsCount": 1,
}]
def test_http_request_no_execution_metrics_results(cls, requests_mock, mocker):
"""
Given: A BaseClient object
When:
- Calling _http_request function without metrics
- An general error is returned
- The client object is then deleted
Then: Verify demisto.results() is not called.
"""
from CommonServerPython import DemistoException
requests_mock.get('http://example.com/api/v2/event', status_code=400, text="err")
demisto_results_mock = mocker.patch.object(demisto, 'results')
client = cls.BaseClient('http://example.com/api/v2/', ok_codes=(200, 201), verify=False)
with pytest.raises(DemistoException, match="Error in API call"):
client._http_request('get', 'event')
del client
demisto_results_mock.assert_not_called
def test_base_client_subclass_without_execution_metrics_initialized(self):
"""
Given: A BaseClient object and a subclass of it that does not initialize execution_metrics
When: deleting the client object
Then: Ensure the deletion does not raise any exception
"""
from CommonServerPython import BaseClient
class Client(BaseClient):
def __init__(self):
pass
client = Client()
del client
@pytest.mark.skipif(not IS_PY3, reason='test not supported in py2')
def test_http_request_params_parser_quote(self, requests_mock):
"""
Given
- query params with spaces with specific quote function.
When
- Calling _https_request function.
Then
- Verify the spaces in the result is as expected.
"""
mock_request = requests_mock.get('http://example.com/api/v2/', json={})
from CommonServerPython import BaseClient
mock_client = BaseClient('http://example.com/api/v2/')
mock_client._http_request('get', params={'key': 'value with spaces'}, params_parser=urllib.parse.quote)
assert mock_request.last_request.query == 'key=value%20with%20spaces'
def test_parse_date_string():
# test unconverted data remains: Z
assert parse_date_string('2019-09-17T06:16:39Z') == datetime(2019, 9, 17, 6, 16, 39)
# test unconverted data remains: .22Z
assert parse_date_string('2019-09-17T06:16:39.22Z') == datetime(2019, 9, 17, 6, 16, 39, 220000)
# test time data without ms does not match format with ms
assert parse_date_string('2019-09-17T06:16:39Z', '%Y-%m-%dT%H:%M:%S.%f') == datetime(2019, 9, 17, 6, 16, 39)
# test time data with timezone Z does not match format with timezone +05:00
assert parse_date_string('2019-09-17T06:16:39Z', '%Y-%m-%dT%H:%M:%S+05:00') == datetime(2019, 9, 17, 6, 16, 39)
# test time data with timezone +05:00 does not match format with timezone Z
assert parse_date_string('2019-09-17T06:16:39+05:00', '%Y-%m-%dT%H:%M:%SZ') == datetime(2019, 9, 17, 6, 16, 39)
# test time data with timezone -05:00 and with ms does not match format with timezone +02:00 without ms
assert parse_date_string(
'2019-09-17T06:16:39.4040+05:00', '%Y-%m-%dT%H:%M:%S+02:00'
) == datetime(2019, 9, 17, 6, 16, 39, 404000)
def test_override_print(mocker):
mocker.patch.object(demisto, 'info')
int_logger = IntegrationLogger()
int_logger.set_buffering(False)
int_logger.print_override("test", "this")
assert demisto.info.call_count == 1
assert demisto.info.call_args[0][0] == "test this"
demisto.info.reset_mock()
int_logger.print_override("test", "this", file=sys.stderr)
assert demisto.info.call_count == 1
assert demisto.info.call_args[0][0] == "test this"
buf = StringIO()
# test writing to custom file (not stdout/stderr)
int_logger.print_override("test", "this", file=buf)
assert buf.getvalue() == 'test this\n'
def test_http_client_debug(mocker):
if not IS_PY3:
pytest.skip("test not supported in py2")
return
mocker.patch.object(demisto, 'info')
debug_log = DebugLogger()
from http.client import HTTPConnection
HTTPConnection.debuglevel = 1
# not using 'with' because its not compatible with all python versions
con = HTTPConnection("google.com")
con.request('GET', '/')
with con.getresponse() as r:
r.read()
con.close()
assert demisto.info.call_count > 5
assert debug_log is not None
def test_http_client_debug_int_logger_sensitive_query_params(mocker):
if not IS_PY3:
pytest.skip("test not supported in py2")
return
mocker.patch.object(demisto, 'params', return_value={'APIKey': 'dummy'})
mocker.patch.object(demisto, 'info')
debug_log = DebugLogger()
from http.client import HTTPConnection
HTTPConnection.debuglevel = 1
con = HTTPConnection("google.com")
con.request('GET', '?apikey=dummy')
# not using 'with' because its not compatible with all python versions
with con.getresponse() as r:
r.read()
con.close()
assert debug_log
for arg in demisto.info.call_args_list:
assert 'dummy' not in arg[0][0]
if 'apikey' in arg[0][0]:
assert 'apikey=' in arg[0][0]
def test_safe_strptime():
"""
Given: A string and a format that is missing the milliseconds
When: Calling safe_strptime
Then: Verify the result is as expected
"""
from CommonServerPython import safe_strptime
assert safe_strptime("2024-01-15 17:00:00Z", "%Y-%m-%d %H:%M:%S.%fZ") == datetime(2024, 1, 15, 17, 0, 0)
assert safe_strptime("2024-01-15 17:00:00", "%Y-%m-%d %H:%M:%S") == datetime(2024, 1, 15, 17, 0, 0)
assert safe_strptime("2024-01-15 17:00:00", "%Y-%m-%d %H:%M:%S", strptime=time.strptime) == time.strptime("2024-01-15 17:00:00", "%Y-%m-%d %H:%M:%S")
with pytest.raises(ValueError):
safe_strptime("2024-01-15 17:00:00", "%Y-%m-%dT%H:%M:%S")
class TestParseDateRange:
@staticmethod
@freeze_time("2024-01-15 17:00:00 UTC")
def test_utc_time_sanity():
utc_now = datetime.utcnow()
utc_start_time, utc_end_time = parse_date_range('2 days', utc=True)
# testing UTC date time and range of 2 days
assert utc_now.replace(microsecond=0) == utc_end_time.replace(microsecond=0)
assert abs(utc_start_time - utc_end_time).days == 2
@staticmethod
@freeze_time("2024-01-15 17:00:00 UTC")
def test_local_time_sanity():
local_now = datetime.now()
local_start_time, local_end_time = parse_date_range('73 minutes', utc=False)
# testing local datetime and range of 73 minutes
assert local_now.replace(microsecond=0) == local_end_time.replace(microsecond=0)
assert abs(local_start_time - local_end_time).seconds / 60 == 73
@staticmethod
@freeze_time("2024-01-15 17:00:00 UTC")
def test_with_trailing_spaces():
utc_now = datetime.utcnow()
utc_start_time, utc_end_time = parse_date_range('2 days ', utc=True)
# testing UTC date time and range of 2 days
assert utc_now.replace(microsecond=0) == utc_end_time.replace(microsecond=0)
assert abs(utc_start_time - utc_end_time).days == 2
@staticmethod
@freeze_time("2022-11-03 13:40:00 UTC")
def test_case_insensitive():
utc_now = datetime.utcnow()
utc_start_time, utc_end_time = parse_date_range('2 Days', utc=True)
# testing UTC date time and range of 2 days
assert utc_now.replace(microsecond=0) == utc_end_time.replace(microsecond=0)
assert abs(utc_start_time - utc_end_time).days == 2
@staticmethod
@freeze_time("2024-01-15 17:00:00 UTC")
def test_error__invalid_input_format(mocker):
mocker.patch.object(sys, 'exit', side_effect=Exception('mock exit'))
demisto_results = mocker.spy(demisto, 'results')
try:
parse_date_range('2 Days ago', utc=True)
except Exception as exp:
assert str(exp) == 'mock exit'
results = demisto.results.call_args[0][0]
assert 'date_range must be "number date_range_unit"' in results['Contents']
@staticmethod
@freeze_time("2024-01-15 17:00:00 UTC")
def test_error__invalid_time_value_not_a_number(mocker):
mocker.patch.object(sys, 'exit', side_effect=Exception('mock exit'))
demisto_results = mocker.spy(demisto, 'results')
try:
parse_date_range('ten Days', utc=True)
except Exception as exp:
assert str(exp) == 'mock exit'
results = demisto.results.call_args[0][0]
assert 'The time value is invalid' in results['Contents']
@staticmethod
@freeze_time("2024-01-15 17:00:00 UTC")
def test_error__invalid_time_value_not_an_integer(mocker):
mocker.patch.object(sys, 'exit', side_effect=Exception('mock exit'))
demisto_results = mocker.spy(demisto, 'results')
try:
parse_date_range('1.5 Days', utc=True)
except Exception as exp:
assert str(exp) == 'mock exit'
results = demisto.results.call_args[0][0]
assert 'The time value is invalid' in results['Contents']
@staticmethod
@freeze_time("2024-01-15 17:00:00 UTC")
def test_error__invalid_time_unit(mocker):
mocker.patch.object(sys, 'exit', side_effect=Exception('mock exit'))
demisto_results = mocker.spy(demisto, 'results')
try:
parse_date_range('2 nights', utc=True)
except Exception as exp:
assert str(exp) == 'mock exit'
results = demisto.results.call_args[0][0]
assert 'The unit of date_range is invalid' in results['Contents']
def test_encode_string_results():
s = "test"
assert s == encode_string_results(s)
s2 = u"בדיקה"
if IS_PY3:
res = str(s2)
else:
res = s2.encode("utf8")
assert encode_string_results(s2) == res
not_string = [1, 2, 3]
assert not_string == encode_string_results(not_string)
class TestReturnOutputs:
def test_return_outputs(self, mocker):
mocker.patch.object(demisto, 'results')
md = 'md'
outputs = {'Event': 1}
raw_response = {'event': 1}
return_outputs(md, outputs, raw_response)
results = demisto.results.call_args[0][0]
assert len(demisto.results.call_args[0]) == 1
assert demisto.results.call_count == 1
assert raw_response == results['Contents']
assert 'json' == results['ContentsFormat']
assert outputs == results['EntryContext']
assert md == results['HumanReadable']
def test_return_outputs_only_md(self, mocker):
mocker.patch.object(demisto, 'results')
md = 'md'
return_outputs(md)
results = demisto.results.call_args[0][0]
assert len(demisto.results.call_args[0]) == 1
assert demisto.results.call_count == 1
assert md == results['HumanReadable']
assert 'text' == results['ContentsFormat']
def test_return_outputs_raw_none(self, mocker):
mocker.patch.object(demisto, 'results')
md = 'md'
outputs = {'Event': 1}
return_outputs(md, outputs, None)
results = demisto.results.call_args[0][0]
assert len(demisto.results.call_args[0]) == 1
assert demisto.results.call_count == 1
assert outputs == results['Contents']
assert 'json' == results['ContentsFormat']
assert outputs == results['EntryContext']
assert md == results['HumanReadable']
def test_return_outputs_timeline(self, mocker):
mocker.patch.object(demisto, 'results')
md = 'md'
outputs = {'Event': 1}
raw_response = {'event': 1}
timeline = [{'Value': 'blah', 'Message': 'test', 'Category': 'test'}]
return_outputs(md, outputs, raw_response, timeline)
results = demisto.results.call_args[0][0]
assert len(demisto.results.call_args[0]) == 1
assert demisto.results.call_count == 1
assert raw_response == results['Contents']
assert 'json' == results['ContentsFormat']
assert outputs == results['EntryContext']
assert md == results['HumanReadable']
assert timeline == results['IndicatorTimeline']
def test_return_outputs_timeline_without_category(self, mocker):
mocker.patch.object(demisto, 'results')
md = 'md'
outputs = {'Event': 1}
raw_response = {'event': 1}
timeline = [{'Value': 'blah', 'Message': 'test'}]
return_outputs(md, outputs, raw_response, timeline)
results = demisto.results.call_args[0][0]
assert len(demisto.results.call_args[0]) == 1
assert demisto.results.call_count == 1
assert raw_response == results['Contents']
assert 'json' == results['ContentsFormat']
assert outputs == results['EntryContext']
assert md == results['HumanReadable']
assert 'Category' in results['IndicatorTimeline'][0].keys()
assert results['IndicatorTimeline'][0]['Category'] == 'Integration Update'
def test_return_outputs_ignore_auto_extract(self, mocker):
mocker.patch.object(demisto, 'results')
md = 'md'
outputs = {'Event': 1}
raw_response = {'event': 1}
ignore_auto_extract = True
return_outputs(md, outputs, raw_response, ignore_auto_extract=ignore_auto_extract)
results = demisto.results.call_args[0][0]
assert len(demisto.results.call_args[0]) == 1
assert demisto.results.call_count == 1
assert raw_response == results['Contents']
assert 'json' == results['ContentsFormat']
assert outputs == results['EntryContext']
assert md == results['HumanReadable']
assert ignore_auto_extract == results['IgnoreAutoExtract']
def test_return_outputs_text_raw_response(self, mocker):
mocker.patch.object(demisto, 'results')
md = 'md'
raw_response = 'string'
return_outputs(md, raw_response=raw_response)
results = demisto.results.call_args[0][0]
assert len(demisto.results.call_args[0]) == 1
assert demisto.results.call_count == 1
assert raw_response == results['Contents']
assert 'text' == results['ContentsFormat']
@pytest.mark.parametrize(
"input_value,expected_result",
[
# True values
pytest.param('true', True, id='string_true_lowercase'),
pytest.param('yes', True, id='string_yes_lowercase'),
pytest.param('TrUe', True, id='string_true_mixed_case'),
pytest.param(True, True, id='boolean_true'),
pytest.param('y', True, id='string_y_lowercase'),
pytest.param('Y', True, id='string_y_uppercase'),
pytest.param('t', True, id='string_t_lowercase'),
pytest.param('T', True, id='string_t_uppercase'),
pytest.param('on', True, id='string_on_lowercase'),
pytest.param('ON', True, id='string_on_uppercase'),
pytest.param('1', True, id='string_one'),
# False values
pytest.param('false', False, id='string_false_lowercase'),
pytest.param('no', False, id='string_no_lowercase'),
pytest.param(False, False, id='boolean_false'),
pytest.param('n', False, id='string_n_lowercase'),
pytest.param('N', False, id='string_n_uppercase'),
pytest.param('f', False, id='string_f_lowercase'),
pytest.param('F', False, id='string_f_uppercase'),
pytest.param('off', False, id='string_off_lowercase'),
pytest.param('OFF', False, id='string_off_uppercase'),
pytest.param('0', False, id='string_zero'),
]
)
def test_argToBoolean(input_value, expected_result):
assert argToBoolean(input_value) is expected_result
batch_params = [
# full batch case
([1, 2, 3], 1, [[1], [2], [3]]),
# empty case
([], 1, []),
# out of index case
([1, 2, 3], 5, [[1, 2, 3]]),
# out of index in end with batches
([1, 2, 3, 4, 5], 2, [[1, 2], [3, 4], [5]]),
([1] * 100, 2, [[1, 1]] * 50)
]
@pytest.mark.parametrize('iterable, sz, expected', batch_params)
def test_batch(iterable, sz, expected):
for i, item in enumerate(batch(iterable, sz)):
assert expected[i] == item
regexes_test = [
(ipv4Regex, '192.168.1.1', True),
(ipv4Regex, '192.168.1.1:8080', True),
(ipv4Regex, '192.168.1.1/24', False),
(ipv4Regex, '192.168.a.1', False),
(ipv4Regex, '192.168..1.1', False),
(ipv4Regex, '192.256.1.1', False),
(ipv4Regex, '192.256.1.1.1', False),
(ipv4Regex, '192.168.1.1/12', False),
(ipv4Regex, '', False),
(ipv4cidrRegex, '192.168.1.1/32', True),
(ipv4cidrRegex, '192.168.1.1.1/30', False),
(ipv4cidrRegex, '192.168.1.b/30', False),
(ipv4cidrRegex, '192.168.1.12/381', False),
(ipv6Regex, '2001:db8:a0b:12f0::1', True),
(ipv6Regex, '2001:db8:a0b:12f0::1/11', False),
(ipv6Regex, '2001:db8:a0b:12f0::1::1', False),
(ipv6Regex, '2001:db8:a0b:12f0::98aa5', False),
(ipv6cidrRegex, '2001:db8:a0b:12f0::1/64', True),
(ipv6cidrRegex, '2001:db8:a0b:12f0::1/256', False),
(ipv6cidrRegex, '2001:db8:a0b:12f0::1::1/25', False),
(ipv6cidrRegex, '2001:db8:a0b:12f0::1aaasds::1/1', False)
]
@pytest.mark.parametrize('pattern, string, expected', regexes_test)
def test_regexes(pattern, string, expected):
# (str, str, bool) -> None
# emulates re.fullmatch from py3.4
assert expected is bool(re.match("(?:" + pattern + r")\Z", string))
IP_TO_INDICATOR_TYPE_PACK = [
('192.168.1.1', FeedIndicatorType.IP),
('192.168.1.1:8080', FeedIndicatorType.IP),
('192.168.1.1/32', FeedIndicatorType.CIDR),
('2001:db8:a0b:12f0::1', FeedIndicatorType.IPv6),
('2001:db8:a0b:12f0::1/64', FeedIndicatorType.IPv6CIDR),
]
@pytest.mark.parametrize('ip, indicator_type', IP_TO_INDICATOR_TYPE_PACK)
def test_ip_to_indicator(ip, indicator_type):
assert FeedIndicatorType.ip_to_indicator_type(ip) is indicator_type
data_test_b64_encode = [
(u'test', 'dGVzdA=='),
('test', 'dGVzdA=='),
(b'test', 'dGVzdA=='),
('', ''),
('%', 'JQ=='),
(u'§', 'wqc='),
(u'§t`e§s`t§', 'wqd0YGXCp3NgdMKn'),
]
@pytest.mark.parametrize('_input, expected_output', data_test_b64_encode)
def test_b64_encode(_input, expected_output):
output = b64_encode(_input)
assert output == expected_output, 'b64_encode({}) returns: {} instead: {}'.format(_input, output, expected_output)
B64_STR = 'This is a test!'
DECODED_B64 = b'N\x18\xac\x8a\xc6\xadz\xcb'
CASE_NO_PADDING = (B64_STR, DECODED_B64)
CASE_LESS_PADDING = (B64_STR + '=', DECODED_B64)
CASE_WITH_PADDING = (B64_STR + '==', DECODED_B64)
CASE_TOO_MUCH_PADDING = (B64_STR + '===', DECODED_B64)
@pytest.mark.parametrize('str_to_decode, expected_encoded',
(CASE_NO_PADDING, CASE_WITH_PADDING, CASE_LESS_PADDING, CASE_TOO_MUCH_PADDING))
def test_b64_decode(str_to_decode, expected_encoded):
"""
Given: A base 64 encoded str that represents an image, with different paddings.
When: Decoding it to an image file.
Then: The str is decoded to binary.
"""
encoded = b64_decode(str_to_decode)
assert encoded == expected_encoded
def test_traceback_in_return_error_debug_mode_on(mocker):
mocker.patch.object(demisto, 'command', return_value="test-command")
mocker.spy(demisto, 'results')
mocker.patch('CommonServerPython.is_debug_mode', return_value=True)
from CommonServerPython import return_error
try:
raise Exception("This is a test string")
except Exception:
with pytest.raises(SystemExit):
return_error("some text")
assert "This is a test string" in str(demisto.results.call_args)
assert "Traceback" in str(demisto.results.call_args)
assert "some text" in str(demisto.results.call_args)
def test_traceback_in_return_error_debug_mode_off(mocker):
mocker.patch.object(demisto, 'command', return_value="test-command")
mocker.spy(demisto, 'results')
mocker.patch('CommonServerPython.is_debug_mode', return_value=False)
from CommonServerPython import return_error
try:
raise Exception("This is a test string")
except Exception:
with pytest.raises(SystemExit):
return_error("some text")
assert "This is a test string" not in str(demisto.results.call_args)
assert "Traceback" not in str(demisto.results.call_args)
assert "some text" in str(demisto.results.call_args)
# append_context unit test
CONTEXT_MOCK = {
'str_key': 'str_value',
'dict_key': {
'key1': 'val1',
'key2': 'val2'
},
'int_key': 1,
'list_key_str': ['val1', 'val2'],
'list_key_list': ['val1', 'val2'],
'list_key_dict': ['val1', 'val2']
}
UPDATED_CONTEXT = {
'str_key': 'str_data,str_value',
'dict_key': {
'key1': 'val1',
'key2': 'val2',
'data_key': 'data_val'
},
'int_key': [1, 2],
'list_key_str': ['val1', 'val2', 'str_data'],
'list_key_list': ['val1', 'val2', 'val1', 'val2'],
'list_key_dict': ['val1', 'val2', {'data_key': 'data_val'}]
}
UPDATED_CONTEXT_WITH_LIST = {
'dict_key': [{
'key1': 'val1',
'key2': 'val2',
'key3': 'val3'
}],
'int_key': [1, 2],
'list_key_str': ['val1', 'val2', 'str_data'],
'list_key_list': ['val1', 'val2', 'val1', 'val2'],
'list_key_dict': ['val1', 'val2', {'data_key': 'data_val'}]
}
DATA_MOCK_STRING = "str_data"
DATA_MOCK_LIST = ['val1', 'val2']
DATA_MOCK_LIST_OF_DICT = [{'key3': 'val3'}]
DATA_MOCK_DICT = {
'data_key': 'data_val'
}
DATA_MOCK_INT = 2
STR_KEY = "str_key"
DICT_KEY = "dict_key"
APPEND_CONTEXT_INPUT = [
(CONTEXT_MOCK, DATA_MOCK_STRING, STR_KEY, "key = {}, val = {}".format(STR_KEY, UPDATED_CONTEXT[STR_KEY])),
(CONTEXT_MOCK, DATA_MOCK_LIST, STR_KEY, "TypeError"),
(CONTEXT_MOCK, DATA_MOCK_DICT, STR_KEY, "TypeError"),
(CONTEXT_MOCK, DATA_MOCK_STRING, DICT_KEY, "TypeError"),
(CONTEXT_MOCK, DATA_MOCK_LIST, DICT_KEY, "TypeError"),
(CONTEXT_MOCK, DATA_MOCK_LIST_OF_DICT, DICT_KEY, "key = {}, val = {}".format(DICT_KEY, UPDATED_CONTEXT_WITH_LIST[DICT_KEY])),
(CONTEXT_MOCK, DATA_MOCK_DICT, DICT_KEY, "key = {}, val = {}".format(DICT_KEY, UPDATED_CONTEXT[DICT_KEY])),
(CONTEXT_MOCK, DATA_MOCK_STRING, 'list_key_str',
"key = {}, val = {}".format('list_key_str', UPDATED_CONTEXT['list_key_str'])),
(CONTEXT_MOCK, DATA_MOCK_LIST, 'list_key_list',
"key = {}, val = {}".format('list_key_list', UPDATED_CONTEXT['list_key_list'])),
(CONTEXT_MOCK, DATA_MOCK_DICT, 'list_key_dict',
"key = {}, val = {}".format('list_key_dict', UPDATED_CONTEXT['list_key_dict'])),
(CONTEXT_MOCK, DATA_MOCK_INT, 'int_key', "key = {}, val = {}".format('int_key', UPDATED_CONTEXT['int_key'])),
]
def get_set_context(key, val):
from CommonServerPython import return_error
return_error("key = {}, val = {}".format(key, val))
@pytest.mark.parametrize('context_mock, data_mock, key, expected_answer', APPEND_CONTEXT_INPUT)
def test_append_context(mocker, context_mock, data_mock, key, expected_answer):
from CommonServerPython import demisto
mocker.patch.object(demisto, 'get', return_value=context_mock.get(key))
mocker.patch.object(demisto, 'setContext', side_effect=get_set_context)
mocker.patch.object(demisto, 'results')
if "TypeError" not in expected_answer:
with pytest.raises(SystemExit, match='0'):
appendContext(key, data_mock)
assert expected_answer in demisto.results.call_args[0][0]['Contents']
else:
with pytest.raises(TypeError) as e:
appendContext(key, data_mock)
assert expected_answer in e.value
INDICATOR_VALUE_AND_TYPE = [
('3fec1b14cea32bbcd97fad4507b06888', "File"),
('1c8893f75089a27ca6a8d49801d7aa6b64ea0c6167fe8b1becfe9bc13f47bdc1', 'File'),
('castaneda-thornton.com', 'Domain'),
('192.0.0.1', 'IP'),
('test@gmail.com', 'Email'),
('test@Demisto.com', 'Email'),
('Test@demisto.com', 'Email'),
('TEST@demisto.com', 'Email'),
('TEST@Demisto.com', 'Email'),
('TEST@DEMISTO.Com', 'Email'),
('TesT@DEMISTO.Com', 'Email'),
('TesT@DemisTO.Com', 'Email'),
('TesT@DeMisTo.CoM', 'Email'),
('TEST@DEMISTO.COM', 'Email'),
('e775eb1250137c0b83d4e7c4549c71d6f10cae4e708ebf0b5c4613cbd1e91087', 'File'),
('test@yahoo.com', 'Email'),
('http://test.com', 'URL'),
('11.111.11.11/11', 'CIDR'),
('CVE-0000-0000', 'CVE'),
('dbot@demisto.works', 'Email'),
('dummy@recipient.com', 'Email'),
('bruce.wayne@pharmtech.zz', 'Email'),
('joe@gmail.com', 'Email'),
('koko@demisto.com', 'Email'),
('42a5e275559a1651b3df8e15d3f5912499f0f2d3d1523959c56fc5aea6371e59', 'File'),
('10676cf66244cfa91567fbc1a937f4cb19438338b35b69d4bcc2cf0d3a44af5e', 'File'),
('52483514f07eb14570142f6927b77deb7b4da99f', 'File'),
('c8092abd8d581750c0530fa1fc8d8318', 'File'),
('fe80:0000:0000:0000:91ba:7558:26d3:acde', 'IPv6'),
('fd60:e22:f1b9::2', 'IPv6'),
('2001:db8:0000:0000:0000:0000:0000:0000', 'IPv6'),
('112.126.94.107', 'IP'),
('a', None),
('*castaneda-thornton.com', 'DomainGlob'),
(
'53e6baa124f54462786f1122e98e38ff1be3de82fe2a96b1849a8637043fd847eec7e0f53307bddf7a066565292d500c36c941f1f3bb9dcac807b2f4a0bfce1b',
'File'),
('1[.]1[.]1[.]1', 'IP'),
('test[@]test.com', 'Email'),
('https[:]//www[.]test[.]com/abc', 'URL'),
('test[.]com', 'Domain'),
('https://192.168.1.1:8080', 'URL'),
]
@pytest.mark.parametrize('indicator_value, indicatory_type', INDICATOR_VALUE_AND_TYPE)
def test_auto_detect_indicator_type(indicator_value, indicatory_type):
"""
Given
- Indicator value
- Indicator type
When
- Trying to detect the type of an indicator.
Then
- Run the auto_detect_indicator_type and validate that the indicator type the function returns is as expected.
"""
if sys.version_info.major == 3 and sys.version_info.minor == 8:
assert auto_detect_indicator_type(indicator_value) == indicatory_type
else:
try:
auto_detect_indicator_type(indicator_value)
except Exception as e:
assert str(e) == "Missing tldextract module, In order to use the auto detect function please" \
" use a docker image with it installed such as: demisto/jmespath"
def test_auto_detect_indicator_type_tldextract(mocker):
"""
Given
tldextract version is lower than 3.0.0
When
Trying to detect the type of an indicator.
Then
Run the auto_detect_indicator_type and validate that tldextract using `cache_file` arg and not `cache_dir`
"""
if sys.version_info.major == 3 and sys.version_info.minor >= 8:
import tldextract as tlde
tlde.__version__ = '2.2.7'
mocker.patch.object(tlde, 'TLDExtract')
auto_detect_indicator_type('8')
res = tlde.TLDExtract.call_args
assert 'cache_file' in res[1].keys()
VALID_DOMAIN_INDICATORS = ['www.static.attackiqtes.com',
'test.com',
'www.testö.com',
'hxxps://path.test.com/check',
'https%3A%2F%2Ftwitter.com%2FPhilipsBeLux&data=02|01||cb2462dc8640484baf7608d638d2a698|1a407a2d7675'
'4d178692b3ac285306e4|0|0|636758874714819880&sdata=dnJiphWFhnAKsk5Ps0bj0p%2FvXVo8TpidtGZcW6t8lDQ%3'
'D&reserved=0%3E%5bcid:image003.gif@01CF4D7F.1DF62650%5d%3C',
'https://emea01.safelinks.protection.outlook.com/',
'good.good']
@pytest.mark.parametrize('indicator_value', VALID_DOMAIN_INDICATORS)
def test_valid_domain_indicator_types(indicator_value):
"""
Given
- Valid Domain indicators.
When
- Trying to match those indicators with the Domain regex.
Then
- The indicators are classified as Domain indicators.
"""
assert re.match(domainRegex, indicator_value)
INVALID_DOMAIN_INDICATORS = ['aaa.2234',
'1.1.1.1',
'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
'1.1',
'2001 : db8: 3333 : 4444 : 5555',
'test..com',
'test/com',
'3.21.32.65/path']
@pytest.mark.parametrize('indicator_value', INVALID_DOMAIN_INDICATORS)
def test_invalid_domain_indicator_types(indicator_value):
"""
Given
- invalid Domain indicators.
When
- Trying to match those indicators with the Domain regex.
Then
- The indicators are not classified as Domain indicators.
"""
assert not re.match(domainRegex, indicator_value)
VALID_URL_INDICATORS = [
'3.21.32.65/path',
'19.117.63.253:28/other/path',
'19.117.63.253:28/path',
'1.1.1.1/7/server/somestring/something.php?fjjasjkfhsjasofds=sjhfhdsfhasld',
'flake8.pycqa.org/en/latest',
'2001:db8:85a3:8d3:1319:8a2e:370:7348/path/path',
'2001:db8:85a3:8d3:1319:8a2e:370:7348/32/path/path',
'https://google.com/sdlfdshfkle3247239elkxszmcdfdstgk4e5pt0/path/path/oatdsfk/sdfjjdf',
'www.google.com/path',
'wwW.GooGle.com/path',
'2001:db8:85a3:8d3:1319:8a2e:370:7348/65/path/path',
'2001:db8:3333:4444:5555:6666:7777:8888/32/path/path',
'2001:db8:85a3:8d3:1319:8a2e:370:7348/h',
'1.1.1.1/7/server',
"1.1.1.1/32/path",
'https://evil.tld/evil.html',
'www.evil.tld/evil.aspx',
'sftp://69.254.57.79:5001/path',
'sftp://75.26.0.1/path',
'www.evil.tld/resource',
'hxxps://google[.]com:443/path',
'http://xn--e1v2i3l4.tld/evilagain.aspx',
'www.evil.tld:443/path/to/resource.html',
'WWW.evil.tld:443/path/to/resource.html',
'wWw.Evil.tld:443/path/to/resource.html',
'Https://wWw.Evil.tld:443/path/to/resource.html',
'https://1.2.3.4/path/to/resource.html',
'HTTPS://1.2.3.4/path/to/resource.html',
'1.2.3.4/path',
'1.2.3.4/path/to/resource.html',
'http://1.2.3.4:8080/resource.html',
'HTTP://1.2.3.4:80/path',
'ftp://foo.bar/resource',
'FTP://foo.bar/resource',
'ftps://foo.bar/resource',
'ftps://foo.bar/Resource',
'5.6.7.8/fdsfs',
'https://serverName.com/deepLinkAction.do?userName=peter%40nable%2Ecom&password=Hello',
'http://serverName.org/deepLinkAction.do?userName=peter%40nable%2Ecom&password=Hello',
'https://1.1.1.1/deepLinkAction.do?userName=peter%40nable%2Ecom&password=Hello',
'https://google.com/deepLinkAction.do?userName=peter%40nable%2Ecom&password=Hello',
'www.google.com/deepLinkAction.do?userName=peter%40nable%2Ecom&password=Hello',
'https://xsoar.pan.dev/docs/welcome',
'5.6.7.8/user/',
'http://www.example.com/and%26here.html',
'https://0.0.0.1/path',
'hxxp://0[x]455e8c6f/0s19ef206s18s2f2s567s49a8s91f7s4s19fd61a', # defanged hexa-decimal IP.
'hxxp://0x325e5c7f/34823jdsasjfd/asdsafgf/324', # hexa-decimal IP.
'hxxps://0xAB268DC1:8080/path',
'hxxps://0xAB268DC1/p',
'hxxps://0xAB268DC1/32',
'http://www.google[.]com:8080/path',
'http://www[.]google.com:8080/path',
'www[.]google.com:8080/path',
'www.google[.]com:8080/path',
'google[.]com/path',
'google[.]com:443/path',
'hXXps://1.1.1.1[.]edu/path',
'2001:db8:85a3:8d3:1319:8a2e:370:7348/80',
'2001:0db8:0001:0000:0000:0ab9:C0A8:0102/resource.html',
'2251:dbc:8fa3:8d3:1f19:8a2e:370:7348/80',
'https[:]//www.test.com/test', # defanged colon sign
"hxxp[:]//1[.]1[.]1[.]1/test[.]php", # Defanged URL with ip as a domain
"hxxp[:]//test[.]com/test[.]php", # Defanged URL with a file extension
"https://test.com/a/b/c-d-e", # hyphen in the path
]
@pytest.mark.parametrize('indicator_value', VALID_URL_INDICATORS)
def test_valid_url_indicator_types(indicator_value):
"""
Given
- Valid URL indicators.
When
- Trying to match those indicators with the URL regex.
Then
- The indicators are classified as URL indicators.
"""
regex_match = re.match(urlRegex, indicator_value)
assert regex_match.group(0) == indicator_value
INVALID_URL_INDICATORS = [
'www.google.com',
'one.two.three.four.com',
'one.two.three.com',
'test',
'httn://bla.com/path',
'google.com*',
'1.1.1.1',
'path/path',
'1.1.1.1:111112243245/path',
'3.4.6.92:8080:/test',
'1.1.1.1:4lll/',
'flake8.pycqa.org',
'google.com',
'HTTPS://dsdffd.c', # not valid tld
'https://test',
'ftp://test',
'ftps:test',
'a.a.a.a',
'b.b.b',
'https:/1.1.1.1.1/path',
'wwww.test',
'help.test.com',
'help-test/com',
'fnvfdsbf/path',
'65.23.7.2',
'k.f.a.f',
'test/test/test/test',
'',
'somestring',
'dsjfshjdfgkjldsh32423123^^&*#@$#@$@!#4',
'aaa/1.1.1.1/path',
'domain*com/1.1.1.1/path',
'http:1.1.1.1/path',
'kfer93420932/path/path',
'1.1.1.1.1/24',
'2.2.2.2.2/3sad',
'http://fdsfesd',
'http://fdsfesd:8080', # no tld
'FTP://Google.',
'https://www.',
'1.1.1.1.1/path',
'2.2.2.2.2/3sad',
'HTTPS://1.1.1.1..1.1.1.1/path',
'https://1.1.1.1.1.1.1.1.1.1.1/path',
'1.1.1.1 .1/path',
' test.com',
'test .com.domain',
'hxxps://0xAB26:8080/path', # must be 8 hexa-decimal chars
'hxxps://34543645356432234e:8080/path', # too large integer IP
'https://35.12.5677.143423:443', # invalid IP address
'https://4578.2436.1254.7423', # invalid octal address (must be numbers between 0-7)
'https://4578.2436.1254.7423:443/p',
'FTP://foo hXXps://1.1.1.1[.]edu/path',
'https://216.58.199.78:12345fdsf',
'https://www.216.58.199.78:sfsdg'
]
@pytest.mark.parametrize('indicator_value', INVALID_URL_INDICATORS)
def test_invalid_url_indicator_types(indicator_value):
"""
Given
- invalid URL indicators.
When
- Trying to match those indicators with the URL regex.
Then
- The indicators are not classified as URL indicators.
"""
assert not re.match(urlRegex, indicator_value)
def test_handle_proxy(mocker):
os.environ['REQUESTS_CA_BUNDLE'] = '/test1.pem'
mocker.patch.object(demisto, 'params', return_value={'insecure': True})
handle_proxy()
assert os.getenv('REQUESTS_CA_BUNDLE') is None
os.environ['REQUESTS_CA_BUNDLE'] = '/test2.pem'
mocker.patch.object(demisto, 'params', return_value={})
handle_proxy()
assert os.environ['REQUESTS_CA_BUNDLE'] == '/test2.pem' # make sure no change
mocker.patch.object(demisto, 'params', return_value={'unsecure': True})
handle_proxy()
assert os.getenv('REQUESTS_CA_BUNDLE') is None
def test_handle_proxy_without_http_prefix():
"""
Given
proxy is configured in environment vars without http/https prefixes
When
run handle_proxy()
Then
the function will return proxies with http:// prefix
"""
os.environ['HTTP_PROXY'] = 'testproxy:8899'
os.environ['HTTPS_PROXY'] = 'testproxy:8899'
proxies = handle_proxy(checkbox_default_value=True)
assert proxies['http'] == 'http://testproxy:8899'
assert proxies['https'] == 'http://testproxy:8899'
def test_handle_proxy_with_http_prefix():
"""
Given
proxy is configured in environment vars with http/https prefixes
When
run handle_proxy()
Then
the function will return proxies unchanged
"""
os.environ['HTTP_PROXY'] = 'http://testproxy:8899'
os.environ['HTTPS_PROXY'] = 'https://testproxy:8899'
proxies = handle_proxy(checkbox_default_value=True)
assert proxies['http'] == 'http://testproxy:8899'
assert proxies['https'] == 'https://testproxy:8899'
def test_handle_proxy_with_socks5_prefix():
"""
Given
proxy is configured in environment vars with socks5 (socks proxy) prefixes
When
run handle_proxy()
Then
the function will return proxies unchanged
"""
os.environ['HTTP_PROXY'] = 'socks5://testproxy:8899'
os.environ['HTTPS_PROXY'] = 'socks5://testproxy:8899'
proxies = handle_proxy(checkbox_default_value=True)
assert proxies['http'] == 'socks5://testproxy:8899'
assert proxies['https'] == 'socks5://testproxy:8899'
@pytest.mark.parametrize(argnames="dict_obj, keys, expected, default_return_value",
argvalues=[
({'a': '1'}, ['a'], '1', None),
({'a': {'b': '2'}}, ['a', 'b'], '2', None),
({'a': {'b': '2'}}, ['a', 'c'], 'test', 'test'),
])
def test_safe_get(dict_obj, keys, expected, default_return_value):
from CommonServerPython import dict_safe_get
assert expected == dict_safe_get(dict_object=dict_obj,
keys=keys,
default_return_value=default_return_value)
MIRRORS = '''
[{
"channel_id":"GKQ86DVPH",
"channel_name": "incident-681",
"channel_topic": "incident-681",
"investigation_id":"681",
"mirror_type":"all",
"mirror_direction":"both",
"mirror_to":"group",
"auto_close":true,
"mirrored":true
},
{
"channel_id":"GKB19PA3V",
"channel_name": "group2",
"channel_topic": "cooltopic",
"investigation_id":"684",
"mirror_type":"all",
"mirror_direction":"both",
"mirror_to":"group",
"auto_close":true,
"mirrored":true
},
{
"channel_id":"GKB19PA3V",
"channel_name": "group2",
"channel_topic": "cooltopic",
"investigation_id":"692",
"mirror_type":"all",
"mirror_direction":"both",
"mirror_to":"group",
"auto_close":true,
"mirrored":true
},
{
"channel_id":"GKNEJU4P9",
"channel_name": "group3",
"channel_topic": "incident-713",
"investigation_id":"713",
"mirror_type":"all",
"mirror_direction":"both",
"mirror_to":"group",
"auto_close":true,
"mirrored":true
},
{
"channel_id":"GL8GHC0LV",
"channel_name": "group5",
"channel_topic": "incident-734",
"investigation_id":"734",
"mirror_type":"all",
"mirror_direction":"both",
"mirror_to":"group",
"auto_close":true,
"mirrored":true
}]
'''
CONVERSATIONS = '''[{
"id": "C012AB3CD",
"name": "general",
"is_channel": true,
"is_group": false,
"is_im": false,
"created": 1449252889,
"creator": "U012A3CDE",
"is_archived": false,
"is_general": true,
"unlinked": 0,
"name_normalized": "general",
"is_shared": false,
"is_ext_shared": false,
"is_org_shared": false,
"pending_shared": [],
"is_pending_ext_shared": false,
"is_member": true,
"is_private": false,
"is_mpim": false,
"topic": {
"value": "Company-wide announcements and work-based matters",
"creator": "",
"last_set": 0
},
"purpose": {
"value": "This channel is for team-wide communication and announcements. All team members are in this channel.",
"creator": "",
"last_set": 0
},
"previous_names": [],
"num_members": 4
},
{
"id": "C061EG9T2",
"name": "random",
"is_channel": true,
"is_group": false,
"is_im": false,
"created": 1449252889,
"creator": "U061F7AUR",
"is_archived": false,
"is_general": false,
"unlinked": 0,
"name_normalized": "random",
"is_shared": false,
"is_ext_shared": false,
"is_org_shared": false,
"pending_shared": [],
"is_pending_ext_shared": false,
"is_member": true,
"is_private": false,
"is_mpim": false,
"topic": {
"value": "Non-work banter and water cooler conversation",
"creator": "",
"last_set": 0
},
"purpose": {
"value": "A place for non-work-related flimflam.",
"creator": "",
"last_set": 0
},
"previous_names": [],
"num_members": 4
}]'''
OBJECTS_TO_KEYS = {
'mirrors': 'investigation_id',
'questions': 'entitlement',
'users': 'id'
}
def set_integration_context_versioned(integration_context, version=-1, sync=False):
global INTEGRATION_CONTEXT_VERSIONED
try:
if not INTEGRATION_CONTEXT_VERSIONED:
INTEGRATION_CONTEXT_VERSIONED = {'context': '{}', 'version': 0}
except NameError:
INTEGRATION_CONTEXT_VERSIONED = {'context': '{}', 'version': 0}
current_version = INTEGRATION_CONTEXT_VERSIONED['version']
if version != -1 and version <= current_version:
raise ValueError('DB Insert version {} does not match version {}'.format(current_version, version))
INTEGRATION_CONTEXT_VERSIONED = {'context': integration_context, 'version': current_version + 1}
def get_integration_context_versioned(refresh=False):
return INTEGRATION_CONTEXT_VERSIONED
def test_merge_lists():
from CommonServerPython import merge_lists
# Set
original = [{'id': '1', 'updated': 'n'}, {'id': '2', 'updated': 'n'}, {'id': '11', 'updated': 'n'}]
updated = [{'id': '1', 'updated': 'y'}, {'id': '3', 'updated': 'y'}, {'id': '11', 'updated': 'n', 'remove': True}]
expected = [{'id': '1', 'updated': 'y'}, {'id': '2', 'updated': 'n'}, {'id': '3', 'updated': 'y'}]
# Arrange
result = merge_lists(original, updated, 'id')
# Assert
assert len(result) == len(expected)
for obj in result:
assert obj in expected
def test_update_context_merge(mocker):
import CommonServerPython
# Set
set_integration_context_versioned({
'mirrors': MIRRORS,
'conversations': CONVERSATIONS
})
mocker.patch.object(demisto, 'getIntegrationContextVersioned', return_value=get_integration_context_versioned())
mocker.patch.object(demisto, 'setIntegrationContextVersioned', side_effecet=set_integration_context_versioned)
mocker.patch.object(CommonServerPython, 'is_versioned_context_available', return_value=True)
new_mirror = {
'channel_id': 'new_group',
'channel_name': 'incident-999',
'channel_topic': 'incident-999',
'investigation_id': '999',
'mirror_type': 'all',
'mirror_direction': 'both',
'mirror_to': 'group',
'auto_close': True,
'mirrored': False
}
mirrors = json.loads(MIRRORS)
mirrors.extend([new_mirror])
# Arrange
context, version = CommonServerPython.update_integration_context({'mirrors': [new_mirror]}, OBJECTS_TO_KEYS, True)
new_mirrors = json.loads(context['mirrors'])
# Assert
assert len(mirrors) == len(new_mirrors)
for mirror in mirrors:
assert mirror in new_mirrors
assert version == get_integration_context_versioned()['version']
def test_update_context_no_merge(mocker):
import CommonServerPython
# Set
set_integration_context_versioned({
'mirrors': MIRRORS,
'conversations': CONVERSATIONS
})
mocker.patch.object(demisto, 'getIntegrationContextVersioned', return_value=get_integration_context_versioned())
mocker.patch.object(demisto, 'setIntegrationContextVersioned', side_effecet=set_integration_context_versioned)
mocker.patch.object(CommonServerPython, 'is_versioned_context_available', return_value=True)
new_conversation = {
'id': 'A0123456',
'name': 'general'
}
conversations = json.loads(CONVERSATIONS)
conversations.extend([new_conversation])
# Arrange
context, version = CommonServerPython.update_integration_context({'conversations': conversations}, OBJECTS_TO_KEYS,
True)
new_conversations = json.loads(context['conversations'])
# Assert
assert conversations == new_conversations
assert version == get_integration_context_versioned()['version']
@pytest.mark.parametrize('versioned_available', [True, False])
def test_get_latest_integration_context(mocker, versioned_available):
import CommonServerPython
# Set
set_integration_context_versioned({
'mirrors': MIRRORS,
'conversations': CONVERSATIONS
})
mocker.patch.object(demisto, 'getIntegrationContextVersioned', return_value=get_integration_context_versioned())
mocker.patch.object(demisto, 'setIntegrationContextVersioned', side_effecet=set_integration_context_versioned)
mocker.patch.object(CommonServerPython, 'is_versioned_context_available', return_value=versioned_available)
mocker.patch.object(demisto, 'getIntegrationContext',
return_value={'mirrors': MIRRORS, 'conversations': CONVERSATIONS})
# Arrange
context, ver = CommonServerPython.get_integration_context_with_version(True)
# Assert
assert context == get_integration_context_versioned()['context']
assert ver == get_integration_context_versioned()['version'] if versioned_available else -1
def test_set_latest_integration_context(mocker):
import CommonServerPython
# Set
set_integration_context_versioned({
'mirrors': MIRRORS,
'conversations': CONVERSATIONS,
})
mocker.patch.object(demisto, 'getIntegrationContextVersioned', return_value=get_integration_context_versioned())
mocker.patch.object(demisto, 'setIntegrationContextVersioned', side_effecet=set_integration_context_versioned)
int_context = get_integration_context_versioned()
mocker.patch.object(CommonServerPython, 'update_integration_context',
side_effect=[(int_context['context'], int_context['version']),
(int_context['context'], int_context['version'] + 1)])
mocker.patch.object(CommonServerPython, 'set_integration_context', side_effect=[ValueError, int_context['context']])
# Arrange
CommonServerPython.set_to_integration_context_with_retries({}, OBJECTS_TO_KEYS)
int_context_calls = CommonServerPython.set_integration_context.call_count
int_context_args_1 = CommonServerPython.set_integration_context.call_args_list[0][0]
int_context_args_2 = CommonServerPython.set_integration_context.call_args_list[1][0]
# Assert
assert int_context_calls == 2
assert int_context_args_1 == (int_context['context'], True, int_context['version'])
assert int_context_args_2 == (int_context['context'], True, int_context['version'] + 1)
def test_set_latest_integration_context_es(mocker):
import CommonServerPython
# Set
mocker.patch.object(demisto, 'getIntegrationContextVersioned', return_value=get_integration_context_versioned())
mocker.patch.object(demisto, 'setIntegrationContextVersioned', side_effecet=set_integration_context_versioned)
es_inv_context_version_first = {'version': 5, 'sequenceNumber': 807, 'primaryTerm': 1}
es_inv_context_version_second = {'version': 7, 'sequenceNumber': 831, 'primaryTerm': 1}
mocker.patch.object(CommonServerPython, 'update_integration_context',
side_effect=[({}, es_inv_context_version_first),
({}, es_inv_context_version_second)])
mocker.patch.object(CommonServerPython, 'set_integration_context', side_effect=[ValueError, {}])
# Arrange
CommonServerPython.set_to_integration_context_with_retries({})
int_context_calls = CommonServerPython.set_integration_context.call_count
int_context_args_1 = CommonServerPython.set_integration_context.call_args_list[0][0]
int_context_args_2 = CommonServerPython.set_integration_context.call_args_list[1][0]
# Assert
assert int_context_calls == 2
assert int_context_args_1[1:] == (True, es_inv_context_version_first)
assert int_context_args_2[1:] == (True, es_inv_context_version_second)
def test_set_latest_integration_context_fail(mocker):
import CommonServerPython
# Set
set_integration_context_versioned({
'mirrors': MIRRORS,
'conversations': CONVERSATIONS,
})
mocker.patch.object(demisto, 'getIntegrationContextVersioned', return_value=get_integration_context_versioned())
mocker.patch.object(demisto, 'setIntegrationContextVersioned', side_effecet=set_integration_context_versioned)
int_context = get_integration_context_versioned()
mocker.patch.object(CommonServerPython, 'update_integration_context', return_value=(
int_context['context'], int_context['version']
))
mocker.patch.object(CommonServerPython, 'set_integration_context', side_effect=ValueError)
# Arrange
with pytest.raises(Exception):
CommonServerPython.set_to_integration_context_with_retries({}, OBJECTS_TO_KEYS)
int_context_calls = CommonServerPython.set_integration_context.call_count
# Assert
assert int_context_calls == CommonServerPython.CONTEXT_UPDATE_RETRY_TIMES
def test_get_x_content_info_headers(mocker):
test_license = 'TEST_LICENSE_ID'
test_brand = 'TEST_BRAND'
mocker.patch.object(
demisto,
'getLicenseID',
return_value=test_license
)
mocker.patch.object(
demisto,
'callingContext',
new_callable=mocker.PropertyMock(return_value={'context': {
'IntegrationBrand': test_brand,
'IntegrationInstance': 'TEST_INSTANCE',
}})
)
headers = get_x_content_info_headers()
assert headers['X-Content-LicenseID'] == test_license
assert headers['X-Content-Name'] == test_brand
def test_script_return_results_execution_metrics_command_results(mocker):
"""
Given:
- List of CommandResult and dicts that contains an execution metrics entry
- The command currently running is a script
When:
- Calling return_results()
Then:
- demisto.results() is called 1 time (without the execution metrics entry)
"""
from CommonServerPython import CommandResults, return_results
mocker.patch.object(demisto, 'callingContext', {'context': {'ExecutedCommands': [{'moduleBrand': 'Scripts'}]}})
demisto_results_mock = mocker.patch.object(demisto, 'results')
mock_command_results = [
CommandResults(outputs_prefix='Mock', outputs={'MockContext': 0}, entry_type=19),
CommandResults(outputs_prefix='Mock', outputs={'MockContext': 1}),
{'MockContext': 1, "Type": 19},
{'MockContext': 1, "Type": 1},
]
return_results(mock_command_results)
for call_args in demisto_results_mock.call_args_list:
for args in call_args.args:
if isinstance(args, list):
for arg in args:
assert arg["Type"] != 19
else:
assert args["Type"] != 19
assert demisto_results_mock.call_count == 2
def test_integration_return_results_execution_metrics_command_results(mocker):
"""
Given:
- List of CommandResult and dicts that contains an execution metrics entry
- The command currently running is an integration command
When:
- Calling return_results()
Then:
- demisto.results() is called 3 times (with the execution metrics entry included)
"""
from CommonServerPython import CommandResults, return_results
mocker.patch.object(demisto, 'callingContext', {'context': {'ExecutedCommands': [{'moduleBrand': 'integration'}]}})
demisto_results_mock = mocker.patch.object(demisto, 'results')
mock_command_results = [
CommandResults(outputs_prefix='Mock', outputs={'MockContext': 0}, entry_type=19),
CommandResults(outputs_prefix='Mock', outputs={'MockContext': 1}),
{'MockContext': 1, "Type": 19},
{'MockContext': 1, "Type": 19},
]
return_results(mock_command_results)
execution_metrics_entry_found = False
for call_args in demisto_results_mock.call_args_list:
if execution_metrics_entry_found:
break
for args in call_args.args:
if execution_metrics_entry_found:
break
execution_metrics_entry_found = args["Type"] != 19
assert execution_metrics_entry_found
assert demisto_results_mock.call_count == 3
def test_dynamic_section_script_return_results_execution_metrics_command_results(mocker):
"""
Given:
- List of CommandResult and dicts that contains execution metrics entries
- The command currently running is a dynamic-section script
When:
- Calling return_results()
Then:
- demisto.results() is called 2 times (without the 2 execution metrics entries)
"""
from CommonServerPython import CommandResults, return_results
mocker.patch.object(demisto, 'callingContext', {'context': {'ScriptName': 'some_script_name'}})
demisto_results_mock = mocker.patch.object(demisto, 'results')
mock_command_results = [
# CommandResults metrics entry: Should not be returned.
CommandResults(outputs_prefix='Mock', outputs={'MockContext': 0}, entry_type=19),
# CommandResults regular entry: Should be returned.
CommandResults(outputs_prefix='Mock', outputs={'MockContext': 1}),
# Dict metrics entry: Should not be returned.
{'MockContext': 1, "Type": 19},
# Dict regular entry: Should be returned.
{'MockContext': 1, "Type": 1},
]
return_results(mock_command_results)
for call_args in demisto_results_mock.call_args_list:
for args in call_args.args:
if isinstance(args, list):
for arg in args:
assert arg["Type"] != 19
else:
assert args["Type"] != 19
assert demisto_results_mock.call_count == 2
def test_return_results_multiple_command_results(mocker):
"""
Given:
- List of 2 CommandResult
When:
- Calling return_results()
Then:
- demisto.results() is called 2 times (with the list items)
"""
from CommonServerPython import CommandResults, return_results
demisto_results_mock = mocker.patch.object(demisto, 'results')
mock_command_results = []
for i in range(2):
mock_output = {'MockContext': i}
mock_command_results.append(CommandResults(outputs_prefix='Mock', outputs=mock_output))
return_results(mock_command_results)
assert demisto_results_mock.call_count == 2
def test_return_results_multiple_dict_results(mocker):
"""
Given:
- List of 2 dictionaries
When:
- Calling return_results()
Then:
- demisto.results() is called 1 time (with the list as an argument)
"""
from CommonServerPython import return_results
demisto_results_mock = mocker.patch.object(demisto, 'results')
mock_command_results = [{'MockContext': 0}, {'MockContext': 1}]
return_results(mock_command_results)
args, _ = demisto_results_mock.call_args_list[0]
assert demisto_results_mock.call_count == 1
assert [{'MockContext': 0}, {'MockContext': 1}] in args
def test_return_results_mixed_results(mocker):
"""
Given:
- List containing a CommandResult object and two dictionaries (representing a demisto result entries)
When:
- Calling return_results()
Then:
- Assert that demisto.results() is called 2 times .
- Assert that the first call was with the CommandResult object.
- Assert that the second call was with the two demisto results dicts.
"""
from CommonServerPython import CommandResults, return_results
demisto_results_mock = mocker.patch.object(demisto, 'results')
mock_command_results_object = CommandResults(outputs_prefix='Mock', outputs={'MockContext': 0})
mock_demisto_results_entry = [{'MockContext': 1}, {'MockContext': 2}]
return_results([mock_command_results_object] + mock_demisto_results_entry)
assert demisto_results_mock.call_count == 2
assert demisto_results_mock.call_args_list[0][0][0] == mock_command_results_object.to_context()
assert demisto_results_mock.call_args_list[1][0][0] == mock_demisto_results_entry
class TestExecuteCommand:
@staticmethod
def test_sanity(mocker):
"""
Given:
- A successful command with a single entry as output.
When:
- Calling execute_command.
Then:
- Assert that only the Contents value is returned.
"""
from CommonServerPython import execute_command, EntryType
demisto_execute_mock = mocker.patch.object(demisto, 'executeCommand',
return_value=[{'Type': EntryType.NOTE,
'Contents': {'hello': 'world'}}])
res = execute_command('command', {'arg1': 'value'})
execute_command_args = demisto_execute_mock.call_args_list[0][0]
assert demisto_execute_mock.call_count == 1
assert execute_command_args[0] == 'command'
assert execute_command_args[1] == {'arg1': 'value'}
assert res == {'hello': 'world'}
@staticmethod
def test_multiple_results(mocker):
"""
Given:
- A successful command with several entries as output.
When:
- Calling execute_command.
Then:
- Assert that the "Contents" values of all entries are returned.
"""
from CommonServerPython import execute_command, EntryType
entries = [
{'Type': EntryType.NOTE, 'Contents': {'hello': 'world'}},
{'Type': EntryType.NOTE, 'Context': 'no contents here'},
{'Type': EntryType.NOTE, 'Contents': {'entry': '2'}},
]
demisto_execute_mock = mocker.patch.object(demisto, 'executeCommand',
return_value=entries)
res = execute_command('command', {'arg1': 'value'})
assert demisto_execute_mock.call_count == 1
assert isinstance(res, list)
assert len(res) == 3
assert res[0] == {'hello': 'world'}
assert res[1] == {}
assert res[2] == {'entry': '2'}
@staticmethod
def test_raw_results(mocker):
"""
Given:
- A successful command with several entries as output.
When:
- Calling execute_command.
Then:
- Assert that the entire entries are returned.
"""
from CommonServerPython import execute_command, EntryType
entries = [
{'Type': EntryType.NOTE, 'Contents': {'hello': 'world'}},
{'Type': EntryType.NOTE, 'Context': 'no contents here'},
'text',
1337,
]
demisto_execute_mock = mocker.patch.object(demisto, 'executeCommand',
return_value=entries)
res = execute_command('command', {'arg1': 'value'}, extract_contents=False)
assert demisto_execute_mock.call_count == 1
assert isinstance(res, list)
assert len(res) == 4
assert res[0] == {'Type': EntryType.NOTE, 'Contents': {'hello': 'world'}}
assert res[1] == {'Type': EntryType.NOTE, 'Context': 'no contents here'}
assert res[2] == 'text'
assert res[3] == 1337
@staticmethod
def test_failure(mocker):
"""
Given:
- A command that fails.
When:
- Calling execute_command.
Then:
- Assert that DemistoException is raised with the original error.
"""
from CommonServerPython import execute_command, EntryType
error_entries = [
{'Type': EntryType.ERROR, 'Contents': 'error number 1'},
{'Type': EntryType.ERROR, 'Contents': 'error number 2'},
]
demisto_execute_mock = mocker.patch.object(demisto, 'executeCommand', return_value=error_entries)
with pytest.raises(DemistoException, match='Failed to execute'):
execute_command('bad', {'arg1': 'value'})
assert demisto_execute_mock.call_count == 1
@staticmethod
def test_failure_integration(monkeypatch):
from CommonServerPython import execute_command
monkeypatch.delattr(demisto, 'executeCommand')
with pytest.raises(DemistoException, match=r'Cannot run demisto.executeCommand\(\) from integrations.'):
execute_command('bad', {'arg1': 'value'})
@staticmethod
def test_multiple_results_fail_on_error_false(mocker):
"""
Given:
- A successful command with several entries as output.
- fail_on_error set to False.
When:
- Calling execute_command.
Then:
- Assert that the status of the execution is True for successful run.
- Assert that the "Contents" values of all entries are returned.
"""
from CommonServerPython import execute_command, EntryType
entries = [
{'Type': EntryType.NOTE, 'Contents': {'hello': 'world'}},
{'Type': EntryType.NOTE, 'Context': 'no contents here'},
{'Type': EntryType.NOTE, 'Contents': {'entry': '2'}},
]
demisto_execute_mock = mocker.patch.object(demisto, 'executeCommand',
return_value=entries)
status, res = execute_command('command', {'arg1': 'value'}, fail_on_error=False)
assert demisto_execute_mock.call_count == 1
assert isinstance(res, list)
assert len(res) == 3
assert status
assert res[0] == {'hello': 'world'}
assert res[1] == {}
assert res[2] == {'entry': '2'}
@staticmethod
def test_raw_results_fail_on_error_false(mocker):
"""
Given:
- A successful command with several entries as output.
- fail_on_error set to False.
When:
- Calling execute_command.
Then:
- Assert that the status of the execution is True for successful run.
- Assert that the entire entries are returned.
"""
from CommonServerPython import execute_command, EntryType
entries = [
{'Type': EntryType.NOTE, 'Contents': {'hello': 'world'}},
{'Type': EntryType.NOTE, 'Context': 'no contents here'},
'text',
1337,
]
demisto_execute_mock = mocker.patch.object(demisto, 'executeCommand',
return_value=entries)
status, res = execute_command('command', {'arg1': 'value'}, extract_contents=False, fail_on_error=False)
assert demisto_execute_mock.call_count == 1
assert isinstance(res, list)
assert len(res) == 4
assert status
assert res[0] == {'Type': EntryType.NOTE, 'Contents': {'hello': 'world'}}
assert res[1] == {'Type': EntryType.NOTE, 'Context': 'no contents here'}
assert res[2] == 'text'
assert res[3] == 1337
@staticmethod
def test_failure_fail_on_error_false(mocker):
"""
Given:
- A command that fails.
- fail_on_error set to False.
When:
- Calling execute_command.
Then:
- Assert that the status of the execution is False for failed run.
- Assert that the original errors are returned as a value, and not to the war-room.
"""
from CommonServerPython import execute_command, EntryType
error_entries = [
{'Type': EntryType.ERROR, 'Contents': 'error number 1'},
{'Type': EntryType.NOTE, 'Contents': 'not an error'},
{'Type': EntryType.ERROR, 'Contents': 'error number 2'},
]
demisto_execute_mock = mocker.patch.object(demisto, 'executeCommand',
return_value=error_entries)
demisto_results_mock = mocker.patch.object(demisto, 'results')
status, error_text = execute_command('bad', {'arg1': 'value'}, fail_on_error=False)
assert demisto_execute_mock.call_count == 1
assert demisto_results_mock.call_count == 0
assert not status
assert 'error number 1' in error_text
assert 'error number 2' in error_text
assert 'not an error' not in error_text
class TestExecuteCommandsMultipleResults:
@staticmethod
def test_sanity(mocker):
"""
Given:
- A successful command with a single entry as output.
When:
- Calling execute_commands.
Then:
- Assert that only the Contents value is returned.
"""
from CommonServerPython import CommandRunner, EntryType
demisto_execute_mock = mocker.patch.object(demisto, 'executeCommand',
return_value=[{'Type': EntryType.NOTE,
'ModuleName': 'module',
'Brand': 'brand',
'Contents': {'hello': 'world'}}])
command_executer = CommandRunner.Command(commands='command', args_lst={'arg1': 'value'})
results, errors = CommandRunner.execute_commands(command_executer)
execute_command_args = demisto_execute_mock.call_args_list[0][0]
assert demisto_execute_mock.call_count == 1
assert execute_command_args[0] == 'command'
assert execute_command_args[1] == {'arg1': 'value'}
assert isinstance(results, list)
assert isinstance(errors, list)
assert len(results) == 1
assert results[0].brand == 'brand'
assert results[0].instance == 'module'
assert results[0].result == {'hello': 'world'}
assert not errors
@staticmethod
def test_multiple_results(mocker):
"""
Given:
- A successful command with several entries as output.
When:
- Calling execute_commands.
Then:
- Assert that the "Contents" values of all entries are returned.
"""
from CommonServerPython import CommandRunner, EntryType
entries = [
{'Type': EntryType.NOTE, 'Contents': {'hello': 'world'}},
{'Type': EntryType.NOTE, 'Context': 'no contents here'},
{'Type': EntryType.NOTE, 'Contents': {'entry': '2'}},
{'Type': EntryType.NOTE, 'Context': 'Content is `None`', 'Contents': None}
]
demisto_execute_mock = mocker.patch.object(demisto, 'executeCommand',
return_value=entries)
command_executer = CommandRunner.Command(commands='command', args_lst={'arg1': 'value'})
results, errors = CommandRunner.execute_commands(command_executer)
assert demisto_execute_mock.call_count == 1
assert isinstance(results, list)
assert isinstance(errors, list)
assert not errors
assert len(results) == 4
assert results[0].result == {'hello': 'world'}
assert results[1].result == {}
assert results[2].result == {'entry': '2'}
assert results[3].result == {}
@staticmethod
def test_raw_results(mocker):
"""
Given:
- A successful command with several entries as output.
When:
- Calling execute_commands.
Then:
- Assert that the entire entries are returned.
"""
from CommonServerPython import EntryType, CommandRunner
entries = [
{'Type': EntryType.NOTE, 'Contents': {'hello': 'world'}},
{'Type': EntryType.NOTE, 'Context': 'no contents here'},
'text',
1337,
]
demisto_execute_mock = mocker.patch.object(demisto, 'executeCommand',
return_value=entries)
command_executer = CommandRunner.Command(commands='command', args_lst={'arg1': 'value'})
results, errors = CommandRunner.execute_commands(command_executer, extract_contents=False)
assert demisto_execute_mock.call_count == 1
assert isinstance(results, list)
assert isinstance(errors, list)
assert not errors
assert len(results) == 4
assert results[0].result == {'Type': EntryType.NOTE, 'Contents': {'hello': 'world'}}
assert results[1].result == {'Type': EntryType.NOTE, 'Context': 'no contents here'}
assert results[2].result == 'text'
assert results[3].result == 1337
assert results[2].brand == results[2].instance == results[3].brand == results[3].instance == 'Unknown'
@staticmethod
def test_with_errors(mocker):
"""
Given:
- A command that sometimes fails and sometimes not.
When:
- Calling execute_command.
Then:
- Assert that the results list and the errors list returned as should've been
"""
from CommonServerPython import CommandRunner, EntryType
entries = [
{'Type': EntryType.ERROR, 'Contents': 'error number 1'},
{'Type': EntryType.NOTE, 'Contents': 'not an error'},
{'Type': EntryType.ERROR, 'Contents': 'error number 2'}
]
def execute_command_mock(command, args):
if command == 'unsupported':
raise ValueError("Command is not supported")
return entries
demisto_execute_mock = mocker.patch.object(demisto, 'executeCommand',
side_effect=execute_command_mock)
command_executer = CommandRunner.Command(commands=['command', 'unsupported'],
args_lst=[{'arg1': 'value'}, {}])
results, errors = CommandRunner.execute_commands(command_executer)
# validate that errors were found and the unsupported is ignored
assert demisto_execute_mock.call_count == 2
assert isinstance(results, list)
assert isinstance(errors, list)
assert len(results) == 1
assert len(errors) == 2
assert errors[0].result == 'error number 1'
assert errors[1].result == 'error number 2'
assert results[0].result == 'not an error'
@staticmethod
def get_result_for_multiple_commands_helper(command, args):
from CommonServerPython import EntryType
return [{'Type': EntryType.NOTE,
'ModuleName': list(args.keys())[0],
'Brand': command,
'Contents': {'hello': list(args.values())[0]}},
]
@staticmethod
def test_multiple_commands(mocker):
"""
Given:
- List of commands and list of args.
When:
- Calling execute_commands with multiple commands to get multiple results.
Then:
- Assert that the results list and the errors list returned as should've been
"""
from CommonServerPython import CommandRunner
demisto_execute_mock = mocker.patch.object(demisto,
'executeCommand',
side_effect=TestExecuteCommandsMultipleResults.get_result_for_multiple_commands_helper)
command_executer = CommandRunner.Command(commands=['command1', 'command2'],
args_lst=[{'arg1': 'value1'},
{'arg2': 'value2'}])
results, errors = CommandRunner.execute_commands(command_executer)
assert demisto_execute_mock.call_count == 2
assert isinstance(results, list)
assert isinstance(errors, list)
assert len(results) == 2
assert results[0].brand == 'command1'
assert results[0].instance == 'arg1'
assert results[0].result == {'hello': 'value1'}
assert results[1].brand == 'command2'
assert results[1].instance == 'arg2'
assert results[1].result == {'hello': 'value2'}
assert not errors
@staticmethod
def test_invalid_args():
"""
Given:
- List of commands and list of args which is invalid
When:
- Calling execute_commands.
Then:
- Assert that error is given.
"""
from CommonServerPython import CommandRunner
with pytest.raises(DemistoException):
CommandRunner.execute_commands(
CommandRunner.Command(commands=['command'],
args_lst=[{'arg': 'val'}, {'arg': 'val'}]))
with pytest.raises(DemistoException):
CommandRunner.execute_commands(
CommandRunner.Command(commands=['command', 'command1'],
args_lst=[{'arg': 'val'}]))
@staticmethod
def test_using_brand_instance(mocker):
"""
Given:
- Provide instance and brand to command wrapper
When:
- Calling execute_commands
Then:
- Assert that the `demisto.executeCommand` runned with `using` and `using-brand`.
"""
from CommonServerPython import CommandRunner
executer_with_brand = CommandRunner.Command(brand='my brand',
instance='my instance',
commands='command',
args_lst={'arg': 'val'})
demisto_execute_mock = mocker.patch.object(demisto, 'executeCommand')
CommandRunner.execute_commands(executer_with_brand)
assert demisto_execute_mock.called
args_for_execute_command = demisto_execute_mock.call_args_list[0][0][1]
assert 'using-brand' in args_for_execute_command
assert 'using' in args_for_execute_command
assert args_for_execute_command['using-brand'] == 'my brand'
assert args_for_execute_command['using'] == 'my instance'
class TestGetResultsWrapper:
NUM_EXECUTE_COMMAND_CALLED = 0
@staticmethod
def execute_command_mock(command_executer, extract_contents):
from CommonServerPython import CommandRunner
TestGetResultsWrapper.NUM_EXECUTE_COMMAND_CALLED += 1
results, errors = [], []
for command, args in zip(command_executer.commands, command_executer.args_lst):
result_wrapper = CommandRunner.Result(command=command,
args=args,
brand='my-brand{}'.format(
TestGetResultsWrapper.NUM_EXECUTE_COMMAND_CALLED),
instance='instance',
result='Command did not succeeded' if command == 'error-command' else {
'Contents': 'Good',
'HumanReadable': 'Good'})
if command == 'error-command':
errors.append(result_wrapper)
elif command != 'unsupported-command':
results.append(result_wrapper)
return results, errors
@staticmethod
def test_get_wrapper_results(mocker):
"""
Given:
- List of CommandWrappers.
When:
- Calling get_wrapper_results to give generic results
Then:
- Assert that the "good" results are returned, and the summary includes the errors.
- Assert that the unsupported command is ignored.
"""
from CommonServerPython import CommandRunner, CommandResults
command_wrappers = [CommandRunner.Command(brand='my-brand1', commands='my-command', args_lst={'arg': 'val'}),
CommandRunner.Command(brand='my-brand2', commands=['command1', 'command2'],
args_lst=[{'arg1': 'val1'}, {'arg2': 'val2'}]),
CommandRunner.Command(brand='my-brand3', commands='error-command',
args_lst={'bad_arg': 'bad_val'}),
CommandRunner.Command(brand='brand-no-exist', commands='unsupported-command',
args_lst={'arg': 'val'})]
mocker.patch.object(CommandRunner, 'execute_commands',
side_effect=TestGetResultsWrapper.execute_command_mock)
results = CommandRunner.run_commands_with_summary(command_wrappers)
assert len(results) == 4 # 1 error (brand3)
assert results[:-1] == [{'Contents': 'Good', 'HumanReadable': '***my-brand1 (instance)***\nGood'},
{'Contents': 'Good', 'HumanReadable': '***my-brand2 (instance)***\nGood'},
{'Contents': 'Good', 'HumanReadable': '***my-brand2 (instance)***\nGood'}]
assert isinstance(results[-1], CommandResults)
if IS_PY3:
md_summary = """### Results Summary
|Instance|Command|Result|Comment|
|---|---|---|---|
| ***my-brand1***: instance | ***command***: my-command
**args**:
***arg***: val | Success | Good |
| ***my-brand2***: instance | ***command***: command1
**args**:
***arg1***: val1 | Success | Good |
| ***my-brand2***: instance | ***command***: command2
**args**:
***arg2***: val2 | Success | Good |
| ***my-brand3***: instance | ***command***: error-command
**args**:
***bad_arg***: bad_val | Error | Command did not succeeded |
"""
else:
md_summary = u"""### Results Summary
|Instance|Command|Result|Comment|
|---|---|---|---|
| ***my-brand1***: instance | **args**:
***arg***: val
***command***: my-command | Success | Good |
| ***my-brand2***: instance | **args**:
***arg1***: val1
***command***: command1 | Success | Good |
| ***my-brand2***: instance | **args**:
***arg2***: val2
***command***: command2 | Success | Good |
| ***my-brand3***: instance | **args**:
***bad_arg***: bad_val
***command***: error-command | Error | Command did not succeeded |
"""
assert results[-1].readable_output == md_summary
@staticmethod
def test_get_wrapper_results_error(mocker):
"""
Given:
- List of CommandWrappers, which all of them returns errors or ignored
When:
- Calling get_wrapper_results to give generic results
Then:
- Assert that error returned.
"""
from CommonServerPython import CommandRunner
command_wrappers = [CommandRunner.Command(brand='my-brand1',
commands='error-command',
args_lst={'arg': 'val'}),
CommandRunner.Command(brand='my-brand2',
commands='error-command',
args_lst={'bad_arg': 'bad_val'}),
CommandRunner.Command(brand='brand-no-exist',
commands='unsupported-command',
args_lst={'arg': 'val'}),
]
mocker.patch.object(CommandRunner, 'execute_commands',
side_effect=TestGetResultsWrapper.execute_command_mock)
with pytest.raises(DemistoException) as e:
CommandRunner.run_commands_with_summary(command_wrappers)
assert 'Command did not succeeded' in e.value
assert 'Script failed. The following errors were encountered:' in e.value
def test_arg_to_int__valid_numbers():
"""
Given
valid numbers
When
converting them to int
Then
ensure proper int returned
"""
from CommonServerPython import arg_to_number
result = arg_to_number(
arg='5',
arg_name='foo')
assert result == 5
result = arg_to_number(
arg='2.0',
arg_name='foo')
assert result == 2
result = arg_to_number(
arg=3,
arg_name='foo')
assert result == 3
result = arg_to_number(
arg=4,
arg_name='foo',
required=True)
assert result == 4
result = arg_to_number(
arg=5,
required=True)
assert result == 5
def test_arg_to_int__invalid_numbers():
"""
Given
invalid numbers
When
converting them to int
Then
raise ValueError
"""
from CommonServerPython import arg_to_number
try:
arg_to_number(
arg='aa',
arg_name='foo')
assert False
except ValueError as e:
assert 'Invalid number' in str(e)
def test_arg_to_int_required():
"""
Given
argument foo which with value None
When
converting the arg to number via required flag as True
Then
ensure ValueError raised
"""
from CommonServerPython import arg_to_number
# required set to false
result = arg_to_number(
arg=None,
arg_name='foo',
required=False)
assert result is None
try:
arg_to_number(
arg=None,
arg_name='foo',
required=True)
assert False
except ValueError as e:
assert 'Missing' in str(e)
try:
arg_to_number(
arg='',
arg_name='foo',
required=True)
assert False
except ValueError as e:
assert 'Missing' in str(e)
try:
arg_to_number(arg='goo')
assert False
except ValueError as e:
assert '"goo" is not a valid number' in str(e)
def test_arg_to_timestamp_valid_inputs():
"""
Given
valid dates provided
When
converting dates into timestamp
Then
ensure returned int which represents timestamp in milliseconds
"""
if sys.version_info.major == 2:
# skip for python 2 - date
assert True
return
from CommonServerPython import arg_to_datetime
from datetime import datetime, timezone
# hard coded date
result = arg_to_datetime(
arg='2020-11-10T21:43:43Z',
arg_name='foo'
)
assert result == datetime(2020, 11, 10, 21, 43, 43, tzinfo=timezone.utc)
# relative dates also work
result = arg_to_datetime(
arg='2 hours ago',
arg_name='foo'
)
assert result > datetime(2020, 11, 10, 21, 43, 43)
# relative dates also work
result = arg_to_datetime(
arg=1581982463,
arg_name='foo'
)
assert int(result.timestamp()) == 1581982463
result = arg_to_datetime(
arg='2 hours ago'
)
assert result > datetime(2020, 11, 10, 21, 43, 43)
def test_arg_to_timestamp_invalid_inputs():
"""
Given
invalid date like 'aaaa' or '2010-32-01'
When
when converting date to timestamp
Then
ensure ValueError is raised
"""
from CommonServerPython import arg_to_datetime
if sys.version_info.major == 2:
# skip for python 2 - date
assert True
return
try:
arg_to_datetime(
arg=None,
arg_name='foo',
required=True)
assert False
except ValueError as e:
assert 'Missing' in str(e)
try:
arg_to_datetime(
arg='aaaa',
arg_name='foo')
assert False
except ValueError as e:
assert 'Invalid date' in str(e)
try:
arg_to_datetime(
arg='2010-32-01',
arg_name='foo')
assert False
except ValueError as e:
assert 'Invalid date' in str(e)
try:
arg_to_datetime(
arg='2010-32-01')
assert False
except ValueError as e:
assert '"2010-32-01" is not a valid date' in str(e)
def test_warnings_handler(mocker):
mocker.patch.object(demisto, 'info')
# need to initialize WarningsHandler as pytest over-rides the handler
with pytest.warns(RuntimeWarning) as r:
warnings.warn("without handler", RuntimeWarning)
handler = WarningsHandler() # noqa
warnings.warn("This is a test", RuntimeWarning)
assert len(r) == 1
assert str(r[0].message) == "without handler"
# call_args is tuple (args list, kwargs). we only need the args
msg = demisto.info.call_args[0][0]
assert 'This is a test' in msg
assert 'python warning' in msg
def test_get_schedule_metadata():
"""
Given
- case 1: no parent entry
- case 2: parent entry with schedule metadata
- case 3: parent entry without schedule metadata
When
querying the schedule metadata
Then
ensure scheduled_metadata is returned correctly
- case 1: no data (empty dict)
- case 2: schedule metadata with all details
- case 3: empty schedule metadata (dict with polling: false)
"""
from CommonServerPython import get_schedule_metadata
# case 1
context = {'ParentEntry': None}
actual_scheduled_metadata = get_schedule_metadata(context=context)
assert actual_scheduled_metadata == {}
# case 2
parent_entry = {
'polling': True,
'pollingCommand': 'foo',
'pollingArgs': {'name': 'foo'},
'timesRan': 5,
'startDate': '2021-04-28T14:20:56.03728+03:00',
'endingDate': '2021-04-28T14:25:35.976244+03:00'
}
context = {
'ParentEntry': parent_entry
}
actual_scheduled_metadata = get_schedule_metadata(context=context)
assert actual_scheduled_metadata.get('is_polling') is True
assert actual_scheduled_metadata.get('polling_command') == parent_entry.get('pollingCommand')
assert actual_scheduled_metadata.get('polling_args') == parent_entry.get('pollingArgs')
assert actual_scheduled_metadata.get('times_ran') == (parent_entry.get('timesRan') + 1)
assert actual_scheduled_metadata.get('startDate') == parent_entry.get('start_date')
assert actual_scheduled_metadata.get('startDate') == parent_entry.get('start_date')
# case 3
parent_entry = {
'polling': False
}
context = {
'ParentEntry': parent_entry
}
actual_scheduled_metadata = get_schedule_metadata(context=context)
assert actual_scheduled_metadata == {'is_polling': False, 'times_ran': 1}
class TestCommonTypes:
def test_create_ip(self):
"""
Given:
- A single IP indicator entry
When
- Creating a Common.IP object
Then
- The context created matches the data entry
"""
from CommonServerPython import CommandResults, Common, DBotScoreType
dbot_score = Common.DBotScore(
indicator='8.8.8.8',
integration_name='Test',
indicator_type=DBotScoreType.IP,
score=Common.DBotScore.GOOD
)
ip = Common.IP(
ip='8.8.8.8',
dbot_score=dbot_score,
asn='some asn',
hostname='test.com',
geo_country='geo_country',
geo_description='geo_description',
geo_latitude='geo_latitude',
geo_longitude='geo_longitude',
positive_engines=5,
detection_engines=10,
as_owner=None,
region='region',
port='port',
internal=None,
updated_date=None,
registrar_abuse_name='Mr Registrar',
registrar_abuse_address='Registrar Address',
registrar_abuse_country='Registrar Country',
registrar_abuse_network='Registrar Network',
registrar_abuse_phone=None,
registrar_abuse_email='registrar@test.com',
campaign='campaign',
traffic_light_protocol='traffic_light_protocol',
threat_types=[Common.ThreatTypes(threat_category='threat_category',
threat_category_confidence='threat_category_confidence')],
community_notes=[Common.CommunityNotes(note='note', timestamp='2019-01-01T00:00:00')],
publications=[Common.Publications(title='title', source='source', timestamp='2019-01-01T00:00:00',
link='link')],
organization_name='Some Organization',
organization_type='Organization type',
feed_related_indicators=None,
tags=['tag1', 'tag2'],
malware_family=['malware_family1', 'malware_family2'],
relationships=None,
blocked=False,
description='description test',
stix_id='stix_id',
whois_records=[Common.WhoisRecord('test_key', 'test_value', 'test_date')],
)
results = CommandResults(
outputs_key_field=None,
outputs_prefix=None,
outputs=None,
indicators=[ip]
)
assert results.to_context() == {
'Type': 1,
'ContentsFormat': 'json',
'Contents': None,
'HumanReadable': None,
'EntryContext': {
'IP(val.Address && val.Address == obj.Address)': [
{'Address': '8.8.8.8',
'ASN': 'some asn',
'Region': 'region',
'Port': 'port',
'STIXID': 'stix_id',
'Registrar': {
'Abuse': {
'Name': 'Mr Registrar',
'Address': 'Registrar Address',
'Country': 'Registrar Country',
'Network': 'Registrar Network',
'Email': 'registrar@test.com'
}
},
'Campaign': 'campaign',
'Description': 'description test',
'TrafficLightProtocol': 'traffic_light_protocol',
'CommunityNotes': [{'note': 'note', 'timestamp': '2019-01-01T00:00:00'}],
'Publications': [
{
'source': 'source',
'title': 'title',
'link': 'link',
'timestamp': '2019-01-01T00:00:00'
}
],
'ThreatTypes': [
{'threatcategory': 'threat_category',
'threatcategoryconfidence': 'threat_category_confidence'}
],
'WhoisRecords': [{'key': 'test_key', 'value': 'test_value', 'date': 'test_date'}],
'Hostname': 'test.com',
'Geo': {
'Location': 'geo_latitude:geo_longitude',
'Country': 'geo_country',
'Description': 'geo_description'
},
'Organization': {
'Name': 'Some Organization',
'Type': 'Organization type'
},
'DetectionEngines': 10,
'PositiveDetections': 5,
'Tags': ['tag1', 'tag2'],
'MalwareFamily': ['malware_family1', 'malware_family2']
}
],
'DBotScore(val.Indicator && val.Indicator == obj.Indicator && '
'val.Vendor == obj.Vendor && val.Type == obj.Type)': [
{'Indicator': '8.8.8.8',
'Type': 'ip',
'Vendor': 'Test',
'Score': 1
}
]
},
'IndicatorTimeline': [],
'IgnoreAutoExtract': False,
'Note': False,
'Relationships': [],
}
def test_create_large_ip(self):
"""
Given:
- A lareg single IP indicator entry
When
- Creating a Common.IP object
Then
- The context created matches the data entry in the small size
"""
from CommonServerPython import CommandResults, Common, DBotScoreType
dbot_score = Common.DBotScore(
indicator='8.8.8.8',
integration_name='Test',
indicator_type=DBotScoreType.IP,
score=Common.DBotScore.GOOD
)
ip = Common.IP(
ip='8.8.8.8',
dbot_score=dbot_score,
asn='some asn',
hostname='test.com',
geo_country='geo_country',
geo_description='geo_description',
geo_latitude='geo_latitude',
geo_longitude='geo_longitude',
positive_engines=5,
detection_engines=10,
as_owner=None,
region='region',
port='port',
internal=None,
updated_date=None,
registrar_abuse_name='Mr Registrar',
registrar_abuse_address='Registrar Address',
registrar_abuse_country='Registrar Country',
registrar_abuse_network='Registrar Network',
registrar_abuse_phone=None,
registrar_abuse_email='registrar@test.com',
campaign='campaign',
traffic_light_protocol='traffic_light_protocol',
threat_types=[Common.ThreatTypes(threat_category='threat_category',
threat_category_confidence='threat_category_confidence')],
community_notes=[Common.CommunityNotes(note='note', timestamp='2019-01-01T00:00:00')],
publications=[Common.Publications(title='title', source='source', timestamp='2019-01-01T00:00:00',
link='link')],
organization_name='Some Organization',
organization_type='Organization type',
feed_related_indicators=None,
tags=['tag1', 'tag2'],
malware_family=['malware_family1', 'malware_family2'],
relationships=None,
blocked=False,
description="a"*1024*3072,
stix_id='stix_id',
whois_records=[Common.WhoisRecord('test_key', 'test_value', 'test_date')],
)
results = CommandResults(
outputs_key_field=None,
outputs_prefix=None,
outputs=None,
indicators=[ip]
)
assert results.to_context() == {
'Type': 1,
'ContentsFormat': 'json',
'Contents': None,
'HumanReadable': 'Note! some of the context data was not included because it went over the 3072KB limit.',
'EntryContext': {
'IP(val.Address && val.Address == obj.Address)': [
{'Address': '8.8.8.8'}
],
'DBotScore(val.Indicator && val.Indicator == obj.Indicator && '
'val.Vendor == obj.Vendor && val.Type == obj.Type)': [
{'Indicator': '8.8.8.8',
'Type': 'ip',
'Vendor': 'Test',
'Score': 1
}
]
},
'IndicatorTimeline': [],
'IgnoreAutoExtract': False,
'Note': False,
'Relationships': [],
}
def test_create_domain(self):
"""
Given:
- A single Domain indicator entry
When
- Creating a Common.Domain object
Then
- The context created matches the data entry
"""
from CommonServerPython import CommandResults, Common, DBotScoreType
dbot_score = Common.DBotScore(
indicator='somedomain.com',
integration_name='Test',
indicator_type=DBotScoreType.DOMAIN,
score=Common.DBotScore.GOOD
)
domain = Common.Domain(
domain='somedomain.com',
dbot_score=dbot_score,
dns='dns.somedomain',
detection_engines=10,
positive_detections=5,
first_seen_by_source='2024-10-06T09:50:50.555Z',
organization='Some Organization',
admin_phone='18000000',
admin_email='admin@test.com',
registrant_name='Mr Registrant',
registrar_name='Mr Registrar',
registrar_abuse_email='registrar@test.com',
creation_date='2019-01-01T00:00:00',
updated_date='2019-01-02T00:00:00',
expiration_date=None,
domain_status='ACTIVE',
name_servers=[
'PNS31.CLOUDNS.NET',
'PNS32.CLOUDNS.NET'
],
sub_domains=[
'sub-domain1.somedomain.com',
'sub-domain2.somedomain.com',
'sub-domain3.somedomain.com'
],
tags=['tag1', 'tag2'],
malware_family=['malware_family1', 'malware_family2'],
feed_related_indicators=[Common.FeedRelatedIndicators(
value='8.8.8.8',
indicator_type="IP",
description='test'
)],
domain_idn_name='domain_idn_name',
port='port',
internal="False",
category='category',
campaign='campaign',
traffic_light_protocol='traffic_light_protocol',
threat_types=[Common.ThreatTypes(threat_category='threat_category',
threat_category_confidence='threat_category_confidence')],
community_notes=[Common.CommunityNotes(note='note', timestamp='2019-01-01T00:00:00')],
publications=[Common.Publications(title='title', source='source', timestamp='2019-01-01T00:00:00',
link='link')],
geo_location='geo_location',
geo_country='geo_country',
geo_description='geo_description',
tech_country='tech_country',
tech_name='tech_name',
tech_organization='tech_organization',
tech_email='tech_email',
billing='billing',
whois_records=[Common.WhoisRecord('test_key', 'test_value', 'test_date')],
description='test_description',
stix_id='test_stix_id',
blocked=True,
certificates=[Common.Certificates('test_issuedto', 'test_issuedby', 'test_validfrom', 'test_validto')],
dns_records=[Common.DNSRecord('test_type', 'test_ttl', 'test_data')]
)
results = CommandResults(
outputs_key_field=None,
outputs_prefix=None,
outputs=None,
indicators=[domain]
)
assert results.to_context() == {
'Type': 1,
'ContentsFormat': 'json',
'Contents': None,
'HumanReadable': None,
'EntryContext': {
'Domain(val.Name && val.Name == obj.Name)': [
{
'Name': 'somedomain.com',
'DNS': 'dns.somedomain',
'DetectionEngines': 10,
'PositiveDetections': 5,
'Registrar': {'Name': 'Mr Registrar', 'AbuseEmail': 'registrar@test.com', 'AbusePhone': None},
'Registrant': {'Name': 'Mr Registrant', 'Email': None, 'Phone': None, 'Country': None},
'Admin': {'Name': None, 'Email': 'admin@test.com', 'Phone': '18000000', 'Country': None},
'FirstSeenBySource': '2024-10-06T09:50:50.555Z',
'Organization': 'Some Organization',
'Subdomains': ['sub-domain1.somedomain.com', 'sub-domain2.somedomain.com',
'sub-domain3.somedomain.com'], 'DomainStatus': 'ACTIVE',
'CreationDate': '2019-01-01T00:00:00',
'UpdatedDate': '2019-01-02T00:00:00',
'NameServers': ['PNS31.CLOUDNS.NET', 'PNS32.CLOUDNS.NET'],
'Tags': ['tag1', 'tag2'],
'FeedRelatedIndicators': [{'value': '8.8.8.8', 'type': 'IP', 'description': 'test'}],
'WhoisRecords': [{'key': 'test_key', 'value': 'test_value', 'date': 'test_date'}],
'MalwareFamily': ['malware_family1', 'malware_family2'], 'DomainIDNName': 'domain_idn_name',
'Port': 'port',
'Internal': 'False',
'Category': 'category',
'Campaign': 'campaign',
'TrafficLightProtocol': 'traffic_light_protocol',
'ThreatTypes': [{'threatcategory': 'threat_category',
'threatcategoryconfidence': 'threat_category_confidence'}],
'CommunityNotes': [{'note': 'note', 'timestamp': '2019-01-01T00:00:00'}],
'Publications': [{'source': 'source', 'title': 'title', 'link': 'link',
'timestamp': '2019-01-01T00:00:00'}],
'Geo': {'Location': 'geo_location', 'Country': 'geo_country', 'Description': 'geo_description'},
'Tech': {'Country': 'tech_country', 'Name': 'tech_name', 'Organization': 'tech_organization',
'Email': 'tech_email'},
'Billing': 'billing',
'WHOIS': {
'Registrar': {'Name': 'Mr Registrar', 'AbuseEmail': 'registrar@test.com',
'AbusePhone': None},
'Registrant': {'Name': 'Mr Registrant', 'Email': None, 'Phone': None, 'Country': None},
'Admin': {'Name': None, 'Email': 'admin@test.com', 'Phone': '18000000', 'Country': None},
'DomainStatus': 'ACTIVE',
'CreationDate': '2019-01-01T00:00:00',
'UpdatedDate': '2019-01-02T00:00:00',
'NameServers': ['PNS31.CLOUDNS.NET', 'PNS32.CLOUDNS.NET']
},
'DNSRecords': [{'type': 'test_type', 'ttl': 'test_ttl', 'data': 'test_data'}],
'STIXID': 'test_stix_id',
'Description': 'test_description',
'Blocked': True,
'Certificates': [{'issuedto': 'test_issuedto', 'issuedby': 'test_issuedby',
'validfrom': 'test_validfrom', 'validto': 'test_validto'}]
}
],
'DBotScore(val.Indicator && val.Indicator == obj.Indicator &&'
' val.Vendor == obj.Vendor && val.Type == obj.Type)': [
{'Indicator': 'somedomain.com', 'Type': 'domain', 'Vendor': 'Test', 'Score': 1}
]
},
'IndicatorTimeline': [],
'IgnoreAutoExtract': False,
'Note': False,
'Relationships': [],
}
def test_create_large_domain(self):
"""
Given:
- A large single Domain indicator entry
When
- Creating a Common.Domain object
Then
- The context created matches the data entry in the small size
"""
from CommonServerPython import CommandResults, Common, DBotScoreType
dbot_score = Common.DBotScore(
indicator='somedomain.com',
integration_name='Test',
indicator_type=DBotScoreType.DOMAIN,
score=Common.DBotScore.GOOD
)
domain = Common.Domain(
domain='somedomain.com',
dbot_score=dbot_score,
dns='dns.somedomain',
detection_engines=10,
positive_detections=5,
first_seen_by_source='2024-10-06T09:50:50.555Z',
organization='Some Organization',
admin_phone='18000000',
admin_email='admin@test.com',
registrant_name='Mr Registrant',
registrar_name='Mr Registrar',
registrar_abuse_email='registrar@test.com',
creation_date='2019-01-01T00:00:00',
updated_date='2019-01-02T00:00:00',
expiration_date=None,
domain_status='ACTIVE',
name_servers=[
'PNS31.CLOUDNS.NET',
'PNS32.CLOUDNS.NET'
],
sub_domains=[
'sub-domain1.somedomain.com',
'sub-domain2.somedomain.com',
'sub-domain3.somedomain.com'
],
tags=['tag1', 'tag2'],
malware_family=['malware_family1', 'malware_family2'],
feed_related_indicators=[Common.FeedRelatedIndicators(
value='8.8.8.8',
indicator_type="IP",
description='test'
)],
domain_idn_name='domain_idn_name',
port='port',
internal="False",
category='category',
campaign='campaign',
traffic_light_protocol='traffic_light_protocol',
threat_types=[Common.ThreatTypes(threat_category='threat_category',
threat_category_confidence='threat_category_confidence')],
community_notes=[Common.CommunityNotes(note='note', timestamp='2019-01-01T00:00:00')],
publications=[Common.Publications(title='title', source='source', timestamp='2019-01-01T00:00:00',
link='link')],
geo_location='geo_location',
geo_country='geo_country',
geo_description='geo_description',
tech_country='tech_country',
tech_name='tech_name',
tech_organization='tech_organization',
tech_email='tech_email',
billing='billing',
whois_records=[Common.WhoisRecord('test_key', 'test_value', 'test_date')],
description='a'*1024*3072,
stix_id='test_stix_id',
blocked=True,
certificates=[Common.Certificates('test_issuedto', 'test_issuedby', 'test_validfrom', 'test_validto')],
dns_records=[Common.DNSRecord('test_type', 'test_ttl', 'test_data')]
)
results = CommandResults(
outputs_key_field=None,
outputs_prefix=None,
outputs=None,
indicators=[domain]
)
assert results.to_context() == {
'Type': 1,
'ContentsFormat': 'json',
'Contents': None,
'HumanReadable': 'Note! some of the context data was not included because it went over the 3072KB limit.',
'EntryContext': {
'Domain(val.Name && val.Name == obj.Name)': [
{
'Name': 'somedomain.com',
}
],
'DBotScore(val.Indicator && val.Indicator == obj.Indicator &&'
' val.Vendor == obj.Vendor && val.Type == obj.Type)': [
{'Indicator': 'somedomain.com', 'Type': 'domain', 'Vendor': 'Test', 'Score': 1}
]
},
'IndicatorTimeline': [],
'IgnoreAutoExtract': False,
'Note': False,
'Relationships': [],
}
def test_create_url(self):
"""
Given:
- A single URL indicator entry
When
- Creating a Common.URL object
Then
- The context created matches the data entry
"""
from CommonServerPython import CommandResults, Common, DBotScoreType
dbot_score = Common.DBotScore(
indicator='https://somedomain.com',
integration_name='Test',
indicator_type=DBotScoreType.URL,
score=Common.DBotScore.GOOD
)
url = Common.URL(
url='https://somedomain.com',
dbot_score=dbot_score,
positive_detections=5,
detection_engines=10,
category='test_category',
feed_related_indicators=None,
tags=['tag1', 'tag2'],
malware_family=['malware_family1', 'malware_family2'],
port='port',
internal=None,
campaign='test_campaign',
traffic_light_protocol='test_traffic_light_protocol',
threat_types=[Common.ThreatTypes(threat_category='threat_category',
threat_category_confidence='threat_category_confidence')],
asn='test_asn',
as_owner='test_as_owner',
geo_country='test_geo_country',
organization='test_organization',
community_notes=[Common.CommunityNotes(note='note', timestamp='2019-01-01T00:00:00')],
publications=[Common.Publications(title='title', source='source', timestamp='2019-01-01T00:00:00',
link='link')],
relationships=None,
blocked=True,
certificates=None,
description='description test',
stix_id='stix_id',
organization_first_seen='2024-11-04T14:48:23.456Z',
)
results = CommandResults(
outputs_key_field=None,
outputs_prefix=None,
outputs=None,
indicators=[url]
)
assert results.to_context() == {
'Type': 1,
'ContentsFormat': 'json',
'Contents': None,
'HumanReadable': None,
'EntryContext': {
'URL(val.Data && val.Data == obj.Data)': [
{
'Data': 'https://somedomain.com',
'Blocked': True,
'Description': 'description test',
'STIXID': 'stix_id',
'DetectionEngines': 10,
'PositiveDetections': 5,
'Category': 'test_category',
'Tags': ['tag1', 'tag2'],
'MalwareFamily': ['malware_family1', 'malware_family2'],
'Port': 'port',
'Campaign': 'test_campaign',
'TrafficLightProtocol': 'test_traffic_light_protocol',
'ThreatTypes': [
{
'threatcategory': 'threat_category',
'threatcategoryconfidence': 'threat_category_confidence'
}
],
'ASN': 'test_asn',
'ASOwner': 'test_as_owner',
'Geo': {'Country': 'test_geo_country'},
'Organization': 'test_organization',
'OrganizationFirstSeen': '2024-11-04T14:48:23.456Z',
'CommunityNotes': [{'note': 'note', 'timestamp': '2019-01-01T00:00:00'}],
'Publications': [
{'source': 'source',
'title': 'title',
'link': 'link',
'timestamp': '2019-01-01T00:00:00'
}
]
}
],
'DBotScore(val.Indicator && val.Indicator == obj.Indicator &&'
' val.Vendor == obj.Vendor && val.Type == obj.Type)': [
{
'Indicator': 'https://somedomain.com',
'Type': 'url',
'Vendor': 'Test',
'Score': 1
}
]
},
'IndicatorTimeline': [],
'IgnoreAutoExtract': False,
'Note': False,
'Relationships': []
}
def test_create_large_url(self):
"""
Given:
- A single large URL indicator entry
When
- Creating a Common.URL object
Then
- The context created matches the data entry in the small size
"""
from CommonServerPython import CommandResults, Common, DBotScoreType
dbot_score = Common.DBotScore(
indicator='https://somedomain.com',
integration_name='Test',
indicator_type=DBotScoreType.URL,
score=Common.DBotScore.GOOD
)
url = Common.URL(
url='https://somedomain.com',
dbot_score=dbot_score,
positive_detections=5,
detection_engines=10,
category='test_category',
feed_related_indicators=None,
tags=['tag1', 'tag2'],
malware_family=['malware_family1', 'malware_family2'],
port='port',
internal=None,
campaign='test_campaign',
traffic_light_protocol='test_traffic_light_protocol',
threat_types=[Common.ThreatTypes(threat_category='threat_category',
threat_category_confidence='threat_category_confidence')],
asn='test_asn',
as_owner='test_as_owner',
geo_country='test_geo_country',
organization='test_organization',
community_notes=[Common.CommunityNotes(note='note', timestamp='2019-01-01T00:00:00')],
publications=[Common.Publications(title='title', source='source', timestamp='2019-01-01T00:00:00',
link='link')],
relationships=None,
blocked=True,
certificates=None,
description='a'*1024*3072,
stix_id='stix_id',
organization_first_seen='2024-11-04T14:48:23.456Z',
)
results = CommandResults(
outputs_key_field=None,
outputs_prefix=None,
outputs=None,
indicators=[url]
)
assert results.to_context() == {
'Type': 1,
'ContentsFormat': 'json',
'Contents': None,
'HumanReadable': 'Note! some of the context data was not included because it went over the 3072KB limit.',
'EntryContext': {
'URL(val.Data && val.Data == obj.Data)': [
{
'Data': 'https://somedomain.com',
}
],
'DBotScore(val.Indicator && val.Indicator == obj.Indicator &&'
' val.Vendor == obj.Vendor && val.Type == obj.Type)': [
{
'Indicator': 'https://somedomain.com',
'Type': 'url',
'Vendor': 'Test',
'Score': 1
}
]
},
'IndicatorTimeline': [],
'IgnoreAutoExtract': False,
'Note': False,
'Relationships': []
}
def test_create_file(self):
"""
Given:
- A single File indicator entry
When
- Creating a Common.File object
Then
- The context created matches the data entry
"""
from CommonServerPython import CommandResults, Common, DBotScoreType
indicator_id = '63347f5d946164a23faca26b78a91e1c'
dbot_score = Common.DBotScore(
indicator=indicator_id,
integration_name='Test',
indicator_type=DBotScoreType.FILE,
score=Common.DBotScore.BAD,
malicious_description='malicious!'
)
file = Common.File(
md5=indicator_id,
sha1='test_sha1',
sha256='test_sha256',
sha512='test_sha512',
ssdeep='test_ssdeep',
imphash='test_imphash',
name='test_name',
entry_id='test_entry_id',
size=1000,
dbot_score=dbot_score,
extension='test_extension',
file_type='test_file_type',
hostname='test_hostname',
path=None,
company=None,
product_name=None,
digital_signature__publisher=None,
signature=None,
actor='test_actor',
tags=['tag1', 'tag2'],
feed_related_indicators=None,
malware_family=['malware_family1', 'malware_family2'],
quarantined=None,
campaign='test_campaign',
associated_file_names=None,
traffic_light_protocol='traffic_light_protocol',
organization='test_organization',
community_notes=[Common.CommunityNotes(note='note', timestamp='2019-01-01T00:00:00')],
publications=[Common.Publications(title='title', source='source', timestamp='2019-01-01T00:00:00',
link='link')],
threat_types=[Common.ThreatTypes(threat_category='threat_category',
threat_category_confidence='threat_category_confidence')],
behaviors=None,
relationships=None,
creation_date='test_creation_date',
description='test_description',
hashes=None,
stix_id='test_stix_id',
organization_prevalence=0,
)
results = CommandResults(
outputs_key_field=None,
outputs_prefix=None,
outputs=None,
indicators=[file]
)
assert results.to_context() == {
'Type': 1,
'ContentsFormat': 'json',
'Contents': None,
'HumanReadable': None,
'EntryContext': {
'File(val.MD5 && val.MD5 == obj.MD5 || val.SHA1 && val.SHA1 == obj.SHA1 || val.SHA256 &&'
' val.SHA256 == obj.SHA256 || val.SHA512 && val.SHA512 == obj.SHA512 || val.CRC32 &&'
' val.CRC32 == obj.CRC32 || val.CTPH && val.CTPH == obj.CTPH || val.SSDeep &&'
' val.SSDeep == obj.SSDeep)': [
{'Hashes': [{'type': 'MD5', 'value': '63347f5d946164a23faca26b78a91e1c'},
{'type': 'SHA1', 'value': 'test_sha1'}, {'type': 'SHA256', 'value': 'test_sha256'},
{'type': 'SHA512', 'value': 'test_sha512'}, {'type': 'SSDeep', 'value': 'test_ssdeep'},
{'type': 'Imphash', 'value': 'test_imphash'}],
'Name': 'test_name',
'EntryID': 'test_entry_id',
'Size': 1000,
'MD5': '63347f5d946164a23faca26b78a91e1c',
'SHA1': 'test_sha1',
'SHA256': 'test_sha256',
'SHA512': 'test_sha512',
'SSDeep': 'test_ssdeep',
'Extension': 'test_extension',
'Type': 'test_file_type',
'Hostname': 'test_hostname',
'Actor': 'test_actor',
'Tags': ['tag1', 'tag2'],
'MalwareFamily': ['malware_family1', 'malware_family2'],
'Campaign': 'test_campaign',
'TrafficLightProtocol': 'traffic_light_protocol',
'CommunityNotes': [{'note': 'note', 'timestamp': '2019-01-01T00:00:00'}], 'Publications': [
{'source': 'source', 'title': 'title', 'link': 'link', 'timestamp': '2019-01-01T00:00:00'}],
'ThreatTypes': [{'threatcategory': 'threat_category',
'threatcategoryconfidence': 'threat_category_confidence'}],
'Imphash': 'test_imphash',
'Organization': 'test_organization',
'OrganizationPrevalence': 0,
'Malicious': {'Vendor': 'Test', 'Description': 'malicious!'}
}
],
'DBotScore(val.Indicator && val.Indicator == obj.Indicator &&'
' val.Vendor == obj.Vendor && val.Type == obj.Type)': [
{'Indicator': '63347f5d946164a23faca26b78a91e1c',
'Type': 'file',
'Vendor': 'Test',
'Score': 3}
]
},
'IndicatorTimeline': [],
'IgnoreAutoExtract': False,
'Note': False,
'Relationships': []
}
def test_create_large_file(self):
"""
Given:
- A single large File indicator entry
When
- Creating a Common.File object
Then
- The context created matches the data entry in the small size
"""
from CommonServerPython import CommandResults, Common, DBotScoreType
indicator_id = '63347f5d946164a23faca26b78a91e1c'
dbot_score = Common.DBotScore(
indicator=indicator_id,
integration_name='Test',
indicator_type=DBotScoreType.FILE,
score=Common.DBotScore.BAD,
malicious_description='malicious!'
)
file = Common.File(
md5=indicator_id,
sha1='test_sha1',
sha256='test_sha256',
sha512='test_sha512',
ssdeep='test_ssdeep',
imphash='test_imphash',
name='test_name'*1024*2073,
entry_id='test_entry_id',
size=1000,
dbot_score=dbot_score,
extension='test_extension',
file_type='test_file_type',
hostname='test_hostname',
path=None,
company=None,
product_name=None,
digital_signature__publisher=None,
signature=None,
actor='test_actor',
tags=['tag1', 'tag2'],
feed_related_indicators=None,
malware_family=['malware_family1', 'malware_family2'],
quarantined=None,
campaign='test_campaign',
associated_file_names=None,
traffic_light_protocol='traffic_light_protocol',
organization='test_organization',
community_notes=[Common.CommunityNotes(note='note', timestamp='2019-01-01T00:00:00')],
publications=[Common.Publications(title='title', source='source', timestamp='2019-01-01T00:00:00',
link='link')],
threat_types=[Common.ThreatTypes(threat_category='threat_category',
threat_category_confidence='threat_category_confidence')],
behaviors=None,
relationships=None,
creation_date='test_creation_date',
description='a',
hashes=None,
stix_id='test_stix_id',
organization_prevalence=0,
)
results = CommandResults(
outputs_key_field=None,
outputs_prefix=None,
outputs=None,
indicators=[file]
)
assert results.to_context() == {
'Type': 1,
'ContentsFormat': 'json',
'Contents': None,
'HumanReadable': 'Note! some of the context data was not included because it went over the 3072KB limit.',
'EntryContext': {
'File(val.MD5 && val.MD5 == obj.MD5 || val.SHA1 && val.SHA1 == obj.SHA1 || val.SHA256 &&'
' val.SHA256 == obj.SHA256 || val.SHA512 && val.SHA512 == obj.SHA512 || val.CRC32 &&'
' val.CRC32 == obj.CRC32 || val.CTPH && val.CTPH == obj.CTPH || val.SSDeep &&'
' val.SSDeep == obj.SSDeep)': [
{
'MD5': '63347f5d946164a23faca26b78a91e1c',
'SHA1': 'test_sha1',
'SHA256': 'test_sha256',
'SHA512': 'test_sha512'
}
],
'DBotScore(val.Indicator && val.Indicator == obj.Indicator &&'
' val.Vendor == obj.Vendor && val.Type == obj.Type)': [
{'Indicator': '63347f5d946164a23faca26b78a91e1c',
'Type': 'file',
'Vendor': 'Test',
'Score': 3}
]
},
'IndicatorTimeline': [],
'IgnoreAutoExtract': False,
'Note': False,
'Relationships': []
}
def test_create_cve(self):
"""
Given:
- A single CVE indicator entry
When
- Creating a Common.CVE object
Then
- The context created matches the data entry
"""
from CommonServerPython import CommandResults, Common
cve = Common.CVE(
id='CVE-2015-1653',
cvss='10.0',
published='2022-04-28T13:16:54+00:00',
modified='2022-04-31T13:16:54+00:00',
description='test_description',
relationships=None,
stix_id='test_stix_id',
cvss_version='test_cvss_version',
cvss_score=10,
cvss_vector='test_cvss_vector',
cvss_table='test_cvss_table',
community_notes=[Common.CommunityNotes(note='note', timestamp='2019-01-01T00:00:00')],
tags=['tag1', 'tag2'],
traffic_light_protocol='traffic_light_protocol'
)
results = CommandResults(
outputs_key_field=None,
outputs_prefix=None,
outputs=None,
indicators=[cve]
)
assert results.to_context() == {
'Type': 1,
'ContentsFormat': 'json',
'Contents': None,
'HumanReadable': None,
'EntryContext': {
'CVE(val.ID && val.ID == obj.ID)': [
{
'ID': 'CVE-2015-1653',
'CVSS': {
'Score': '10.0',
'Version': 'test_cvss_version',
'Vector': 'test_cvss_vector',
'Table': 'test_cvss_table'
},
'Published': '2022-04-28T13:16:54+00:00',
'Modified': '2022-04-31T13:16:54+00:00',
'Description': 'test_description',
'STIXID': 'test_stix_id',
'CommunityNotes': [{'note': 'note', 'timestamp': '2019-01-01T00:00:00'}],
'Tags': ['tag1', 'tag2'],
'TrafficLightProtocol': 'traffic_light_protocol'
}
],
'DBotScore(val.Indicator && val.Indicator == obj.Indicator &&'
' val.Vendor == obj.Vendor && val.Type == obj.Type)': [
{'Indicator': 'CVE-2015-1653',
'Type': 'cve',
'Vendor': None,
'Score': 0
}
]
},
'IndicatorTimeline': [],
'IgnoreAutoExtract': False,
'Note': False,
'Relationships': []
}
def test_create_account(self):
"""
Given:
- A single Account indicator entry
When
- Creating a Common.Account object
Then
- The context created matches the data entry
"""
from CommonServerPython import CommandResults, Common
dbot_score = Common.DBotScore(
indicator='test_account_id',
integration_name='Test',
indicator_type=DBotScoreType.ACCOUNT,
score=Common.DBotScore.GOOD
)
account = Common.Account(
id='test_account_id',
type='test_account_type',
username='test_username',
display_name='test_display_name',
groups=None,
domain=None,
email_address='user@test.com',
telephone_number=None,
office='test_office',
job_title='test_job_title',
department='test_department',
country='test_country',
state='test_state',
city='test_city',
street='test_street',
is_enabled=None,
dbot_score=dbot_score,
relationships=None,
blocked=True,
community_notes=[Common.CommunityNotes(note='note', timestamp='2019-01-01T00:00:00')],
creation_date='test_creation_date',
description='test_description',
stix_id='test_stix_id',
tags=['tag1', 'tag2'],
traffic_light_protocol='traffic_light_protocol',
user_id='test_user_id',
manager_email='test_manager_email@test.com',
manager_display_name='test_manager_display_name',
risk_level='test_risk_level',
**{'some_undefinedKey': 'value'}
)
results = CommandResults(
outputs_key_field=None,
outputs_prefix=None,
outputs=None,
indicators=[account]
)
assert results.to_context() == {
'Type': 1,
'ContentsFormat': 'json',
'Contents': None,
'HumanReadable': None,
'EntryContext': {
'Account(val.id && val.id == obj.id)': [
{'ID': 'test_account_id',
'Type': 'test_account_type',
'Blocked': True,
'CreationDate': 'test_creation_date',
'City': 'test_city',
'CommunityNotes': [{'note': 'note', 'timestamp': '2019-01-01T00:00:00'}],
'Country': 'test_country',
'Department': 'test_department',
'Description': 'test_description',
'DisplayName': 'test_display_name',
'Email': {
'Address': 'user@test.com'
},
'JobTitle': 'test_job_title',
'Office': 'test_office',
'State': 'test_state',
'StixId': 'test_stix_id',
'Street': 'test_street',
'Tags': ['tag1', 'tag2'],
'TrafficLightProtocol': 'traffic_light_protocol',
'UserId': 'test_user_id',
'Username': 'test_username',
'Manager': {
'Email': 'test_manager_email@test.com',
'DisplayName': 'test_manager_display_name'
},
'RiskLevel': 'test_risk_level',
'some_undefinedKey': 'value'
}
],
'DBotScore(val.Indicator && val.Indicator == obj.Indicator &&'
' val.Vendor == obj.Vendor && val.Type == obj.Type)': [
{
'Indicator': 'test_account_id',
'Type': 'account',
'Vendor': 'Test',
'Score': 1
}
]
},
'IndicatorTimeline': [],
'IgnoreAutoExtract': False,
'Note': False,
'Relationships': []
}
def test_create_certificate(self):
"""
Given:
- an X509 Certificate with its properties
When
- creating a CommandResults with the Certificate Standard Context
Then
- the proper output Context is created
"""
from CommonServerPython import CommandResults, Common, EntryType, EntryFormat, DBotScoreType
dbot_score = Common.DBotScore(
indicator='bc33cf76519f1ec5ae7f287f321df33a7afd4fd553f364cf3c753f91ba689f8d',
integration_name='Test',
indicator_type=DBotScoreType.CERTIFICATE,
score=Common.DBotScore.NONE
)
cert_extensions = [
Common.CertificateExtension(
extension_type=Common.CertificateExtension.ExtensionType.AUTHORITYKEYIDENTIFIER,
authority_key_identifier=Common.CertificateExtension.AuthorityKeyIdentifier(
key_identifier="0f80611c823161d52f28e78d4638b42ce1c6d9e2"
),
critical=False
),
Common.CertificateExtension(
extension_type=Common.CertificateExtension.ExtensionType.SUBJECTKEYIDENTIFIER,
digest="b34972bb12121b8851cd5564ff9656dcbca3f288",
critical=False
),
Common.CertificateExtension(
extension_type=Common.CertificateExtension.ExtensionType.SUBJECTALTERNATIVENAME,
subject_alternative_names=[
Common.GeneralName(
gn_type="dNSName",
gn_value="*.paloaltonetworks.com"
),
Common.GeneralName(
gn_type="dNSName",
gn_value="paloaltonetworks.com"
)
],
critical=False
),
Common.CertificateExtension(
extension_type=Common.CertificateExtension.ExtensionType.KEYUSAGE,
digital_signature=True,
key_encipherment=True,
critical=True
),
Common.CertificateExtension(
extension_type=Common.CertificateExtension.ExtensionType.EXTENDEDKEYUSAGE,
usages=[
"serverAuth",
"clientAuth"
],
critical=False
),
Common.CertificateExtension(
extension_type=Common.CertificateExtension.ExtensionType.CRLDISTRIBUTIONPOINTS,
distribution_points=[
Common.CertificateExtension.DistributionPoint(
full_name=[
Common.GeneralName(
gn_type="uniformResourceIdentifier",
gn_value="http://crl3.digicert.com/ssca-sha2-g7.crl"
)
]
),
Common.CertificateExtension.DistributionPoint(
full_name=[
Common.GeneralName(
gn_type="uniformResourceIdentifier",
gn_value="http://crl4.digicert.com/ssca-sha2-g7.crl"
)
]
)
],
critical=False
),
Common.CertificateExtension(
extension_type=Common.CertificateExtension.ExtensionType.CERTIFICATEPOLICIES,
certificate_policies=[
Common.CertificateExtension.CertificatePolicy(
policy_identifier="2.16.840.1.114412.1.1",
policy_qualifiers=["https://www.digicert.com/CPS"]
),
Common.CertificateExtension.CertificatePolicy(
policy_identifier="2.23.140.1.2.2"
)
],
critical=False
),
Common.CertificateExtension(
extension_type=Common.CertificateExtension.ExtensionType.AUTHORITYINFORMATIONACCESS,
authority_information_access=[
Common.CertificateExtension.AuthorityInformationAccess(
access_method="OCSP",
access_location=Common.GeneralName(
gn_type="uniformResourceIdentifier",
gn_value="http://ocsp.digicert.com"
)
),
Common.CertificateExtension.AuthorityInformationAccess(
access_method="caIssuers",
access_location=Common.GeneralName(
gn_type="uniformResourceIdentifier",
gn_value="http://cacerts.digicert.com/DigiCertSHA2SecureServerCA.crt"
)
)
],
critical=False
),
Common.CertificateExtension(
extension_type=Common.CertificateExtension.ExtensionType.BASICCONSTRAINTS,
basic_constraints=Common.CertificateExtension.BasicConstraints(
ca=False
),
critical=False
),
Common.CertificateExtension(
extension_type=Common.CertificateExtension.ExtensionType.PRESIGNEDCERTIFICATETIMESTAMPS,
signed_certificate_timestamps=[
Common.CertificateExtension.SignedCertificateTimestamp(
version=0,
log_id="f65c942fd1773022145418083094568ee34d131933bfdf0c2f200bcc4ef164e3",
timestamp="2020-10-23T19:31:49.000Z",
entry_type="PreCertificate"
),
Common.CertificateExtension.SignedCertificateTimestamp(
version=0,
log_id="5cdc4392fee6ab4544b15e9ad456e61037fbd5fa47dca17394b25ee6f6c70eca",
timestamp="2020-10-23T19:31:49.000Z",
entry_type="PreCertificate"
)
],
critical=False
),
Common.CertificateExtension(
extension_type=Common.CertificateExtension.ExtensionType.SIGNEDCERTIFICATETIMESTAMPS,
signed_certificate_timestamps=[
Common.CertificateExtension.SignedCertificateTimestamp(
version=0,
log_id="f65c942fd1773022145418083094568ee34d131933bfdf0c2f200bcc4ef164e3",
timestamp="2020-10-23T19:31:49.000Z",
entry_type="X509Certificate"
),
Common.CertificateExtension.SignedCertificateTimestamp(
version=0,
log_id="5cdc4392fee6ab4544b15e9ad456e61037fbd5fa47dca17394b25ee6f6c70eca",
timestamp="2020-10-23T19:31:49.000Z",
entry_type="X509Certificate"
)
],
critical=False
)
]
certificate = Common.Certificate(
subject_dn='CN=*.paloaltonetworks.com,O=Palo Alto Networks\\, Inc.,L=Santa Clara,ST=California,C=US',
dbot_score=dbot_score,
serial_number='19290688218337824112020565039390569720',
issuer_dn='CN=DigiCert SHA2 Secure Server CA,O=DigiCert Inc,C=US',
validity_not_before='2020-10-23T00:00:00.000Z',
validity_not_after='2021-11-21T23:59:59.000Z',
sha256='bc33cf76519f1ec5ae7f287f321df33a7afd4fd553f364cf3c753f91ba689f8d',
sha1='2392ea5cd4c2a61e51547570634ef887ab1942e9',
md5='22769ae413997b86da4a0934072d9ed0',
publickey=Common.CertificatePublicKey(
algorithm=Common.CertificatePublicKey.Algorithm.RSA,
length=2048,
modulus='00:00:00:00',
exponent=65537
),
spki_sha256='94b716aeda21cd661949cfbf3f55457a277da712cdce0ab31989a4f288fad9b9',
signature_algorithm='sha256',
signature='SIGNATURE',
extensions=cert_extensions
)
results = CommandResults(
outputs_key_field=None,
outputs_prefix=None,
outputs=None,
indicators=[certificate]
)
CONTEXT_PATH = "Certificate(val.MD5 && val.MD5 == obj.MD5 || val.SHA1 && val.SHA1 == obj.SHA1 || " \
"val.SHA256 && val.SHA256 == obj.SHA256 || val.SHA512 && val.SHA512 == obj.SHA512)"
assert results.to_context() == {
'Type': EntryType.NOTE,
'ContentsFormat': EntryFormat.JSON,
'Contents': None,
'HumanReadable': None,
'EntryContext': {
CONTEXT_PATH: [{
"SubjectDN": "CN=*.paloaltonetworks.com,O=Palo Alto Networks\\, Inc.,L=Santa Clara,ST=California,C=US",
"SubjectAlternativeName": [
{
"Type": "dNSName",
"Value": "*.paloaltonetworks.com"
},
{
"Type": "dNSName",
"Value": "paloaltonetworks.com"
}
],
"Name": [
"*.paloaltonetworks.com",
"paloaltonetworks.com"
],
"IssuerDN": "CN=DigiCert SHA2 Secure Server CA,O=DigiCert Inc,C=US",
"SerialNumber": "19290688218337824112020565039390569720",
"ValidityNotBefore": "2020-10-23T00:00:00.000Z",
"ValidityNotAfter": "2021-11-21T23:59:59.000Z",
"SHA256": "bc33cf76519f1ec5ae7f287f321df33a7afd4fd553f364cf3c753f91ba689f8d",
"SHA1": "2392ea5cd4c2a61e51547570634ef887ab1942e9",
"MD5": "22769ae413997b86da4a0934072d9ed0",
"PublicKey": {
"Algorithm": "RSA",
"Length": 2048,
"Modulus": "00:00:00:00",
"Exponent": 65537
},
"SPKISHA256": "94b716aeda21cd661949cfbf3f55457a277da712cdce0ab31989a4f288fad9b9",
"Signature": {
"Algorithm": "sha256",
"Signature": "SIGNATURE"
},
"Extension": [
{
"OID": "2.5.29.35",
"Name": "authorityKeyIdentifier",
"Critical": False,
"Value": {
"KeyIdentifier": "0f80611c823161d52f28e78d4638b42ce1c6d9e2"
}
},
{
"OID": "2.5.29.14",
"Name": "subjectKeyIdentifier",
"Critical": False,
"Value": {
"Digest": "b34972bb12121b8851cd5564ff9656dcbca3f288"
}
},
{
"OID": "2.5.29.17",
"Name": "subjectAltName",
"Critical": False,
"Value": [
{
"Type": "dNSName",
"Value": "*.paloaltonetworks.com"
},
{
"Type": "dNSName",
"Value": "paloaltonetworks.com"
}
]
},
{
"OID": "2.5.29.15",
"Name": "keyUsage",
"Critical": True,
"Value": {
"DigitalSignature": True,
"KeyEncipherment": True
}
},
{
"OID": "2.5.29.37",
"Name": "extendedKeyUsage",
"Critical": False,
"Value": {
"Usages": [
"serverAuth",
"clientAuth"
]
}
},
{
"OID": "2.5.29.31",
"Name": "cRLDistributionPoints",
"Critical": False,
"Value": [
{
"FullName": [
{
"Type": "uniformResourceIdentifier",
"Value": "http://crl3.digicert.com/ssca-sha2-g7.crl"
}
]
},
{
"FullName": [
{
"Type": "uniformResourceIdentifier",
"Value": "http://crl4.digicert.com/ssca-sha2-g7.crl"
}
]
}
]
},
{
"OID": "2.5.29.32",
"Name": "certificatePolicies",
"Critical": False,
"Value": [
{
"PolicyIdentifier": "2.16.840.1.114412.1.1",
"PolicyQualifiers": [
"https://www.digicert.com/CPS"
]
},
{
"PolicyIdentifier": "2.23.140.1.2.2"
}
]
},
{
"OID": "1.3.6.1.5.5.7.1.1",
"Name": "authorityInfoAccess",
"Critical": False,
"Value": [
{
"AccessMethod": "OCSP",
"AccessLocation": {
"Type": "uniformResourceIdentifier",
"Value": "http://ocsp.digicert.com"
}
},
{
"AccessMethod": "caIssuers",
"AccessLocation": {
"Type": "uniformResourceIdentifier",
"Value": "http://cacerts.digicert.com/DigiCertSHA2SecureServerCA.crt"
}
}
]
},
{
"OID": "2.5.29.19",
"Name": "basicConstraints",
"Critical": False,
"Value": {
"CA": False
}
},
{
"OID": "1.3.6.1.4.1.11129.2.4.2",
"Name": "signedCertificateTimestampList",
"Critical": False,
"Value": [
{
"Version": 0,
"LogId": "f65c942fd1773022145418083094568ee34d131933bfdf0c2f200bcc4ef164e3",
"Timestamp": "2020-10-23T19:31:49.000Z",
"EntryType": "PreCertificate"
},
{
"Version": 0,
"LogId": "5cdc4392fee6ab4544b15e9ad456e61037fbd5fa47dca17394b25ee6f6c70eca",
"Timestamp": "2020-10-23T19:31:49.000Z",
"EntryType": "PreCertificate"
}
]
},
{
"OID": "1.3.6.1.4.1.11129.2.4.5",
"Name": "signedCertificateTimestampList",
"Critical": False,
"Value": [
{
"Version": 0,
"LogId": "f65c942fd1773022145418083094568ee34d131933bfdf0c2f200bcc4ef164e3",
"Timestamp": "2020-10-23T19:31:49.000Z",
"EntryType": "X509Certificate"
},
{
"Version": 0,
"LogId": "5cdc4392fee6ab4544b15e9ad456e61037fbd5fa47dca17394b25ee6f6c70eca",
"Timestamp": "2020-10-23T19:31:49.000Z",
"EntryType": "X509Certificate"
}
]
}
]
}],
'DBotScore(val.Indicator && val.Indicator == obj.Indicator && '
'val.Vendor == obj.Vendor && val.Type == obj.Type)': [{
"Indicator": "bc33cf76519f1ec5ae7f287f321df33a7afd4fd553f364cf3c753f91ba689f8d",
"Type": "certificate",
"Vendor": "Test",
"Score": 0
}]
},
'IndicatorTimeline': [],
'Relationships': [],
'IgnoreAutoExtract': False,
'Note': False
}
def test_create_external_reference(self):
"""
Given:
- A single ExternalReference object
When
- Running 'to_context' function
Then
- Verify that the context is as expected
"""
from CommonServerPython import Common
external_reference = Common.ExternalReference(
source_name='test_source_name',
source_id='test_source_id'
)
assert external_reference.to_context() == {
'sourcename': 'test_source_name',
'sourceid': 'test_source_id'
}
def test_create_attack_pattern(self):
"""
Given:
- A single AttackPattern indicator entry
When
- Creating a Common.AttackPattern object
Then
- The context created matches the data entry
"""
from CommonServerPython import CommandResults, Common
dbot_score = Common.DBotScore(
indicator='test_stix_id',
integration_name='Test',
indicator_type=DBotScoreType.ATTACKPATTERN,
score=Common.DBotScore.GOOD
)
attack_pattern = Common.AttackPattern(
stix_id='test_stix_id',
kill_chain_phases='test_kill_chain_phases',
first_seen_by_source=None,
description='test_description',
operating_system_refs=None,
publications='test_publications',
mitre_id='test_mitre_id',
tags=['tag1', 'tag2'],
traffic_light_protocol='test_traffic_light_protocol',
dbot_score=dbot_score,
value='test_stix_id',
community_notes=[Common.CommunityNotes(note='note', timestamp='2019-01-01T00:00:00')],
external_references=None
)
results = CommandResults(
outputs_key_field=None,
outputs_prefix=None,
outputs=None,
indicators=[attack_pattern]
)
assert results.to_context() == {
'Type': 1,
'ContentsFormat': 'json',
'Contents': None,
'HumanReadable': None,
'EntryContext': {
'AttackPattern(val.value && val.value == obj.value)': [
{
'STIXID': 'test_stix_id',
'KillChainPhases': 'test_kill_chain_phases',
'FirstSeenBySource': None,
'OperatingSystemRefs': None,
'Publications': 'test_publications',
'MITREID': 'test_mitre_id',
'Value': 'test_stix_id',
'Tags': ['tag1', 'tag2'],
'Description': 'test_description',
'TrafficLightProtocol': 'test_traffic_light_protocol'
}
],
'DBotScore(val.Indicator && val.Indicator == obj.Indicator &&'
' val.Vendor == obj.Vendor && val.Type == obj.Type)': [
{
'Indicator': 'test_stix_id',
'Type': 'attackpattern',
'Vendor': 'Test',
'Score': 1
}
]
},
'IndicatorTimeline': [],
'IgnoreAutoExtract': False,
'Note': False,
'Relationships': []
}
def test_create_certificates(self):
"""
Given:
- A Certificates object
When
- Running 'to_context' function
Then
- Verify that the context is as expected
"""
from CommonServerPython import Common
certificates = Common.Certificates(
issued_to='test_issued_to',
issued_by='test_issued_by',
valid_from='test_valid_from',
valid_to='test_valid_to'
)
assert certificates.to_context() == {
'issuedto': 'test_issued_to',
'issuedby': 'test_issued_by',
'validfrom': 'test_valid_from',
'validto': 'test_valid_to'
}
def test_create_hash(self):
"""
Given:
- A single Hash object
When
- Running 'to_context' function
Then
- Verify that the context is as expected
"""
from CommonServerPython import Common
hash_object = Common.Hash(
hash_type='test_hash_type',
hash_value='test_hash_value'
)
assert hash_object.to_context() == {
'type': 'test_hash_type',
'value': 'test_hash_value',
}
def test_create_whois_record(self):
"""
Given:
- A single WhoisRecord object
When
- Running 'to_context' function
Then
- Verify that the context is as expected
"""
from CommonServerPython import Common
whois_record = Common.WhoisRecord(
whois_record_type='test_whois_record_type',
whois_record_value='test_whois_record_value',
whois_record_date='test_whois_record_date',
)
assert whois_record.to_context() == {
'key': 'test_whois_record_type',
'value': 'test_whois_record_value',
'date': 'test_whois_record_date'
}
def test_create_dns_record(self):
"""
Given:
- A single DNSRecord object
When
- Running 'to_context' function
Then
- Verify that the context is as expected
"""
from CommonServerPython import Common
dns_record = Common.DNSRecord(
dns_record_type='test_dns_record_type',
dns_ttl='test_dns_ttl',
dns_record_data='test_dns_record_data',
)
assert dns_record.to_context() == {
'type': 'test_dns_record_type',
'ttl': 'test_dns_ttl',
'data': 'test_dns_record_data'
}
def test_email_indicator_type(self, mocker):
"""
Given:
- a single email indicator entry
When
- creating an Common.EMAIL object
Then
- The context created matches the data entry
"""
from CommonServerPython import Common, DBotScoreType
mocker.patch.object(demisto, 'params', return_value={'insecure': True})
dbot_score = Common.DBotScore(
indicator='user@example.com',
integration_name='Test',
indicator_type=DBotScoreType.EMAIL,
score=Common.DBotScore.GOOD
)
dbot_context = {'DBotScore(val.Indicator && val.Indicator == obj.Indicator && '
'val.Vendor == obj.Vendor && val.Type == obj.Type)':
{'Indicator': 'user@example.com', 'Type': 'email', 'Vendor': 'Test', 'Score': 1}}
assert dbot_context == dbot_score.to_context()
email_context = Common.EMAIL(
domain='example.com',
address='user@example.com',
dbot_score=dbot_score,
description='test',
internal=True,
stix_id='stix_id_test',
tags=['tag1', 'tag2'],
traffic_light_protocol='traffic_light_protocol_test'
)
assert email_context.to_context()[email_context.CONTEXT_PATH] == \
{"Email": {'Address': 'user@example.com'},
'Domain': 'example.com',
'Description': 'test',
'Internal': True,
'STIXID': 'stix_id_test',
'Tags': ['tag1', 'tag2'],
'TrafficLightProtocol': 'traffic_light_protocol_test'}
@pytest.mark.parametrize('item', [
'CommunityNotes', 'Publications', 'ThreatTypes'
])
def test_common_indicator_create_context_table(self, item):
"""
Tests the functionality of the 'create_context_table' function.
Given:
Case a: A list containing CommunityNotes items.
Case b: A list containing Publications items.
Case c: A list containing ThreatTypes items.
When:
Running the 'create_context_table' function.
Then:
Case a: Verify that the output is a list of CommunityNotes context items as expected.
Case b: Verify that the output is a list of Publications context items as expected.
Case c: Verify that the output is a list of ThreatTypes context items as expected.
"""
if item == 'CommunityNotes':
community_notes1 = Common.CommunityNotes(note='note1', timestamp='time1')
community_notes2 = Common.CommunityNotes(note='note2', timestamp='time2')
items = [community_notes1, community_notes2]
expected_output = [{'note': 'note1', 'timestamp': 'time1'}, {'note': 'note2', 'timestamp': 'time2'}]
elif item == 'Publications':
publications1 = Common.Publications(source='source1', title='title1', link='link1', timestamp='time1')
publications2 = Common.Publications(source='source2', title='title2', link='link2', timestamp='time2')
items = [publications1, publications2]
expected_output = [{'source': 'source1', 'title': 'title1', 'link': 'link1', 'timestamp': 'time1'},
{'source': 'source2', 'title': 'title2', 'link': 'link2', 'timestamp': 'time2'}]
elif item == 'ThreatTypes':
threat_types1 = Common.ThreatTypes(threat_category='test1', threat_category_confidence='10')
threat_types2 = Common.ThreatTypes(threat_category='test2', threat_category_confidence='20')
items = [threat_types1, threat_types2]
expected_output = [{'threatcategory': 'test1', 'threatcategoryconfidence': '10'},
{'threatcategory': 'test2', 'threatcategoryconfidence': '20'}]
table = Common.Indicator.create_context_table(items)
assert table == expected_output
class TestIndicatorsSearcher:
def mock_search_after_output(self, fromDate='', toDate='', query='', size=0, value='', page=0, searchAfter='',
populateFields=None):
if not searchAfter:
searchAfter = 0
iocs = [{'value': 'mock{}'.format(searchAfter)}]
if searchAfter < 6:
searchAfter += 1
else:
# mock the end of indicators
searchAfter = None
if page and page >= 17:
# checking a unique case when trying to reach a certain page and not all the indicators
iocs = []
searchAfter = None
return {'searchAfter': searchAfter, 'iocs': iocs, 'total': 7}
def mock_search_indicators_search_after(self, fromDate='', toDate='', query='', size=0, value='', page=0,
searchAfter=None, populateFields=None):
"""
Mocks search indicators returning different results for searchAfter value:
- None: {searchAfter: 0, iocs: [...]}
- 0-2: {searchAfter: i+1, iocs: [...]}
- 3+: {searchAfter: None, iocs: []}
total of 4 iocs available
"""
search_after_options = (0, 1, 2)
if searchAfter is None:
search_after_value = search_after_options[0]
else:
if searchAfter in search_after_options:
search_after_value = searchAfter + 1
else:
return {'searchAfter': None, 'iocs': [], "total": 0}
iocs = [{'value': 'mock{}'.format(search_after_value)}]
return {'searchAfter': search_after_value, 'iocs': iocs, 'total': 4}
def test_search_indicators_by_search_after(self, mocker):
"""
Given:
- Searching indicators couple of times
- Server version in equal or higher than 6.1.0
When:
- Mocking search indicators using the searchAfter parameter
Then:
- The search after param is rising
- The page param is rising
"""
from CommonServerPython import IndicatorsSearcher
mocker.patch.object(demisto, 'searchIndicators', side_effect=self.mock_search_after_output)
search_indicators_obj_search_after = IndicatorsSearcher()
try:
for n in range(5):
search_indicators_obj_search_after.search_indicators_by_version()
except Exception as e:
print(e)
assert search_indicators_obj_search_after._search_after_param == 5
assert search_indicators_obj_search_after._page == 5
def test_search_all_indicators_by_search_after(self, mocker):
"""
Given:
- Searching indicators couple of times
- Server version in equal or higher than 6.1.0
When:
- Mocking search indicators using the searchAfter parameter until there are no more indicators
so search_after is None
Then:
- The search after param is None
- The page param is rising
"""
from CommonServerPython import IndicatorsSearcher
mocker.patch.object(demisto, 'searchIndicators', side_effect=self.mock_search_after_output)
search_indicators_obj_search_after = IndicatorsSearcher()
for n in range(7):
search_indicators_obj_search_after.search_indicators_by_version()
assert search_indicators_obj_search_after._search_after_param is None
assert search_indicators_obj_search_after._page == 7
def test_iterator__search_after(self, mocker):
"""
Given:
- Searching indicators from first page
- Total available indicators == 7
- Limit is set to 10
When:
- Searching indicators using iterator
- search_after is supported
Then:
- Get 7 indicators
"""
from CommonServerPython import IndicatorsSearcher
mocker.patch.object(demisto, 'searchIndicators', side_effect=self.mock_search_indicators_search_after)
search_indicators = IndicatorsSearcher(limit=10)
results = []
for res in search_indicators:
results.append(res)
assert len(results) == 4
def test_iterator__research_flow(self, mocker):
from CommonServerPython import IndicatorsSearcher
mocker.patch.object(demisto, 'searchIndicators', side_effect=self.mock_search_indicators_search_after)
# fetch first 3
search_indicators = IndicatorsSearcher(limit=3)
results = []
for res in search_indicators:
results.append(res)
assert len(results) == 3
# fetch 1 more (limit set to 2, but only 1 available)
search_indicators.limit += 2
results = []
for res in search_indicators:
results.append(res)
assert len(results) == 1
def test_search_indicators_with_sort(self, mocker):
"""
Given:
- Searching indicators with a custom sort parameter.
- Mocking the searchIndicators function.
When:
- Calling the searchIndicators function with the custom sort parameter.
Then:
- Ensure that the sort parameter is set correctly.
- Ensure that the searchIndicators function is called with the expected arguments.
"""
from CommonServerPython import IndicatorsSearcher
get_demisto_version._version = None # clear cache between runs of the test
mocker.patch.object(demisto, 'demistoVersion', return_value={'version': '6.6.0'})
mocker.patch.object(demisto, 'searchIndicators')
sort_param = [{"field": "created", "asc": False}]
search_indicators_obj_search_after = IndicatorsSearcher(sort=sort_param)
search_indicators_obj_search_after.search_indicators_by_version()
expected_args = {'size': 100, 'sort': [{'asc': False, 'field': 'created'}]}
assert search_indicators_obj_search_after._sort == sort_param
demisto.searchIndicators.assert_called_once_with(**expected_args)
class TestAutoFocusKeyRetriever:
def test_instantiate_class_with_param_key(self, mocker, clear_version_cache):
"""
Given:
- giving the api_key parameter
When:
- Mocking getAutoFocusApiKey
- Mocking server version to be 6.2.0
Then:
- The Auto Focus API Key is the one given to the class
"""
from CommonServerPython import AutoFocusKeyRetriever
mocker.patch.object(demisto, 'getAutoFocusApiKey', return_value='test')
mocker.patch.object(demisto, 'demistoVersion', return_value={'version': '6.2.0', 'buildNumber': '62000'})
auto_focus_key_retriever = AutoFocusKeyRetriever(api_key='1234')
assert auto_focus_key_retriever.key == '1234'
def test_instantiate_class_without_param_key(self, mocker, clear_version_cache):
"""
Given:
- not giving the api_key parameter
When:
- Mocking getAutoFocusApiKey
- Mocking server version to be 6.2.0
Then:
- The Auto Focus API Key is the one given by the getAutoFocusApiKey method
"""
from CommonServerPython import AutoFocusKeyRetriever
mocker.patch.object(demisto, 'getAutoFocusApiKey', return_value='test')
mocker.patch.object(demisto, 'demistoVersion', return_value={'version': '6.2.0', 'buildNumber': '62000'})
auto_focus_key_retriever = AutoFocusKeyRetriever(api_key='')
assert auto_focus_key_retriever.key == 'test'
class TestEntityRelationship:
"""Global vars for all of the tests"""
name = 'related-to'
reverse_name = 'related-to'
relationship_type = 'IndicatorToIndicator'
entity_a = 'test1'
entity_a_family = 'Indicator'
entity_a_type = 'Domain'
entity_b = 'test2'
entity_b_family = 'Indicator'
entity_b_type = 'Domain'
source_reliability = 'F - Reliability cannot be judged'
def test_entity_relations_context(self):
"""
Given
- an EntityRelationship object.
When
- running to_context function of the object
Then
- Validate that the expected context is created
"""
from CommonServerPython import EntityRelationship
relationship = EntityRelationship(name='related-to',
relationship_type='IndicatorToIndicator',
entity_a='test1',
entity_a_family='Indicator',
entity_a_type='Domain',
entity_b='test2',
entity_b_family='Indicator',
entity_b_type='Domain',
source_reliability='F - Reliability cannot be judged',
brand='test')
expected_context = {
"Relationship": 'related-to',
"EntityA": 'test1',
"EntityAType": 'Domain',
"EntityB": 'test2',
"EntityBType": 'Domain',
}
assert relationship.to_context() == expected_context
def test_entity_relations_to_entry(self):
"""
Given
- an EntityRelationship object.
When
- running to_entry function of the object
Then
- Validate that the expected context is created
"""
from CommonServerPython import EntityRelationship
relationship = EntityRelationship(name=TestEntityRelationship.name,
relationship_type=TestEntityRelationship.relationship_type,
entity_a=TestEntityRelationship.entity_a,
entity_a_family=TestEntityRelationship.entity_a_family,
entity_a_type=TestEntityRelationship.entity_a_type,
entity_b=TestEntityRelationship.entity_b,
entity_b_family=TestEntityRelationship.entity_b_family,
entity_b_type=TestEntityRelationship.entity_b_type,
source_reliability=TestEntityRelationship.source_reliability
)
expected_entry = {
"name": TestEntityRelationship.name,
"reverseName": TestEntityRelationship.reverse_name,
"type": TestEntityRelationship.relationship_type,
"entityA": TestEntityRelationship.entity_a,
"entityAFamily": TestEntityRelationship.entity_a_family,
"entityAType": TestEntityRelationship.entity_a_type,
"entityB": TestEntityRelationship.entity_b,
"entityBFamily": TestEntityRelationship.entity_b_family,
"entityBType": TestEntityRelationship.entity_b_type,
"fields": {},
"reliability": TestEntityRelationship.source_reliability
}
assert relationship.to_entry() == expected_entry
def test_entity_relations_to_indicator(self):
"""
Given
- an EntityRelationship object.
When
- running to_indicator function of the object
Then
- Validate that the expected context is created
"""
from CommonServerPython import EntityRelationship
relationship = EntityRelationship(name=TestEntityRelationship.name,
relationship_type=TestEntityRelationship.relationship_type,
entity_a=TestEntityRelationship.entity_a,
entity_a_family=TestEntityRelationship.entity_a_family,
entity_a_type=TestEntityRelationship.entity_a_type,
entity_b=TestEntityRelationship.entity_b,
entity_b_family=TestEntityRelationship.entity_b_family,
entity_b_type=TestEntityRelationship.entity_b_type,
)
expected_to_indicator = {
"name": TestEntityRelationship.name,
"reverseName": TestEntityRelationship.reverse_name,
"type": TestEntityRelationship.relationship_type,
"entityA": TestEntityRelationship.entity_a,
"entityAFamily": TestEntityRelationship.entity_a_family,
"entityAType": TestEntityRelationship.entity_a_type,
"entityB": TestEntityRelationship.entity_b,
"entityBFamily": TestEntityRelationship.entity_b_family,
"entityBType": TestEntityRelationship.entity_b_type,
"fields": {},
}
assert relationship.to_indicator() == expected_to_indicator
def test_invalid_name_init(self):
"""
Given
- an EntityRelation object which has a invalid relation name.
When
- Creating the EntityRelation object.
Then
- Validate a ValueError is raised.
"""
from CommonServerPython import EntityRelationship
try:
EntityRelationship(name='ilegal',
relationship_type=TestEntityRelationship.relationship_type,
entity_a=TestEntityRelationship.entity_a,
entity_a_family=TestEntityRelationship.entity_a_family,
entity_a_type=TestEntityRelationship.entity_a_type,
entity_b=TestEntityRelationship.entity_b,
entity_b_family=TestEntityRelationship.entity_b_family,
entity_b_type=TestEntityRelationship.entity_b_type
)
except ValueError as exception:
assert "Invalid relationship: ilegal" in str(exception)
def test_invalid_relation_type_init(self):
"""
Given
- an EntityRelation object which has a invalid relation type.
When
- Creating the EntityRelation object.
Then
- Validate a ValueError is raised.
"""
from CommonServerPython import EntityRelationship
try:
EntityRelationship(name=TestEntityRelationship.name,
relationship_type='TestRelationshipType',
entity_a=TestEntityRelationship.entity_a,
entity_a_family=TestEntityRelationship.entity_a_family,
entity_a_type=TestEntityRelationship.entity_a_type,
entity_b=TestEntityRelationship.entity_b,
entity_b_family=TestEntityRelationship.entity_b_family,
entity_b_type=TestEntityRelationship.entity_b_type
)
except ValueError as exception:
assert "Invalid relationship type: TestRelationshipType" in str(exception)
def test_invalid_a_family_init(self):
"""
Given
- an EntityRelation object which has a invalid family type of the source.
When
- Creating the EntityRelation object.
Then
- Validate a ValueError is raised.
"""
from CommonServerPython import EntityRelationship
try:
EntityRelationship(name=TestEntityRelationship.name,
relationship_type=TestEntityRelationship.relationship_type,
entity_a=TestEntityRelationship.entity_a,
entity_a_family='IndicatorIlegal',
entity_a_type=TestEntityRelationship.entity_a_type,
entity_b=TestEntityRelationship.entity_b,
entity_b_family=TestEntityRelationship.entity_b_family,
entity_b_type=TestEntityRelationship.entity_b_type
)
except ValueError as exception:
assert "Invalid entity A Family type: IndicatorIlegal" in str(exception)
def test_invalid_a_type_init(self):
"""
Given
- an EntityRelation object which has a invalid type of the source.
When
- Creating the EntityRelation object.
Then
- Validate a ValueError is raised.
"""
from CommonServerPython import EntityRelationship
try:
EntityRelationship(name=TestEntityRelationship.name,
relationship_type=TestEntityRelationship.relationship_type,
entity_a=TestEntityRelationship.entity_a,
entity_a_family=TestEntityRelationship.entity_a_family,
entity_a_type='DomainTest',
entity_b=TestEntityRelationship.entity_b,
entity_b_family=TestEntityRelationship.entity_b_family,
entity_b_type=TestEntityRelationship.entity_b_type
)
except ValueError as exception:
assert "Invalid entity A type: DomainTest" in str(exception)
def test_invalid_b_family_init(self):
"""
Given
- an EntityRelation object which has a invalid family type of the destination.
When
- Creating the EntityRelation object.
Then
- Validate a ValueError is raised.
"""
from CommonServerPython import EntityRelationship
try:
EntityRelationship(name=TestEntityRelationship.name,
relationship_type=TestEntityRelationship.relationship_type,
entity_a=TestEntityRelationship.entity_a,
entity_a_family=TestEntityRelationship.entity_a_family,
entity_a_type=TestEntityRelationship.entity_a_type,
entity_b=TestEntityRelationship.entity_b,
entity_b_family='IndicatorIlegal',
entity_b_type=TestEntityRelationship.entity_b_type
)
except ValueError as exception:
assert "Invalid entity B Family type: IndicatorIlegal" in str(exception)
def test_invalid_b_type_init(self):
"""
Given
- an EntityRelation object which has a invalid type of the destination.
When
- Creating the EntityRelation object.
Then
- Validate a ValueError is raised.
"""
from CommonServerPython import EntityRelationship
try:
EntityRelationship(name=TestEntityRelationship.name,
relationship_type=TestEntityRelationship.relationship_type,
entity_a=TestEntityRelationship.entity_a,
entity_a_family=TestEntityRelationship.entity_a_family,
entity_a_type=TestEntityRelationship.entity_a_type,
entity_b=TestEntityRelationship.entity_b,
entity_b_family=TestEntityRelationship.entity_b_family,
entity_b_type='DomainTest'
)
except ValueError as exception:
assert "Invalid entity B type: DomainTest" in str(exception)
class TestSetAndGetLastRun:
def test_get_last_run_in_6_2_when_get_last_run_has_results(self, mocker):
"""
Given: 6.2.0 environment and getLastRun returns results
When: Fetch indicators
Then: Returning all indicators from demisto.getLastRun object
"""
import demistomock as demisto
from CommonServerPython import get_feed_last_run
mocker.patch('CommonServerPython.get_demisto_version', return_value={"version": "6.2.0"})
mocker.patch.object(demisto, 'getLastRun', return_value={1: "first indicator"})
result = get_feed_last_run()
assert result == {1: "first indicator"}
def test_get_last_run_in_6_2_when_get_last_run_has_no_results(self, mocker):
"""
Given: 6.2.0 environment and getLastRun and getIntegrationContext are empty
When: Fetch indicators
Then: function will return empty dict
"""
import demistomock as demisto
from CommonServerPython import get_feed_last_run
mocker.patch('CommonServerPython.get_demisto_version', return_value={"version": "6.2.0"})
mocker.patch.object(demisto, 'getIntegrationContext', return_value={})
mocker.patch.object(demisto, 'getLastRun', return_value={})
result = get_feed_last_run()
assert result == {}
def test_get_last_run_in_6_2_when_get_last_is_empty_and_get_integration_is_not(self, mocker):
"""
Given: 6.2.0 environment and getLastRun is empty and getIntegrationContext has results.
When: Fetch indicators
Then: function will return empty dict
"""
import demistomock as demisto
from CommonServerPython import get_feed_last_run
mocker.patch('CommonServerPython.get_demisto_version', return_value={"version": "6.2.0"})
mocker.patch.object(demisto, 'getIntegrationContext', return_value={1: "first indicator"})
mocker.patch.object(demisto, 'getLastRun', return_value={})
set_last_run = mocker.patch.object(demisto, 'setLastRun', return_value={})
set_integration_context = mocker.patch.object(demisto, 'setIntegrationContext', return_value={})
result = get_feed_last_run()
assert result == {1: "first indicator"}
set_last_run.assert_called_with({1: "first indicator"})
set_integration_context.assert_called_with({})
def test_set_last_run_in_6_2(self, mocker):
"""
Given: 6.2.0 environment
When: Fetch indicators
Then: Using demisto.setLastRun to save results
"""
import demistomock as demisto
from CommonServerPython import set_feed_last_run
mocker.patch('CommonServerPython.get_demisto_version', return_value={"version": "6.2.0"})
set_last_run = mocker.patch.object(demisto, 'setLastRun', return_value={})
set_integration_context = mocker.patch.object(demisto, 'setIntegrationContext', return_value={})
set_feed_last_run({1: "first indicator"})
assert set_integration_context.called is False
set_last_run.assert_called_with({1: "first indicator"})
class TestIsDemistoServerGE:
@classmethod
@pytest.fixture(scope='function', autouse=True)
def clear_cache(cls):
get_demisto_version._version = None
def test_get_demisto_version(self, mocker):
# verify expected server version and build returned in case Demisto class has attribute demistoVersion
mocker.patch.object(
demisto,
'demistoVersion',
return_value={
'version': '5.0.0',
'buildNumber': '50000'
}
)
assert get_demisto_version() == {
'version': '5.0.0',
'buildNumber': '50000'
}
# call again to check cache
assert get_demisto_version() == {
'version': '5.0.0',
'buildNumber': '50000'
}
# call count should be 1 as we cached
assert demisto.demistoVersion.call_count == 1
# test is_demisto_version_ge
assert is_demisto_version_ge('5.0.0')
assert is_demisto_version_ge('4.5.0')
assert not is_demisto_version_ge('5.5.0')
assert get_demisto_version_as_str() == '5.0.0-50000'
def test_get_demisto_version_2(self, mocker):
mocker.patch.object(
demisto,
'demistoVersion',
return_value={
'version': '6.10.0',
'buildNumber': '50000'
}
)
assert get_demisto_version() == {
'version': '6.10.0',
'buildNumber': '50000'
}
assert is_demisto_version_ge('6.5.0')
assert is_demisto_version_ge('6.1.0')
assert is_demisto_version_ge('6.5')
assert not is_demisto_version_ge('7.0.0')
def test_is_demisto_version_ge_4_5(self, mocker):
get_version_patch = mocker.patch('CommonServerPython.get_demisto_version')
get_version_patch.side_effect = AttributeError('simulate missing demistoVersion')
assert not is_demisto_version_ge('5.0.0')
assert not is_demisto_version_ge('6.0.0')
with pytest.raises(AttributeError, match='simulate missing demistoVersion'):
is_demisto_version_ge('4.5.0')
def test_is_demisto_version_ge_dev_version(self, mocker):
mocker.patch.object(
demisto,
'demistoVersion',
return_value={
'version': '6.0.0',
'buildNumber': '50000'
}
)
assert is_demisto_version_ge('6.0.0', '1-dev')
@pytest.mark.parametrize('version, build', [
('6.0.0', '49999'),
('6.0.0', '50000'),
('6.0.0', '6'), # Added with the fix of https://github.com/demisto/etc/issues/36876
('5.5.0', '50001')
])
def test_is_demisto_version_build_ge(self, mocker, version, build):
mocker.patch.object(
demisto,
'demistoVersion',
return_value={
'version': '6.0.0',
'buildNumber': '50000'
}
)
assert is_demisto_version_ge(version, build)
@pytest.mark.parametrize('version, build', [
('6.0.0', '50001'),
('6.1.0', '49999')
])
def test_is_demisto_version_build_ge_negative(self, mocker, version, build):
mocker.patch.object(
demisto,
'demistoVersion',
return_value={
'version': '6.0.0',
'buildNumber': '50000'
}
)
assert not is_demisto_version_ge(version, build)
class TestDeterminePlatform:
@classmethod
@pytest.fixture(scope='function', autouse=True)
def clear_cache(cls):
get_demisto_version._version = None
@pytest.mark.parametrize('demistoVersion, method', [
({'platform': 'xsoar', 'version': '6.5.0'}, is_xsoar),
({'platform': 'xsoar', 'version': '8.2.0'}, is_xsoar),
({'platform': 'xsoar_hosted', 'version': '6.5.0'}, is_xsoar),
({'platform': 'x2', 'version': '8.2.0'}, is_xsiam_or_xsoar_saas),
({'platform': 'xsoar', 'version': '8.2.0'}, is_xsiam_or_xsoar_saas),
({'platform': 'xsoar', 'version': '6.5.0'}, is_xsoar_on_prem),
({'platform': 'xsoar_hosted', 'version': '6.5.0'}, is_xsoar_hosted),
({'platform': 'xsoar', 'version': '8.2.0'}, is_xsoar_saas),
({'platform': 'x2', 'version': '8.2.0'}, is_xsiam),
])
def test_determine_platform(self, mocker, demistoVersion, method):
mocker.patch.object(demisto, 'demistoVersion', return_value=demistoVersion)
assert method()
class TestResolveShouldPushEvents:
"""Tests for the resolve_should_push_events utility function."""
def test_true_on_xsiam_returns_true(self, mocker):
"""Given should_push_events='true' on XSIAM, should return True."""
mocker.patch('CommonServerPython.is_xsiam', return_value=True)
assert resolve_should_push_events({'should_push_events': 'true'}) is True
def test_true_on_non_xsiam_returns_false(self, mocker):
"""Given should_push_events='true' on non-XSIAM, should return False and log debug."""
mocker.patch('CommonServerPython.is_xsiam', return_value=False)
mock_debug = mocker.patch.object(demisto, 'debug')
assert resolve_should_push_events({'should_push_events': 'true'}) is False
mock_debug.assert_called_once()
assert 'should_push_events' in mock_debug.call_args[0][0]
def test_false_on_any_platform_returns_false(self, mocker):
"""Given should_push_events='false', should return False regardless of platform."""
mocker.patch('CommonServerPython.is_xsiam', return_value=True)
assert resolve_should_push_events({'should_push_events': 'false'}) is False
mocker.patch('CommonServerPython.is_xsiam', return_value=False)
assert resolve_should_push_events({'should_push_events': 'false'}) is False
def test_missing_arg_defaults_to_false(self, mocker):
"""Given no should_push_events arg, should default to False."""
mocker.patch('CommonServerPython.is_xsiam', return_value=True)
assert resolve_should_push_events({}) is False
def test_smart_get_dict():
d = {'t1': None, "t2": 1}
# before we remove the dict will return null which is unexpected by a lot of users
assert d.get('t1', 2) is None
s = SmartGetDict(d)
assert s.get('t1', 2) == 2
assert s.get('t2') == 1
assert s.get('t3') is None
class TestCustomIndicator:
def test_custom_indicator_init_success(self):
"""
Given: Data needed for creating a custom indicator
When: Data is valid
Then: Create a valid custom indicator
"""
from CommonServerPython import Common, DBotScoreType
dbot_score = Common.DBotScore(
'test',
DBotScoreType.CUSTOM,
'VirusTotal',
score=Common.DBotScore.BAD,
malicious_description='malicious!'
)
indicator = Common.CustomIndicator('test', 'test_value', dbot_score, {'param': 'value'}, 'prefix')
assert indicator.CONTEXT_PATH == 'prefix(val.value && val.value == obj.value)'
assert indicator.param == 'value'
assert indicator.value == 'test_value'
def test_custom_indicator_init_existing_type(self):
"""
Given: Data needed for creating a custom indicator
When: Type already exists
Then: raise a Value Error
"""
with pytest.raises(ValueError):
from CommonServerPython import Common, DBotScoreType
dbot_score = Common.DBotScore(
'test',
DBotScoreType.CUSTOM,
'VirusTotal',
score=Common.DBotScore.BAD,
malicious_description='malicious!'
)
Common.CustomIndicator('ip', 'test_value', dbot_score, {'param': 'value'}, 'prefix')
def test_custom_indicator_init_no_prefix(self):
"""
Given: Data needed for Custom indicator
When: Prefix provided is None
Then: Raise ValueError
"""
with pytest.raises(ValueError):
from CommonServerPython import Common, DBotScoreType
dbot_score = Common.DBotScore(
'test',
DBotScoreType.CUSTOM,
'VirusTotal',
score=Common.DBotScore.BAD,
malicious_description='malicious!'
)
Common.CustomIndicator('test', 'test_value', dbot_score, {'param': 'value'}, None)
def test_custom_indicator_init_no_dbot_score(self):
"""
Given: Data needed for Custom indicator
When: Dbotscore is not a DBotScore object
Then: Raise ValueError
"""
with pytest.raises(ValueError):
from CommonServerPython import Common
dbot_score = ''
Common.CustomIndicator('test', 'test_value', dbot_score, {'param': 'value'}, 'prefix')
def test_custom_indicator_to_context(self):
"""
Given: Data needed for Custom indicator
When: there's a call to to_context
Then: create a valid context
"""
from CommonServerPython import Common, DBotScoreType
dbot_score = Common.DBotScore(
'test',
DBotScoreType.CUSTOM,
'VirusTotal',
score=Common.DBotScore.BAD,
malicious_description='malicious!'
)
indicator = Common.CustomIndicator('test', 'test_value', dbot_score, {'param': 'value'}, 'prefix')
context = indicator.to_context()
assert context['DBotScore(val.Indicator &&'
' val.Indicator == obj.Indicator &&'
' val.Vendor == obj.Vendor && val.Type == obj.Type)']['Indicator'] == 'test'
assert context['prefix(val.value && val.value == obj.value)']['value'] == 'test_value'
assert context['prefix(val.value && val.value == obj.value)']['param'] == 'value'
def test_custom_indicator_no_params(self):
"""
Given: Data needed for creating a custom indicator
When: params are None
Then: Raise an error
"""
with pytest.raises(TypeError):
from CommonServerPython import Common, DBotScoreType
dbot_score = Common.DBotScore(
'test',
DBotScoreType.CUSTOM,
'VirusTotal',
score=Common.DBotScore.BAD,
malicious_description='malicious!'
)
Common.CustomIndicator('test', 'test_value', dbot_score, None, 'prefix')
def test_custom_indicator_no_value(self):
"""
Given: Data needed for creating a custom indicator
When: value is None
Then: Raise an error
"""
with pytest.raises(ValueError):
from CommonServerPython import Common, DBotScoreType
dbot_score = Common.DBotScore(
'test',
DBotScoreType.CUSTOM,
'VirusTotal',
score=Common.DBotScore.BAD,
malicious_description='malicious!'
)
Common.CustomIndicator('test', None, dbot_score, {'param': 'value'}, 'prefix')
@pytest.mark.parametrize(
"demistoUrls,expected_result",
[({'server': 'https://localhost:8443:/acc_test_tenant'}, 'acc_test_tenant'),
({'server': 'https://localhost:8443'}, '')])
def test_get_tenant_name(mocker, demistoUrls, expected_result):
"""
Given
- demistoUrls dictionary
When
- Running on multi tenant mode
- Running on single tenant mode
Then
- Return tenant account name if is multi tenant
"""
from CommonServerPython import get_tenant_account_name
mocker.patch.object(demisto, 'demistoUrls', return_value=demistoUrls)
result = get_tenant_account_name()
assert result == expected_result
IOCS = {'iocs': [{'id': '2323', 'value': 'google.com'},
{'id': '5942', 'value': '1.1.1.1'}]}
def test_indicators_value_to_clickable(mocker):
from CommonServerPython import indicators_value_to_clickable
from CommonServerPython import IndicatorsSearcher
mocker.patch.object(IndicatorsSearcher, '__next__', side_effect=[IOCS, StopIteration])
result = indicators_value_to_clickable(['1.1.1.1', 'google.com'])
assert result.get('1.1.1.1') == '[1.1.1.1](#/indicator/5942)'
assert result.get('google.com') == '[google.com](#/indicator/2323)'
def test_indicators_value_to_clickable_invalid(mocker):
from CommonServerPython import indicators_value_to_clickable
from CommonServerPython import IndicatorsSearcher
mocker.patch.object(IndicatorsSearcher, '__next__', side_effect=[StopIteration])
result = indicators_value_to_clickable(['8.8.8.8', 'abc.com'])
assert not result
result = indicators_value_to_clickable(None)
assert not result
def test_arg_to_number():
"""
Test if arg_to_number handles unicode object without failing.
"""
from CommonServerPython import arg_to_number
result = arg_to_number(u'1')
assert result == 1
def test_get_message_threads_dump():
from CommonServerPython import get_message_threads_dump
result = str(get_message_threads_dump(None, None))
assert ' Start Threads Dump ' in result
assert ' End Threads Dump ' in result
assert 'CommonServerPython.py' in result
assert 'get_message_threads_dump' in result
def test_get_message_memory_dump():
from CommonServerPython import get_message_memory_dump
result = str(get_message_memory_dump(None, None))
assert ' Start Variables Dump ' in result
assert ' Start Local Vars ' in result
assert ' End Local Vars ' in result
assert ' Start Top ' in result
assert ' Globals by Size ' in result
assert ' End Top ' in result
assert ' End Variables Dump ' in result
def test_shorten_string_for_printing():
from CommonServerPython import shorten_string_for_printing
assert shorten_string_for_printing(None, None) is None
assert shorten_string_for_printing('1', 9) == '1'
assert shorten_string_for_printing('123456789', 9) == '123456789'
assert shorten_string_for_printing('1234567890', 9) == '123...890'
assert shorten_string_for_printing('12345678901', 9) == '123...901'
assert shorten_string_for_printing('123456789012', 9) == '123...012'
assert shorten_string_for_printing('1234567890', 10) == '1234567890'
assert shorten_string_for_printing('12345678901', 10) == '1234...901'
assert shorten_string_for_printing('123456789012', 10) == '1234...012'
def test_get_size_of_object():
from CommonServerPython import get_size_of_object
class Object(object):
pass
level_3 = Object()
level_3.key3 = 'val3'
level_2 = Object()
level_2.key2 = 'val2'
level_2.child = level_3
level_1 = Object()
level_1.key1 = 'val1'
level_1.child = level_2
level_1_sys_size = sys.getsizeof(level_1)
level_1_deep_size = get_size_of_object(level_1)
# 3 levels, so shoulod be at least 3 times as large
assert level_1_deep_size > 3 * level_1_sys_size
class TestSetAndGetLastMirrorRun:
def test_get_last_mirror_run_in_6_6(self, mocker):
"""
Given: 6.6.0 environment and getLastMirrorRun returns results
When: Execute mirroring run
Then: Returning demisto.getLastRun object
"""
import demistomock as demisto
from CommonServerPython import get_last_mirror_run
mocker.patch('CommonServerPython.get_demisto_version', return_value={"version": "6.6.0"})
mocker.patch.object(demisto, 'getLastMirrorRun', return_value={"lastMirrorRun": "2018-10-24T14:13:20+00:00"})
result = get_last_mirror_run()
assert result == {"lastMirrorRun": "2018-10-24T14:13:20+00:00"}
def test_get_last_mirror_run_in_6_6_when_return_empty_results(self, mocker):
"""
Given: 6.6.0 environment and getLastMirrorRun returns empty results
When: Execute mirroring run
Then: Returning demisto.getLastRun empty object
"""
import demistomock as demisto
from CommonServerPython import get_last_mirror_run
mocker.patch('CommonServerPython.get_demisto_version', return_value={"version": "6.6.0"})
mocker.patch.object(demisto, 'getLastMirrorRun', return_value={})
result = get_last_mirror_run()
assert result == {}
def test_get_last_run_in_6_5(self, mocker):
"""
Given: 6.5.0 environment and getLastMirrorRun returns results
When: Execute mirroring run
Then: Get a string which represent we can't use this function
"""
import demistomock as demisto
from CommonServerPython import get_last_mirror_run
mocker.patch('CommonServerPython.get_demisto_version', return_value={"version": "6.5.0"})
get_last_run = mocker.patch.object(demisto, 'getLastMirrorRun')
with pytest.raises(DemistoException, match='You cannot use getLastMirrorRun as your version is below 6.6.0'):
get_last_mirror_run()
assert get_last_run.called is False
def test_set_mirror_last_run_in_6_6(self, mocker):
"""
Given: 6.6.0 environment
When: Execute mirroring run
Then: Using demisto.setLastMirrorRun to save results
"""
import demistomock as demisto
from CommonServerPython import set_last_mirror_run
mocker.patch('CommonServerPython.get_demisto_version', return_value={"version": "6.6.0"})
set_last_run = mocker.patch.object(demisto, 'setLastMirrorRun', return_value={})
set_last_mirror_run({"lastMirrorRun": "2018-10-24T14:13:20+00:00"})
set_last_run.assert_called_with({"lastMirrorRun": "2018-10-24T14:13:20+00:00"})
def test_set_mirror_last_run_in_6_5(self, mocker):
"""
Given: 6.5.0 environment
When: Execute mirroring run
Then: Don't use demisto.setLastMirrorRun
"""
import demistomock as demisto
from CommonServerPython import set_last_mirror_run
mocker.patch('CommonServerPython.get_demisto_version', return_value={"version": "6.5.0"})
set_last_run = mocker.patch.object(demisto, 'setLastMirrorRun', return_value={})
with pytest.raises(DemistoException, match='You cannot use setLastMirrorRun as your version is below 6.6.0'):
set_last_mirror_run({"lastMirrorRun": "2018-10-24T14:13:20+00:00"})
assert set_last_run.called is False
class TestFetchWithLookBack:
LAST_RUN = {}
INCIDENTS = [
{
'incident_id': 1,
'created': '2022-04-01T08:00:00'
},
{
'incident_id': 2,
'created': '2022-04-01T10:00:00'
},
{
'incident_id': 3,
'created': '2022-04-01T10:31:00'
},
{
'incident_id': 4,
'created': '2022-04-01T10:41:00'
},
{
'incident_id': 5,
'created': '2022-04-01T10:51:00'
}
]
NEW_INCIDENTS = [
{
'incident_id': 6,
'created': '2022-04-01T10:11:00'
},
{
'incident_id': 7,
'created': '2022-04-01T10:35:00'
},
{
'incident_id': 8,
'created': '2022-04-01T10:37:00'
}
]
INCIDENTS_TIME_AWARE = [
{
'incident_id': incident.get('incident_id'),
'created': incident.get('created') + 'Z'
} for incident in INCIDENTS
]
NEW_INCIDENTS_TIME_AWARE = [
{
'incident_id': incident.get('incident_id'),
'created': incident.get('created') + 'Z'
} for incident in NEW_INCIDENTS
]
def example_fetch_incidents(self, time_aware=False):
"""
An example fetch for testing
"""
from CommonServerPython import get_fetch_run_time_range, filter_incidents_by_duplicates_and_limit, \
update_last_run_object, arg_to_number
date_format = '%Y-%m-%dT%H:%M:%S' + ('Z' if time_aware else '')
incidents = []
params = demisto.params()
fetch_limit_param = params.get('limit')
look_back = arg_to_number(params.get('look_back', 0))
first_fetch = params.get('first_fetch')
time_zone = params.get('time_zone', 0)
last_run = demisto.getLastRun()
fetch_limit = last_run.get('limit') or fetch_limit_param
start_fetch_time, end_fetch_time = get_fetch_run_time_range(last_run=last_run, first_fetch=first_fetch,
look_back=look_back, timezone=time_zone,
date_format=date_format)
query = self.build_query(start_fetch_time, end_fetch_time, fetch_limit)
incidents_res = self.get_incidents_request(query, date_format)
incidents = filter_incidents_by_duplicates_and_limit(incidents_res=incidents_res, last_run=last_run,
fetch_limit=fetch_limit_param, id_field='incident_id')
last_run = update_last_run_object(last_run=last_run, incidents=incidents, fetch_limit=fetch_limit_param,
start_fetch_time=start_fetch_time,
end_fetch_time=end_fetch_time, look_back=look_back,
created_time_field='created', id_field='incident_id', date_format=date_format)
demisto.setLastRun(last_run)
return incidents
@staticmethod
def build_query(start_time, end_time, limit, return_incidents_by_limit=True):
query = {'from': start_time, 'to': end_time}
if return_incidents_by_limit:
query['limit'] = limit
return query
def get_incidents_request(self, query, date_format):
time_aware = 'Z' in date_format
source_incidents = self.INCIDENTS_TIME_AWARE if time_aware else self.INCIDENTS
from_time = datetime.strptime(query['from'], date_format)
incidents = [inc for inc in source_incidents if
datetime.strptime(inc['created'], date_format) > from_time]
if query.get('limit') is not None:
return incidents[:query['limit']]
return incidents
def set_last_run(self, new_last_run):
self.LAST_RUN = new_last_run
@pytest.mark.parametrize('params, result_phase1, result_phase2, expected_last_run', [
({'limit': 2, 'first_fetch': '40 minutes'}, [INCIDENTS[2], INCIDENTS[3]], [INCIDENTS[4]],
{'limit': 2, 'time': INCIDENTS[3]['created'], 'found_incident_ids': {3: 1667482800, 4: 1667482800}}),
({'limit': 2, 'first_fetch': '40 minutes', 'look_back': None}, [INCIDENTS[2], INCIDENTS[3]], [INCIDENTS[4]],
{'limit': 2, 'time': INCIDENTS[3]['created'], 'found_incident_ids': {3: 1667482800, 4: 1667482800}}),
({'limit': 3, 'first_fetch': '40 minutes'}, [INCIDENTS[2], INCIDENTS[3], INCIDENTS[4]], [],
{'limit': 3, 'time': INCIDENTS[4]['created'], 'found_incident_ids': {3: 1667482800, 4: 1667482800, 5: 1667482800}}),
({'limit': 2, 'first_fetch': '2 hours'}, [INCIDENTS[1], INCIDENTS[2]], [INCIDENTS[3], INCIDENTS[4]],
{'limit': 2, 'time': INCIDENTS[2]['created'], 'found_incident_ids': {2: 1667482800, 3: 1667482800}}),
({'limit': 3, 'first_fetch': '2 hours'}, [INCIDENTS[1], INCIDENTS[2], INCIDENTS[3]], [INCIDENTS[4]],
{'limit': 3, 'time': INCIDENTS[3]['created'], 'found_incident_ids': {2: 1667482800, 3: 1667482800, 4: 1667482800}}),
({'limit': 2, 'first_fetch': '40 minutes'}, [INCIDENTS_TIME_AWARE[2], INCIDENTS_TIME_AWARE[3]],
[INCIDENTS_TIME_AWARE[4]], {'limit': 2, 'time': INCIDENTS_TIME_AWARE[3]['created'],
'found_incident_ids': {3: 1667482800, 4: 1667482800}}),
({'limit': 3, 'first_fetch': '40 minutes'}, [INCIDENTS_TIME_AWARE[2], INCIDENTS_TIME_AWARE[3], INCIDENTS_TIME_AWARE[4]],
[],
{'limit': 3, 'time': INCIDENTS_TIME_AWARE[4]['created'],
'found_incident_ids': {3: 1667482800, 4: 1667482800, 5: 1667482800}}),
({'limit': 2, 'first_fetch': '2 hours'}, [INCIDENTS_TIME_AWARE[1], INCIDENTS_TIME_AWARE[2]], [INCIDENTS_TIME_AWARE[3],
INCIDENTS_TIME_AWARE[4]],
{'limit': 2, 'time': INCIDENTS_TIME_AWARE[2]['created'], 'found_incident_ids': {2: 1667482800, 3: 1667482800}}),
({'limit': 3, 'first_fetch': '2 hours'}, [INCIDENTS_TIME_AWARE[1], INCIDENTS_TIME_AWARE[2], INCIDENTS_TIME_AWARE[3]],
[INCIDENTS_TIME_AWARE[4]],
{'limit': 3, 'time': INCIDENTS_TIME_AWARE[3]['created'],
'found_incident_ids': {2: 1667482800, 3: 1667482800, 4: 1667482800}}),
])
@freeze_time("2022-11-03 13:40:00 UTC")
def test_regular_fetch(self, mocker, params, result_phase1, result_phase2, expected_last_run):
"""
Given:
- Connfiguration fetch parameters (incidents limit and first fetch time)
When:
- Running the example fetch incidents
Then:
- Ensure the return incidents and LastRun object as expected
"""
if sys.version_info.major == 2:
# skip for python 2 - date
assert True
return
time_aware = 'Z' in expected_last_run['time']
self.LAST_RUN = {}
mocker.patch.object(dateparser, 'parse', side_effect=self.mock_dateparser)
mocker.patch.object(demisto, 'params', return_value=params)
mocker.patch.object(demisto, 'getLastRun', return_value=self.LAST_RUN)
mocker.patch.object(demisto, 'setLastRun', side_effect=self.set_last_run)
# Run first fetch
incidents_phase1 = self.example_fetch_incidents(time_aware)
assert incidents_phase1 == result_phase1
assert self.LAST_RUN == expected_last_run
# Run second fetch
mocker.patch.object(demisto, 'getLastRun', return_value=self.LAST_RUN)
incidents_phase2 = self.example_fetch_incidents(time_aware)
assert incidents_phase2 == result_phase2
def mock_dateparser(self, date_string, settings):
time_aware = isinstance(date_string, str) and 'Z' in date_string
date_format = '%Y-%m-%dT%H:%M:%S' + ('Z' if time_aware else '')
date_arr = date_string.split(' ')
if len(date_arr) > 1 and date_arr[0].isdigit():
return datetime(2022, 4, 1, 11, 0, 0) - timedelta(minutes=int(date_arr[0])) if date_arr[1] == 'minutes' \
else datetime(2022, 4, 1, 11, 0, 0) - timedelta(hours=int(date_arr[0]))
return datetime(2022, 4, 1, 11, 0, 0) - (
datetime(2022, 4, 1, 11, 0, 0) - datetime.strptime(date_string, date_format))
@pytest.mark.parametrize(
'params, result_phase1, result_phase2, result_phase3, expected_last_run_phase1, expected_last_run_phase2, new_incidents, index',
[
(
{'limit': 2, 'first_fetch': '50 minutes', 'look_back': 15}, [INCIDENTS[2], INCIDENTS[3]],
[INCIDENTS[4]], [],
{'found_incident_ids': {3: '', 4: ''}, 'limit': 4},
{'found_incident_ids': {3: '', 4: '', 5: ''}, 'limit': 5},
[NEW_INCIDENTS[0]], 2
),
(
{'limit': 2, 'first_fetch': '20 minutes', 'look_back': 30}, [INCIDENTS[2], INCIDENTS[3]],
[NEW_INCIDENTS[1], NEW_INCIDENTS[2]], [INCIDENTS[4]],
{'found_incident_ids': {3: '', 4: ''}, 'limit': 4},
{'found_incident_ids': {3: '', 4: '', 7: '', 8: ''}, 'limit': 6},
[NEW_INCIDENTS[1], NEW_INCIDENTS[2]], 3
),
(
{'limit': 3, 'first_fetch': '181 minutes', 'look_back': 15},
[INCIDENTS[0], INCIDENTS[1], INCIDENTS[2]], [INCIDENTS[3], INCIDENTS[4]], [],
{'found_incident_ids': {1: '', 2: '', 3: ''}, 'limit': 6},
{'found_incident_ids': {1: '', 2: '', 3: '', 4: '', 5: ''}, 'limit': 8},
[NEW_INCIDENTS[0]], 2
),
(
{'limit': 3, 'first_fetch': '181 minutes', 'look_back': 30 * 60},
[INCIDENTS[0], INCIDENTS[1], INCIDENTS[2]], [NEW_INCIDENTS[0], INCIDENTS[3], INCIDENTS[4]], [],
{'found_incident_ids': {1: '', 2: '', 3: ''}, 'limit': 6},
{'found_incident_ids': {1: '', 2: '', 3: '', 4: '', 5: '', 6: ''}, 'limit': 9},
[NEW_INCIDENTS[0]], 2
),
(
{'limit': 3, 'first_fetch': '20 minutes', 'look_back': 30},
[INCIDENTS[2], INCIDENTS[3], INCIDENTS[4]], [NEW_INCIDENTS[1], NEW_INCIDENTS[2]], [],
{'found_incident_ids': {3: '', 4: '', 5: ''}, 'limit': 6},
{'found_incident_ids': {3: '', 4: '', 5: '', 7: '', 8: ''}, 'limit': 8},
[NEW_INCIDENTS[1], NEW_INCIDENTS[2]], 3
),
(
{'limit': 2, 'first_fetch': '50 minutes', 'look_back': 15}, [INCIDENTS_TIME_AWARE[2], INCIDENTS_TIME_AWARE[3]],
[INCIDENTS_TIME_AWARE[4]], [],
{'found_incident_ids': {3: '', 4: ''}, 'limit': 4},
{'found_incident_ids': {3: '', 4: '', 5: ''}, 'limit': 5},
[NEW_INCIDENTS_TIME_AWARE[0]], 2
),
(
{'limit': 2, 'first_fetch': '20 minutes', 'look_back': 30}, [INCIDENTS_TIME_AWARE[2], INCIDENTS_TIME_AWARE[3]],
[NEW_INCIDENTS_TIME_AWARE[1], NEW_INCIDENTS_TIME_AWARE[2]], [INCIDENTS_TIME_AWARE[4]],
{'found_incident_ids': {3: '', 4: ''}, 'limit': 4},
{'found_incident_ids': {3: '', 4: '', 7: '', 8: ''}, 'limit': 6},
[NEW_INCIDENTS_TIME_AWARE[1], NEW_INCIDENTS_TIME_AWARE[2]], 3
),
(
{'limit': 3, 'first_fetch': '181 minutes', 'look_back': 15},
[INCIDENTS_TIME_AWARE[0], INCIDENTS_TIME_AWARE[1], INCIDENTS_TIME_AWARE[2]], [INCIDENTS_TIME_AWARE[3],
INCIDENTS_TIME_AWARE[4]], [],
{'found_incident_ids': {1: '', 2: '', 3: ''}, 'limit': 6},
{'found_incident_ids': {1: '', 2: '', 3: '', 4: '', 5: ''}, 'limit': 8},
[NEW_INCIDENTS_TIME_AWARE[0]], 2
),
(
{'limit': 3, 'first_fetch': '20 minutes', 'look_back': 30},
[INCIDENTS_TIME_AWARE[2], INCIDENTS_TIME_AWARE[3], INCIDENTS_TIME_AWARE[4]],
[NEW_INCIDENTS_TIME_AWARE[1], NEW_INCIDENTS_TIME_AWARE[2]], [],
{'found_incident_ids': {3: '', 4: '', 5: ''}, 'limit': 6},
{'found_incident_ids': {3: '', 4: '', 5: '', 7: '', 8: ''}, 'limit': 8},
[NEW_INCIDENTS_TIME_AWARE[1], NEW_INCIDENTS_TIME_AWARE[2]], 3
),
])
def test_fetch_with_look_back(self, mocker, params, result_phase1, result_phase2, result_phase3,
expected_last_run_phase1, expected_last_run_phase2, new_incidents, index):
"""
Given:
- Connfiguration fetch parameters (incidents limit, first fetch time and look back)
When:
- Running the example fetch incidents and creating new incidents between fetch calles
Then:
- Ensure the return incidents and LastRun object as expected
"""
if sys.version_info.major == 2:
# skip for python 2 - date
assert True
return
time_aware = 'Z' in result_phase1[0]['created']
self.LAST_RUN = {}
incidents = self.INCIDENTS_TIME_AWARE[:] if time_aware else self.INCIDENTS[:]
mocker.patch.object(CommonServerPython, 'get_current_time', return_value=datetime(2022, 4, 1, 11, 0, 0))
mocker.patch.object(dateparser, 'parse', side_effect=self.mock_dateparser)
mocker.patch.object(demisto, 'params', return_value=params)
mocker.patch.object(demisto, 'getLastRun', return_value=self.LAST_RUN)
mocker.patch.object(demisto, 'setLastRun', side_effect=self.set_last_run)
# Run first fetch
incidents_phase1 = self.example_fetch_incidents(time_aware)
assert incidents_phase1 == result_phase1
assert self.LAST_RUN['limit'] == expected_last_run_phase1['limit']
assert self.LAST_RUN['found_incident_ids'].keys() == expected_last_run_phase1['found_incident_ids'].keys()
for inc in incidents_phase1:
assert inc['incident_id'] in self.LAST_RUN['found_incident_ids']
source_incidents = incidents[:index] + new_incidents + incidents[index:]
if time_aware:
self.INCIDENTS_TIME_AWARE = source_incidents
else:
self.INCIDENTS = source_incidents
# Run second fetch
mocker.patch.object(demisto, 'getLastRun', return_value=self.LAST_RUN)
incidents_phase2 = self.example_fetch_incidents(time_aware)
assert incidents_phase2 == result_phase2
assert self.LAST_RUN['limit'] == expected_last_run_phase2['limit']
assert self.LAST_RUN['found_incident_ids'].keys() == expected_last_run_phase2['found_incident_ids'].keys()
assert incidents_phase2[-1]['created'] == self.LAST_RUN['time']
for inc in incidents_phase2:
assert inc['incident_id'] in self.LAST_RUN['found_incident_ids']
# Run third fetch
mocker.patch.object(demisto, 'getLastRun', return_value=self.LAST_RUN)
incidents_phase3 = self.example_fetch_incidents(time_aware)
assert incidents_phase3 == result_phase3
# Remove new incidents from self.INCIDENTS
if time_aware:
self.INCIDENTS_TIME_AWARE = incidents
else:
self.INCIDENTS = incidents
@pytest.mark.parametrize(
'args1, expected_results1, args2, expected_results2, args3, expected_results3',
[
(
{
'incidents': [
{'createAt': '2022-04-01T10:11:00', 'id': '1'},
{'createAt': '2022-04-01T10:12:00', 'id': '2'},
{'createAt': '2022-04-01T10:13:00', 'id': '3'}
],
'fetch_limit': 3,
'start_fetch_time': '2022-04-01T10:11:00',
'end_fetch_time': '2022-04-05T10:11:00',
'look_back': 1,
'created_time_field': 'createAt',
'id_field': 'id',
'date_format': '%Y-%m-%dT%H:%M:%S',
'increase_last_run_time': True
},
{
'time': '2022-04-01T10:13:00',
'limit': 6,
'found_incident_ids': {'1': '', '2': '', '3': ''}
},
{
'incidents': [
{'createAt': '2022-04-02T10:11:00', 'id': '4'},
{'createAt': '2022-04-02T10:12:00', 'id': '5'},
{'createAt': '2022-04-02T10:13:00', 'id': '6'}
],
'fetch_limit': 3,
'start_fetch_time': '2022-04-01T10:11:00',
'end_fetch_time': '2022-04-06T10:11:00',
'look_back': 1,
'created_time_field': 'createAt',
'id_field': 'id',
'date_format': '%Y-%m-%dT%H:%M:%S',
'increase_last_run_time': True
},
{
'time': '2022-04-02T10:13:00',
'limit': 9,
'found_incident_ids': {'1': '', '2': '', '3': '',
'4': '', '5': '', '6': ''}
},
{
'incidents': [
{'createAt': '2022-04-03T10:11:00', 'id': '7'},
{'createAt': '2022-04-03T10:12:00', 'id': '8'},
{'createAt': '2022-04-03T10:13:00', 'id': '9'}
],
'fetch_limit': 3,
'start_fetch_time': '2022-04-01T10:11:00',
'end_fetch_time': '2022-04-07T10:11:00',
'look_back': 1,
'created_time_field': 'createAt',
'id_field': 'id',
'date_format': '%Y-%m-%dT%H:%M:%S',
'increase_last_run_time': True
},
{
'time': '2022-04-03T10:13:00',
'limit': 9,
'found_incident_ids': {'1': '', '2': '', '3': '',
'4': '', '5': '', '6': '',
'7': '', '8': '', '9': ''}
}
),
(
{
'incidents': [
{'createAt': '2022-04-01T10:11:00', 'id': '1'},
{'createAt': '2022-04-01T10:12:00', 'id': '2'},
{'createAt': '2022-04-01T10:13:00', 'id': '3'}
],
'fetch_limit': 3,
'start_fetch_time': '2022-04-01T10:11:00',
'end_fetch_time': '2022-04-05T10:11:00',
'look_back': 1,
'created_time_field': 'createAt',
'id_field': 'id',
'date_format': '%Y-%m-%dT%H:%M:%S',
'increase_last_run_time': True
},
{
'time': '2022-04-01T10:13:00',
'limit': 6,
'found_incident_ids': {'1': '', '2': '', '3': ''}
},
{
'incidents': [
{'createAt': '2022-04-02T10:11:00', 'id': '4'},
{'createAt': '2022-04-02T10:12:00', 'id': '5'},
],
'fetch_limit': 3,
'start_fetch_time': '2022-04-01T10:11:00',
'end_fetch_time': '2022-04-06T10:11:00',
'look_back': 1,
'created_time_field': 'createAt',
'id_field': 'id',
'date_format': '%Y-%m-%dT%H:%M:%S',
'increase_last_run_time': True
},
{
'time': '2022-04-02T10:12:00',
'limit': 8,
'found_incident_ids': {'4': '', '5': ''}
},
{
'incidents': [
{'createAt': '2022-04-03T10:11:00', 'id': '7'},
{'createAt': '2022-04-03T10:12:00', 'id': '8'},
{'createAt': '2022-04-03T10:13:00', 'id': '9'}
],
'fetch_limit': 3,
'start_fetch_time': '2022-04-02T10:12:00',
'end_fetch_time': '2022-04-07T10:11:00',
'look_back': 1,
'created_time_field': 'createAt',
'id_field': 'id',
'date_format': '%Y-%m-%dT%H:%M:%S',
'increase_last_run_time': True
},
{
'time': '2022-04-03T10:13:00',
'limit': 8,
'found_incident_ids': {'4': '', '5': '',
'7': '', '8': '', '9': ''}
}
),
(
{
'incidents': [
{'createAt': '2022-04-01T10:11:00', 'id': '1'},
{'createAt': '2022-04-01T10:12:00', 'id': '2'},
{'createAt': '2022-04-01T10:13:00', 'id': '3'}
],
'fetch_limit': 3,
'start_fetch_time': '2022-04-01T10:11:00',
'end_fetch_time': '2022-04-05T10:11:00',
'look_back': 1,
'created_time_field': 'createAt',
'id_field': 'id',
'date_format': '%Y-%m-%dT%H:%M:%S',
'increase_last_run_time': True
},
{
'time': '2022-04-01T10:13:00',
'limit': 6,
'found_incident_ids': {'1': '', '2': '', '3': ''}
},
{
'incidents': [],
'fetch_limit': 3,
'start_fetch_time': '2022-04-01T10:11:00',
'end_fetch_time': '2022-04-06T10:11:00',
'look_back': 1,
'created_time_field': 'createAt',
'id_field': 'id',
'date_format': '%Y-%m-%dT%H:%M:%S',
'increase_last_run_time': True
},
{
'time': '2022-04-06T10:11:00',
'limit': 6,
'found_incident_ids': {'1': '', '2': '', '3': ''}
},
{
'incidents': [],
'fetch_limit': 3,
'start_fetch_time': '2022-04-02T10:12:00',
'end_fetch_time': '2022-04-07T10:13:00',
'look_back': 1,
'created_time_field': 'createAt',
'id_field': 'id',
'date_format': '%Y-%m-%dT%H:%M:%S',
'increase_last_run_time': True
},
{
'time': '2022-04-07T10:13:00',
'limit': 6,
'found_incident_ids': {'1': '', '2': '', '3': ''}
}
)
]
)
def test_update_last_run_object(self, args1, expected_results1, args2, expected_results2, args3, expected_results3):
from CommonServerPython import update_last_run_object
args1.update({'last_run': {}})
results = update_last_run_object(**args1)
assert results.get('time') == expected_results1.get('time')
assert results.get('limit') == expected_results1.get('limit')
for id_ in results.get('found_incident_ids').keys():
assert id_ in expected_results1.get('found_incident_ids')
for id_ in results.get('found_incident_ids'):
results['found_incident_ids'][id_] = results['found_incident_ids'][id_] - 200
args2.update({'last_run': results})
results = update_last_run_object(**args2)
assert results.get('time') == expected_results2.get('time')
assert results.get('limit') == expected_results2.get('limit')
for id_ in results.get('found_incident_ids').keys():
assert id_ in expected_results2.get('found_incident_ids')
for id_ in results.get('found_incident_ids'):
results['found_incident_ids'][id_] = results['found_incident_ids'][id_] - 200
args3.update({'last_run': results})
results = update_last_run_object(**args3)
assert results.get('time') == expected_results3.get('time')
assert results.get('limit') == expected_results3.get('limit')
for id_ in results.get('found_incident_ids').keys():
assert id_ in expected_results3.get('found_incident_ids')
def test_lookback_with_offset_update_last_run(self):
"""
Given:
A last run
When:
Calling create_updated_last_run_object with a new offset to change
Then:
- The last run is updated with the new offset, and the start time remains as it was.
- When the offset needs to be reset, the last time is the latest incident time and the offset resets
"""
from CommonServerPython import create_updated_last_run_object
last_time = "2022-04-07T10:13:00"
last_run = {"time": last_time, "offset": 3}
new_offset = 4
new_last_run, _ = create_updated_last_run_object(last_run,
self.INCIDENTS,
fetch_limit=3,
look_back=1,
start_fetch_time=last_time,
end_fetch_time=datetime.now().isoformat(),
created_time_field="created",
new_offset=new_offset,
)
# make sure that the start time is unchanged because of the offset, and the offset is updated
assert new_last_run["offset"] == 4
assert new_last_run["time"] == last_time
last_run = {"time": last_time, "offset": new_offset}
new_offset = 0
new_last_run, _ = create_updated_last_run_object(last_run,
self.INCIDENTS,
fetch_limit=3,
look_back=1,
start_fetch_time=last_time,
end_fetch_time=datetime.now().isoformat(),
created_time_field="created",
new_offset=new_offset,
)
assert new_last_run["offset"] == 0
assert new_last_run["time"] == "2022-04-01T10:51:00"
def test_calculate_new_offset(self):
"""
Test that the new offset for the next run calculated correctly based on the old offset, number of incidents and total number of incidents.
The first argument is the old offset, the second is number of incidents and the third is the total number of incidents returned.
Given:
old offset, number of incidents, total number of incidents (could be None)
When:
Calculating a new offset to the next run
Then:
Make sure that the new offset is correct
"""
from CommonServerPython import calculate_new_offset
assert calculate_new_offset(0, 2, 4) == 2
assert calculate_new_offset(0, 2, 2) == 0
assert calculate_new_offset(0, 2, 3) == 2
assert calculate_new_offset(1, 2, 4) == 3
assert calculate_new_offset(1, 2, 3) == 0
assert calculate_new_offset(1, 2, None) == 3
def test_remove_old_incidents_ids(self):
"""
Test that the remove_old_incidents_ids function removes old incident IDs from the last run object.
Given:
A last run object with incident IDs and their addition times.
When:
Calling remove_old_incidents_ids with the last run object and a look back period.
Then:
Make sure that the old incident IDs are removed from the last run object and the latest incident IDs is returned.
"""
from CommonServerPython import remove_old_incidents_ids
assert remove_old_incidents_ids(
{"inc1": 1, "inc2": 2, "inc3": 3}, 1000, 1) == {"inc3": 3}
assert remove_old_incidents_ids(
{"inc1": 1, "inc2": 2, "inc3": 2}, 1000, 1) == {"inc2": 2, "inc3": 2}
class TestTracebackLineNumberAdgustment:
@staticmethod
def test_module_line_number_mapping():
from CommonServerPython import _MODULES_LINE_MAPPING
assert _MODULES_LINE_MAPPING['CommonServerPython']['start'] == 0
@staticmethod
def test_register_module_line_sanity():
"""
Given:
A module with a start and an end boundries.
When:
registering a module.
Then:
* module exists in the mapping with valid boundries.
"""
import CommonServerPython
CommonServerPython.register_module_line('Sanity', 'start', 5)
CommonServerPython.register_module_line('Sanity', 'end', 50)
assert CommonServerPython._MODULES_LINE_MAPPING['Sanity'] == {
'start': 5,
'start_wrapper': 5,
'end': 50,
'end_wrapper': 50,
}
@staticmethod
def test_register_module_line_single_boundry():
"""
Given:
* A module with only an end boundry.
* A module with only a start boundry.
When:
registering a module.
Then:
* both modules exists in the mapping.
* the missing boundry is 0 for start and infinity for end.
"""
import CommonServerPython
CommonServerPython.register_module_line('NoStart', 'end', 4)
CommonServerPython.register_module_line('NoEnd', 'start', 100)
assert CommonServerPython._MODULES_LINE_MAPPING['NoStart'] == {
'start': 0,
'start_wrapper': 0,
'end': 4,
'end_wrapper': 4,
}
assert CommonServerPython._MODULES_LINE_MAPPING['NoEnd'] == {
'start': 100,
'start_wrapper': 100,
'end': float('inf'),
'end_wrapper': float('inf'),
}
@staticmethod
def test_register_module_line_invalid_inputs():
"""
Given:
* invalid start_end flag.
* invalid line number.
When:
registering a module.
Then:
function exits quietly
"""
import CommonServerPython
CommonServerPython.register_module_line('Cactus', 'statr', 5)
CommonServerPython.register_module_line('Cactus', 'start', '5')
CommonServerPython.register_module_line('Cactus', 'statr', -5)
CommonServerPython.register_module_line('Cactus', 'statr', 0, -1)
@staticmethod
def test_fix_traceback_line_numbers():
import CommonServerPython
CommonServerPython._MODULES_LINE_MAPPING = {
'CommonServerPython': {'start': 200, 'end': 865, 'end_wrapper': 900},
'TestTracebackLines': {'start': 901, 'end': float('inf'), 'start_wrapper': 901},
'TestingApiModule': {'start': 1004, 'end': 1032, 'start_wrapper': 1001, 'end_wrapper': 1033},
}
traceback = '''Traceback (most recent call last):
File "", line 1043, in
File "", line 986, in main
File "", line 600, in func_wrapper
File "", line 1031, in api_module_call_script
File "", line 927, in call_func
Exception: WTF?!!!'''
expected_traceback = '''Traceback (most recent call last):
File "", line 110, in
File "", line 85, in main
File "", line 400, in func_wrapper
File "", line 27, in api_module_call_script
File "", line 26, in call_func
Exception: WTF?!!!'''
result = CommonServerPython.fix_traceback_line_numbers(traceback)
assert result == expected_traceback
TEST_CREATE_INDICATOR_RESULT_WITH_DBOTSCOR_UNKNOWN = [
(
{'indicator': 'f4dad67d0f0a8e53d87fc9506e81b76e043294da77ae50ce4e8f0482127e7c12',
'indicator_type': DBotScoreType.FILE, 'reliability': DBotScoreReliability.A},
{'instance': Common.File, 'indicator_type': 'SHA256', 'reliability': 'A - Completely reliable'}
),
(
{'indicator': 'd26cec10398f2b10202d23c966022dce', 'indicator_type': DBotScoreType.FILE,
'reliability': DBotScoreReliability.B},
{'instance': Common.File, 'indicator_type': 'MD5', 'reliability': 'B - Usually reliable'}
),
(
{'indicator': 'd26cec10398f2b10202d23c966022dce', 'indicator_type': DBotScoreType.FILE,
'reliability': DBotScoreReliability.B},
{'instance': Common.File, 'indicator_type': 'MD5', 'reliability': 'B - Usually reliable',
'integration_name': 'test'}
),
(
{'indicator': 'f4dad67d0f0a8e53d8*****937fc9506e81b76e043294da77ae50ce4e8f0482127e7c12',
'indicator_type': DBotScoreType.FILE, 'reliability': DBotScoreReliability.A},
{
'error_message': 'This indicator -> f4dad67d0f0a8e53d8*****937fc9506e81b76e043294da77ae50ce4e8f0482127e7c12 is incorrect'}
),
(
{'indicator': '8.8.8.8', 'indicator_type': DBotScoreType.IP},
{'instance': Common.IP, 'indicator_type': 'IP', 'reliability': None}
),
(
{'indicator': 'www.google.com', 'indicator_type': DBotScoreType.URL, 'reliability': DBotScoreReliability.A},
{'instance': Common.URL, 'indicator_type': 'URL', 'reliability': 'A - Completely reliable'}
),
(
{'indicator': 'google.com', 'indicator_type': DBotScoreType.DOMAIN},
{'instance': Common.Domain, 'indicator_type': 'DOMAIN', 'reliability': None}
),
(
{'indicator': 'test@test.com', 'indicator_type': DBotScoreType.ACCOUNT},
{'instance': Common.Account, 'indicator_type': 'ACCOUNT', 'reliability': None}
),
(
{'indicator': 'test@test.com', 'indicator_type': DBotScoreType.CRYPTOCURRENCY, 'address_type': 'bitcoin'},
{'instance': Common.Cryptocurrency, 'indicator_type': 'BITCOIN', 'reliability': None}
),
(
{'indicator': 'test@test.com', 'indicator_type': DBotScoreType.CERTIFICATE},
{'instance': Common.Certificate, 'indicator_type': 'CERTIFICATE', 'reliability': None}
),
(
{'indicator': 'test@test.com', 'indicator_type': 'test', 'context_prefix': 'test'},
{'instance': Common.CustomIndicator, 'indicator_type': 'TEST', 'reliability': None}
),
(
{'indicator': 'test@test.com', 'indicator_type': 'test'},
{'error_message': 'Indicator type is invalid'}
),
(
{'indicator': 'test@test.com', 'indicator_type': DBotScoreType.CRYPTOCURRENCY},
{'error_message': 'Missing address_type parameter'}
),
(
{'indicator': 'test@test.com', 'indicator_type': DBotScoreType.CVE},
{'error_message': 'DBotScoreType.CVE is unsupported'}
)
]
@pytest.mark.parametrize('args, expected', TEST_CREATE_INDICATOR_RESULT_WITH_DBOTSCOR_UNKNOWN)
def test_create_indicator_result_with_dbotscore_unknown(mocker, args, expected):
from CommonServerPython import create_indicator_result_with_dbotscore_unknown
if expected.get('integration_name'):
mocker.patch('CommonServerPython.Common.DBotScore',
return_value=Common.DBotScore(indicator=args['indicator'],
indicator_type=args['indicator_type'],
score=0,
integration_name=expected['integration_name'],
reliability=args['reliability'],
message='No results found.'))
try:
results = create_indicator_result_with_dbotscore_unknown(**args)
except ValueError as e:
assert str(e) == expected['error_message']
return
assert expected['indicator_type'] in results.readable_output
assert isinstance(results.indicator, expected['instance'])
assert results.indicator.dbot_score.score == 0
assert results.indicator.dbot_score.reliability == expected['reliability']
assert results.indicator.dbot_score.message == 'No results found.'
if expected.get('integration_name'):
assert expected['integration_name'] in results.readable_output
else:
assert 'Results:' in results.readable_output
@pytest.mark.parametrize('content_format,outputs,expected_type', ((None, {}, 'json'),
(None, 'foo', 'text'),
(None, 1, 'text'),
('html', '', 'html'),
('html', {}, 'html')))
def test_content_type(content_format, outputs, expected_type):
from CommonServerPython import CommandResults
command_results = CommandResults(
outputs=outputs,
readable_output='human_readable',
outputs_prefix='prefix',
content_format=content_format,
)
assert command_results.to_context()['ContentsFormat'] == expected_type
class TestSendEventsToXSIAMTest:
with open('test_data/events.json') as f:
test_data = json.load(f)
events_test_log_data = EVENTS_LOG_ERROR
assets_test_log_data = ASSETS_LOG_ERROR
orig_xsiam_file_size = 2 ** 20 # 1Mib
@staticmethod
def get_license_custom_field_mock(arg):
if 'token' in arg:
return "TOKEN"
elif 'url' in arg:
return "url"
@pytest.mark.parametrize('data_use_case, data_type', [
('json_events', 'events'),
('text_list_events', 'events'),
('text_events', 'events'),
('cef_events', 'events'),
('json_zero_events', 'events'),
('big_event', 'events'),
('json_assets', 'assets'),
])
def test_send_data_to_xsiam_positive(self, mocker, data_use_case, data_type):
"""
Test for the fetch events and fetch assets function
Given:
Case a: a list containing dicts representing events.
Case b: a list containing strings representing events.
Case c: a string representing events (separated by a new line).
Case d: a string representing events (separated by a new line).
Case e: an empty list of events.
Case f: a "big" event. a big event is bigger than XSIAM EVENT SIZE declared.
Case g: a list containing dicts representing assets.
( currently the Ideal event size is 1 Mib)
When:
Case a: Calling the send_assets_to_xsiam function with no explicit data format specified.
Case b: Calling the send_assets_to_xsiam function with no explicit data format specified.
Case c: Calling the send_assets_to_xsiam function with no explicit data format specified.
Case d: Calling the send_assets_to_xsiam function with a cef data format specification.
Case e: Calling the send_assets_to_xsiam function with no explicit data format specified.
Case f: Calling the send_assets_to_xsiam function with no explicit data format specified.
Case g: Calling the send_assets_to_xsiam function with no explicit data format specified.
Then ensure that:
Case a:
- The events data was compressed correctly
- The data format was automatically identified as json.
- The number of events reported to the module health equals to number of events sent to XSIAM - 2
Case b:
- The events data was compressed correctly
- The data format was automatically identified as text.
- The number of events reported to the module health equals to number of events sent to XSIAM - 2
Case c:
- The events data was compressed correctly
- The data format was automatically identified as text.
- The number of events reported to the module health equals to number of events sent to XSIAM - 2
Case d:
- The events data was compressed correctly
- The data format remained as cef.
- The number of events reported to the module health equals to number of events sent to XSIAM - 2
Case e:
- No request to XSIAM API was made.
- The number of events reported to the module health - 0.
Case f:
- The events data was compressed correctly. Expecting to see that last chunk sent.
- The data format remained as json.
- The number of events reported to the module health - 2. For the last chunk.
Case g:
- The assets data was compressed correctly
- The data format was automatically identified as json.
- The number of assets reported to the module health equals to number of assets sent to XSIAM - 2
"""
if not IS_PY3:
return
from CommonServerPython import BaseClient
from requests import Response
mocker.patch.object(demisto, 'getLicenseCustomField', side_effect=self.get_license_custom_field_mock)
mocker.patch.object(demisto, 'updateModuleHealth')
mocker.patch('time.time', return_value=123)
api_response = Response()
api_response.status_code = 200
api_response._content = json.dumps({'error': 'false'}).encode('utf-8')
_http_request_mock = mocker.patch.object(BaseClient, '_http_request', return_value=api_response)
items = self.test_data[data_use_case][data_type]
number_of_items = self.test_data[data_use_case]['number_of_events'] # pushed in each chunk.
chunk_size = self.test_data[data_use_case].get('XSIAM_FILE_SIZE', self.orig_xsiam_file_size)
data_format = self.test_data[data_use_case].get('format')
send_data_to_xsiam(data=items, vendor='some vendor', product='some product', data_format=data_format,
chunk_size=chunk_size, data_type=data_type)
if number_of_items:
expected_format = self.test_data[data_use_case]['expected_format']
expected_data = self.test_data[data_use_case]['expected_data']
arguments_called = _http_request_mock.call_args[1]
decompressed_data = gzip.decompress(arguments_called['data']).decode("utf-8")
assert arguments_called['headers']['format'] == expected_format
assert decompressed_data == expected_data
assert arguments_called['headers']['collector-type'] == data_type
else:
assert _http_request_mock.call_count == 0
if data_type == "events":
demisto.updateModuleHealth.assert_called_with({'eventsPulled': number_of_items})
elif data_type == "assets":
demisto.updateModuleHealth.assert_called_with({'assetsPulled': number_of_items})
assert arguments_called['headers']['snapshot-id'] == '123000'
assert arguments_called['headers']['total-items-count'] == '2'
@pytest.mark.parametrize('chunk_size', [2 ** 20, 50])
def test_send_data_to_xsiam_streaming_matches_legacy(self, mocker, chunk_size):
"""
Given: a list of dict events.
When: calling send_data_to_xsiam with use_streaming_send=False (legacy) and True (streaming),
including a small chunk_size to force multiple chunks.
Then: the union of decompressed lines sent to XSIAM is identical between the two paths
(same events, same JSON, same newline separation), and the reported count matches.
"""
if not IS_PY3:
return
from CommonServerPython import BaseClient
from requests import Response
mocker.patch.object(demisto, 'getLicenseCustomField', side_effect=self.get_license_custom_field_mock)
mocker.patch.object(demisto, 'updateModuleHealth')
mocker.patch.object(demisto, 'params', return_value={'url': 'some-url'})
api_response = Response()
api_response.status_code = 200
api_response._content = json.dumps({'error': 'false'}).encode('utf-8')
events = [{'id': i, 'msg': 'event number {}'.format(i)} for i in range(25)]
# legacy path
legacy_mock = mocker.patch.object(BaseClient, '_http_request', return_value=api_response)
send_data_to_xsiam(data=list(events), vendor='v', product='p', chunk_size=chunk_size,
data_type='events', use_streaming_send=False)
legacy_lines = []
for call in legacy_mock.call_args_list:
legacy_lines.extend(gzip.decompress(call[1]['data']).decode('utf-8').split('\n'))
# streaming path
streaming_mock = mocker.patch.object(BaseClient, '_http_request', return_value=api_response)
send_data_to_xsiam(data=list(events), vendor='v', product='p', chunk_size=chunk_size,
data_type='events', use_streaming_send=True)
streaming_lines = []
for call in streaming_mock.call_args_list:
streaming_lines.extend(gzip.decompress(call[1]['data']).decode('utf-8').split('\n'))
# same content (order-independent: same set of serialized events)
assert sorted(streaming_lines) == sorted(legacy_lines)
assert len(streaming_lines) == len(events)
# streaming reports the same total count to the health module
demisto.updateModuleHealth.assert_called_with({'eventsPulled': len(events)})
def test_send_data_to_xsiam_streaming_empty(self, mocker):
"""Streaming path with an empty list makes no HTTP call and reports 0."""
if not IS_PY3:
return
from CommonServerPython import BaseClient
mocker.patch.object(demisto, 'getLicenseCustomField', side_effect=self.get_license_custom_field_mock)
mocker.patch.object(demisto, 'updateModuleHealth')
mocker.patch.object(demisto, 'params', return_value={'url': 'some-url'})
http_mock = mocker.patch.object(BaseClient, '_http_request')
send_data_to_xsiam(data=[], vendor='v', product='p', data_type='events', use_streaming_send=True)
assert http_mock.call_count == 0
def test_send_data_to_xsiam_streaming_skips_oversized_entry_like_legacy(self, mocker):
"""
Given: a list of events where one single entry exceeds MAX_ALLOWED_ENTRY_SIZE.
When: calling send_data_to_xsiam with use_streaming_send=False (legacy) and True (streaming).
Then: both paths skip the oversized entry and send exactly the same remaining lines, so the
streaming path is as safe/reliable as the legacy path for this edge case.
"""
if not IS_PY3:
return
from CommonServerPython import BaseClient, MAX_ALLOWED_ENTRY_SIZE
from requests import Response
mocker.patch.object(demisto, 'getLicenseCustomField', side_effect=self.get_license_custom_field_mock)
mocker.patch.object(demisto, 'updateModuleHealth')
mocker.patch.object(demisto, 'params', return_value={'url': 'some-url'})
mocker.patch.object(demisto, 'error')
api_response = Response()
api_response.status_code = 200
api_response._content = json.dumps({'error': 'false'}).encode('utf-8')
# One oversized event (its serialized form exceeds MAX_ALLOWED_ENTRY_SIZE) between two normal events.
events = [
{'id': 0, 'msg': 'first'},
{'id': 1, 'blob': 'x' * (MAX_ALLOWED_ENTRY_SIZE + 1000)},
{'id': 2, 'msg': 'second'},
]
legacy_mock = mocker.patch.object(BaseClient, '_http_request', return_value=api_response)
send_data_to_xsiam(data=list(events), vendor='v', product='p',
data_type='events', use_streaming_send=False)
legacy_lines = []
for call in legacy_mock.call_args_list:
legacy_lines.extend(gzip.decompress(call[1]['data']).decode('utf-8').split('\n'))
streaming_mock = mocker.patch.object(BaseClient, '_http_request', return_value=api_response)
send_data_to_xsiam(data=list(events), vendor='v', product='p',
data_type='events', use_streaming_send=True)
streaming_lines = []
for call in streaming_mock.call_args_list:
streaming_lines.extend(gzip.decompress(call[1]['data']).decode('utf-8').split('\n'))
# Both paths drop the oversized event and keep the two normal events, identically.
assert sorted(streaming_lines) == sorted(legacy_lines)
assert len(streaming_lines) == 2
assert all('blob' not in line for line in streaming_lines)
@pytest.mark.parametrize('data_type, snapshot_id, items_count, expected', [
('assets', None, None, {'snapshot_id': '123000', 'items_count': '2'}),
('assets', '12345', 25, {'snapshot_id': '12345', 'items_count': '25'})
])
def test_send_data_to_xsiam_custom_snapshot_id_and_items_count(self, mocker, data_type, snapshot_id, items_count, expected):
"""
Test the send_data_to_xsiam with and without custom snapshot_id and items_count
Given:
Case a: no custom snapshot_id and items_count.
Case b: custom snapshot_id and items_count.
When:
Case a: Calling the send_assets_to_xsiam function without custom snapshot_id and items_count.
Case b: Calling the send_assets_to_xsiam function with custom snapshot_id and items_count.
Then ensure that:
Case a: The headers was set with the default data.
Case b: The headers was set with the custom data
"""
if not IS_PY3:
return
from CommonServerPython import BaseClient
from requests import Response
mocker.patch.object(demisto, 'getLicenseCustomField', side_effect=self.get_license_custom_field_mock)
mocker.patch.object(demisto, 'updateModuleHealth')
mocker.patch('time.time', return_value=123)
api_response = Response()
api_response.status_code = 200
api_response._content = json.dumps({'error': 'false'}).encode('utf-8')
_http_request_mock = mocker.patch.object(BaseClient, '_http_request', return_value=api_response)
items = self.test_data['json_assets'][data_type]
send_data_to_xsiam(data=items, vendor='some vendor', product='some product', data_type=data_type, snapshot_id=snapshot_id,
items_count=items_count)
arguments_called = _http_request_mock.call_args[1]
assert arguments_called['headers']['collector-type'] == data_type
assert arguments_called['headers']['snapshot-id'] == expected['snapshot_id']
assert arguments_called['headers']['total-items-count'] == expected['items_count']
@pytest.mark.parametrize('error_msg, data_type', [(None, "events"), ({'error': 'error'}, "events"), ('', "events"),
({'error': 'error'}, "assets")])
def test_send_data_to_xsiam_error_handling(self, mocker, requests_mock, error_msg, data_type):
"""
Given:
case a: response type containing None
case b: response type containing json
case c: response type containing empty string
When:
calling the send_data_to_xsiam function
Then:
case a:
- DemistoException is raised with the empty response message
- Error log is created with the empty response message and status code of 403
- Make sure only single api request was sent and that retry mechanism was not triggered
case b:
- DemistoException is raised with the Unauthorized[401] message
- Error log is created with Unauthorized[401] message and status code of 401
- Make sure only single api request was sent and that retry mechanism was not triggered
case c:
- DemistoException is raised with the empty response message
- Error log is created with the empty response message and status code of 403
- Make sure only single api request was sent and that retry mechanism was not triggered
"""
if not IS_PY3:
return
mocker.patch.object(demisto, "params", return_value={"url": "www.test_url.com"})
mocker.patch.object(demisto, "callingContext", {"context": {"IntegrationInstance": "test_integration_instance",
"IntegrationBrand": "test_brand"}})
mocker.patch('time.time', return_value=123)
if isinstance(error_msg, dict):
status_code = 401
request_mocker = requests_mock.post(
'https://api-url/logs/v1/xsiam', json=error_msg, status_code=status_code, reason='Unauthorized[401]'
)
expected_error_msg = 'Unauthorized[401]'
else:
status_code = 403
request_mocker = requests_mock.post('https://api-url/logs/v1/xsiam', text=None, status_code=status_code)
expected_error_msg = 'Received empty response from the server'
mocker.patch.object(demisto, 'getLicenseCustomField', side_effect=self.get_license_custom_field_mock)
mocker.patch.object(demisto, 'updateModuleHealth')
error_log_mocker = mocker.patch.object(demisto, 'error')
events = self.test_data['json_events']['events']
expected_request_and_response_info = self.events_test_log_data if data_type == "events" else self.assets_test_log_data
expected_error_header = 'Error sending new {data_type} into XSIAM.\n'.format(data_type=data_type)
with pytest.raises(
DemistoException,
match=re.escape(expected_error_header + expected_error_msg),
):
send_data_to_xsiam(data=events, vendor='some vendor', product='some product', data_type=data_type)
# make sure the request was sent only once and retry mechanism was not triggered
assert request_mocker.call_count == 1
error_log_mocker.assert_called_with(
expected_request_and_response_info.format(status_code=str(status_code), error_received=expected_error_msg))
@pytest.mark.parametrize(
'mocked_responses, expected_request_call_count, expected_error_log_count, should_succeed', [
(
[
(429, None), (429, None), (429, None)
],
3,
1,
False
),
(
[
(401, None)
],
1,
1,
False
),
(
[
(429, None), (429, None), (200, json.dumps({'error': 'false'}).encode('utf-8'))
],
3,
0,
True
),
(
[
(429, None), (200, json.dumps({'error': 'false'}).encode('utf-8'))
],
2,
0,
True
),
(
[
(200, json.dumps({'error': 'false'}).encode('utf-8'))
],
1,
0,
True
)
]
)
def test_retries_send_data_to_xsiam_rate_limit(
self, mocker, mocked_responses, expected_request_call_count, expected_error_log_count, should_succeed
):
"""
Given:
case a: 3 responses indicating about api limit from xsiam (429)
case b: 2 responses indicating about unauthorized access from xsiam (401)
case c: 2 responses indicating about api limit from xsiam (429) and the third indicating about success
case d: 1 response indicating about api limit from xsiam (429) and the second indicating about success
case e: 1 response indicating about success from xsiam with no rate limit errors
When:
calling the send_data_to_xsiam function
Then:
case a:
- DemistoException is raised
- Error log is called 1 time
- Make sure 3 api requests were sent by the retry mechanism
case b:
- DemistoException is raised
- Error log is called 1 time
- Make sure only 1 api request were sent by the retry mechanism
case c:
- Error log is not called at all
- Make sure only 3 api requests were sent by the retry mechanism
case d:
- EError log is not called at all
- Make sure only 2 api requests were sent by the retry mechanism
case e:
- Error log is not called at all
- Make sure only 1 api request were sent by the retry mechanism
"""
if not IS_PY3:
return
import requests
mocked_responses_side_effect = []
for status_code, text in mocked_responses:
api_response = requests.Response()
api_response.status_code = status_code
api_response._content = text
mocked_responses_side_effect.append(api_response)
request_mock = mocker.patch.object(requests.Session, 'request', side_effect=mocked_responses_side_effect)
mocker.patch.object(demisto, 'getLicenseCustomField', side_effect=self.get_license_custom_field_mock)
mocker.patch.object(demisto, 'updateModuleHealth')
error_mock = mocker.patch.object(demisto, 'error')
events = self.test_data['json_events']['events']
if should_succeed:
send_data_to_xsiam(data=events, vendor='some vendor', product='some product')
else:
with pytest.raises(DemistoException):
send_data_to_xsiam(data=events, vendor='some vendor', product='some product')
assert error_mock.call_count == expected_error_log_count
assert request_mock.call_count == expected_request_call_count
class TestIsMetricsSupportedByServer:
@classmethod
@pytest.fixture(scope='function', autouse=True)
def clear_cache(cls):
get_demisto_version._version = None
def test_metrics_supported(self, mocker):
"""
Given: An XSOAR server running version 6.8.0
When: Testing that a server supports ExecutionMetrics
Then: Assert that is_supported reports True
"""
from CommonServerPython import ExecutionMetrics
mocker.patch.object(
demisto,
'demistoVersion',
return_value={
'version': '6.8.0',
'buildNumber': '50000'
}
)
mock_metrics = ExecutionMetrics()
# XSOAR version is 7.0.0 and should be supported. Assert that it is.
assert mock_metrics.is_supported() is True
def test_metrics_are_not_supported(self, mocker):
"""
Given: An XSOAR server running version 1.0.0
When: Testing that a server does not support ExecutionMetrics
Then: Assert that is_supported reports False
"""
from CommonServerPython import ExecutionMetrics
# XSOAR version is not supported.
mocker.patch.object(
demisto,
'demistoVersion',
return_value={
'version': '1.0.0',
'buildNumber': '50000'
}
)
mock_metrics = ExecutionMetrics()
# XSOAR version is 1.0.0 and should not be supported. Assert that it isn't.
assert mock_metrics.is_supported() is False
def test_collect_execution_metrics():
"""
Given:
An ExecutionMetrics object -
Case 1 - Reports a successful metric
Case 2 - Reports a quota error metric
Case 3 - Reports multiple quota error metrics
When:
Case 1 - Testing that a success metric has been reported
Case 2 - Testing that a quota metric has been reported
Case 3 - Testing that multiple quota errors have been reported
Then:
Case 1 - Assert that there is one successful metric and that the entry is an ExecutionMetrics entry
Case 2 - Assert that there is a quota error added to the metric report and is contained in the ExecutionMetrics entry
Case 3 - Assert that there are 26 total quota errors that are contained in the ExecutionMetrics entry
"""
from CommonServerPython import ExecutionMetrics
mock_metrics = ExecutionMetrics()
# Report Successful Metrics
mock_metrics.success += 1
# Collect Metrics
collected_metrics = mock_metrics.metrics
expected_command_results = {'APIExecutionMetrics': [{'APICallsCount': 1, 'Type': 'Successful'}],
'Contents': 'Metrics reported successfully.',
'ContentsFormat': 'text',
'EntryContext': {},
'HumanReadable': None,
'IgnoreAutoExtract': False,
'IndicatorTimeline': [],
'Note': False,
'Relationships': [],
'Type': 19}
# Assert collected metrics are correct
assert collected_metrics.to_context() == expected_command_results
# Report Quota Error
mock_metrics.quota_error += 1
# Update Test Bank
expected_command_results['APIExecutionMetrics'].append({'APICallsCount': 1, 'Type': 'QuotaError'})
# Assert collected metrics are correct
assert collected_metrics.to_context() == expected_command_results
# Report multiple metrics
mock_metrics.quota_error += 25
# Update Test Bank
expected_command_results['APIExecutionMetrics'][1]['APICallsCount'] = 26
# Assert collected metrics are correct
assert collected_metrics.to_context() == expected_command_results
def test_is_scheduled_command_retry(mocker):
"""
Given:
Test Case 1 - A command's metadata indicates it is scheduled.
Test Case 2 - A command's metadata indicates it is not scheduled.
When:
Test Case 1 - Checking if a command is scheduled or not.
Test Case 2 - Checking if a command is scheduled or not.
Then:
Test Case 1 - Assert the function returns True
Test Case 2 - Assert the function returns False
"""
from CommonServerPython import is_scheduled_command_retry
mock_scheduled_command = {
'polling': True,
'pollingCommand': 'SomeCommand',
'pollingArgs': {'some': 'args'},
'timesRan': 0,
'startDate': '5.4.2022',
'endingDate': '1.2.2022'
}
mocker.patch.dict(demisto.callingContext, {'context': {'ParentEntry': mock_scheduled_command}})
mocker.patch.object(CommonServerPython, 'get_integration_name', return_value='')
# The run should be considered a scheduled command
assert is_scheduled_command_retry() is True
# Change run to not be a scheduled command
mock_scheduled_command['polling'] = False
# The run should not be considered a scheduled command
assert is_scheduled_command_retry() is False
def test_append_metrics(mocker):
"""
Given: CommandResults list and Execution_metrics object to be added to the list.
When: Metrics need to be added after reputation commands ran.
Then: Metrics added as the last object of the list.
"""
mocker.patch.object(ExecutionMetrics, 'is_supported', return_value=True)
metrics = ExecutionMetrics()
results = []
metrics.success += 1
results = CommonServerPython.append_metrics(metrics, results)
assert len(results) == 1
def test_convert_dict_values_bytes_to_str():
"""
Given:
Dictionary contains bytes objects
When:
Creating outputs for commands
Then:
assert all bytes objects have been converted to strings
"""
input_dict = {'some_key': b'some_value',
'some_key1': [b'some_value'],
'some_key2': {'some_key': [b'some_value'],
'some_key1': b'some_value'}
}
expected_output_dict = {'some_key': 'some_value',
'some_key1': ['some_value'],
'some_key2': {'some_key': ['some_value'],
'some_key1': 'some_value'}
}
actual_output = convert_dict_values_bytes_to_str(input_dict)
assert actual_output == expected_output_dict
@pytest.mark.parametrize(
'filename',
['/test', '\\test', ',test', ':test', 't/est.pdf', '../../test.xslx', '~test.png']
)
def test_is_valid_filename_faild(filename):
"""
Given:
Filename.
When:
Checking if the filename is invalid
Then:
Test - Assert the function returns Exception
"""
assert is_filename_valid(filename=filename) is False
@pytest.mark.parametrize(
'filename',
['test', 'test.txt', 'test.xslx', 'Test', 'טסט', 'test-test.pdf', 'test test.md']
)
def test_is_valid_filename(filename):
"""
Given:
Filename.
When:
Checking if the filename is invalid
Then:
Test - Assert the function does not raise an Exception
"""
assert is_filename_valid(filename)
TEST_REPLACE_SPACES_IN_CREDENTIAL = [
(
'TEST test TEST', 'TEST test TEST'
),
(
'-----BEGIN SSH CERTIFICATE----- MIIF7z gdwZcx IENpdH -----END SSH CERTIFICATE-----',
'-----BEGIN SSH CERTIFICATE-----\nMIIF7z\ngdwZcx\nIENpdH\n-----END SSH CERTIFICATE-----'
),
(
'-----BEGIN RSA PRIVATE KEY----- MIIF7z gdwZcx IENpdH -----END RSA PRIVATE KEY-----',
'-----BEGIN RSA PRIVATE KEY-----\nMIIF7z\ngdwZcx\nIENpdH\n-----END RSA PRIVATE KEY-----'
),
(
'-----BEGIN RSA PRIVATE KEY----- MIIF7z gdwZcx IENpdH',
'-----BEGIN RSA PRIVATE KEY----- MIIF7z gdwZcx IENpdH'
),
(
None, None
),
(
'', ''
)
]
@pytest.mark.parametrize('credential, expected', TEST_REPLACE_SPACES_IN_CREDENTIAL)
def test_replace_spaces_in_credential(credential, expected):
"""
Given:
Credential with spaces.
When:
Running replace_spaces_in_credential function.
Then:
Test - Assert the function not returning as expected.
"""
from CommonServerPython import replace_spaces_in_credential
result = replace_spaces_in_credential(credential)
assert result == expected
TEST_RESPONSE_TO_CONTEXT_DATA = [
(
{"id": "111"}, {"ID": "111"}, {}
),
(
{"test": [1]}, {"Test": [1]}, {}
),
(
{"test1": [{'test2': "val"}]}, {"Test1": [{'Test2': "val"}]}, {}
),
(
{"test1": {'test2': "val"}}, {"Test1": {'Test2': "val"}}, {}
),
(
[{"test1": {'test2': "val"}}], [{"Test1": {'Test2': "val"}}], {}
),
(
"test", "test", {}
),
(
{"test_func": "test"}, {"TestFunc": "test"}, {}
),
(
{"testid": "test"}, {"TestID": "test"}, {"testid": "TestID"}
),
(
{"testid": "test", "id": "test_id", "test": "test_val"}, {"TestID": "test", "ID": "test_id", "Test": "test_val"},
{"testid": "TestID"}
)
]
@pytest.mark.parametrize('response, expected_results, user_predefiend_keys', TEST_RESPONSE_TO_CONTEXT_DATA)
def test_response_to_context(response, expected_results, user_predefiend_keys):
"""
Given:
A response and user_predefiend_keys dict.
Case 1: a response dict with a key "id".
Case 2: a response dict with a list as a value.
Case 3: a response dict with a list of dicts as a value.
Case 4: a response dict with a dict as a value.
Case 5: a response list.
Case 6: a response string.
Case 7: a response dict with a key with underscore.
Case 8: a response dict and a user_predefiend_keys dict,
where the key of the response dict is in the user_predefiend_keys dict.
Case 9: a response dict with 3 keys and a user_predefiend_keys dict,
where one key of the response dict is in the user_predefiend_keys dict.
where one key of the response dict is in the predefined_keys dict.
where one key of the response is not in any predefined dict.
When:
Running response_to_context function.
Then:
Test - Assert the function created the dict formatted succesfuly.
Case 1: Should transfom key to "ID".
Case 2: Should attempt to transform only the dict key.
Case 3: Should attempt to transform only the dict inside the list.
Case 4: Should attempt to transform both the given dict key and the keys of the nested dict.
Case 5: Should modify the dict inside the list.
Case 6: Should return the input as is.
Case 7: Should remove the underscore and capitalize the first letters of both words.
Case 8: Should change the key according to the user_predefiend_keys dict.
Case 9: Should change the first key according to the user_predefiend_keys dict,
the second key according to predefined_keys, and the third regularly.
"""
assert response_to_context(response, user_predefiend_keys) == expected_results
class TestIsIntegrationCommandExecution:
def test_with_script_exec(self, mocker):
mocker.patch.object(demisto, 'callingContext', {'context': {'ExecutedCommands': [{'moduleBrand': 'Scripts'}]}})
assert is_integration_command_execution() == False
def test_with_integration_exec(self, mocker):
mocker.patch.object(demisto, 'callingContext', {'context': {'ExecutedCommands': [{'moduleBrand': 'some-integration'}]}})
assert is_integration_command_execution() == True
data_test_problematic_cases = [
None, 1, [], {}, {'context': {}}, {'context': {'ExecutedCommands': None}},
{'context': {'ExecutedCommands': []}}, {'context': {'ExecutedCommands': [None]}},
{'context': {'ExecutedCommands': [{}]}}
]
@pytest.mark.parametrize('calling_context_mock', data_test_problematic_cases)
def test_problematic_cases(self, mocker, calling_context_mock):
mocker.patch.object(demisto, 'callingContext', calling_context_mock)
assert is_integration_command_execution() == True
@pytest.mark.parametrize("timestamp_str, seconds_threshold, expected", [
("2019-01-01T00:00:00Z", 60, True),
("2022-01-01T00:00:00GMT+1", 60, True),
("2022-01-01T00:00:00Z", 60, False),
("invalid", 60, ValueError)
])
def test_has_passed_time_threshold__different_timestamps(timestamp_str, seconds_threshold, expected, mocker):
"""
Given:
A timestamp string and a seconds threshold.
When:
Running has_passed_time_threshold function.
Then:
Test - Assert the function returns the expected result.
Case 1: The timestamp is in the past.
Case 2: Though the timestamp appears identical, it is in a different timezone, so the time passed the threshold.
Case 3: The timestamp did not pass the threshold.
Case 4: The timestamp is invalid.
"""
from CommonServerPython import has_passed_time_threshold
mocker.patch('CommonServerPython.datetime', autospec=True)
mocker.patch.object(CommonServerPython.datetime, 'now', return_value=datetime(2022, 1, 1, 0, 0, 0, tzinfo=pytz.utc))
if expected == ValueError:
with pytest.raises(expected) as e:
has_passed_time_threshold(timestamp_str, seconds_threshold)
assert str(e.value) == "Failed to parse timestamp: invalid"
else:
assert has_passed_time_threshold(timestamp_str, seconds_threshold) == expected
@pytest.mark.parametrize("indicator,expected_result", [
("e61fcc6a06420106fa6642ef833b9c38", "md5"),
("3fec1b14cea32bbcd97fad4507b06888", "md5"),
("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "sha256"),
("bb8098f4627441f6a29c31757c45339c74b2712b92783173df9ab58d47ae3bfa", "sha256"),
("193:iAklVz3fzvBk5oFblLPBN1iXf2bCRErwyN4aEbwyiNwyiQwNeDAi4XMG:iAklVzfzvBTFblLpN1iXOYpyuapyiWym", "ssdeep"),
("3:Wg8oEIjOH9+KS3qvRBTdRi690oVqzBUGyT0/n:Vx0HgKnTdE6eoVafY8", "ssdeep"),
("1ff8be1766d9e16b0b651f89001e8e7375c9e71f", "sha1"),
("6c5360d41bd2b14b1565f5b18e5c203cf512e493", "sha1"),
(
"eaf7542ade2c338d8d2cc76fcbf883e62c31336e60cb236f86ed66c8154ea9fb836fd88367880911529bdafed0e76cd34272123a4d656db61b120b95eaa3e069",
"sha512"),
(
"a7c19471fb4f2b752024246c28a37127ea7475148c04ace743392334d0ecc4762baf30b892d6a24b335e1065b254166f905fc46cc3ba5dba89e757bb7023a211",
"sha512"),
("@", None)
])
def test_detect_file_indicator_type(indicator, expected_result):
"""
Given:
An indicator string.
When:
Running detect_file_indicator_type function.
Then:
Test - Assert the function returns the expected result.
Case 1: md5 indicator type.
Case 2: sha256 indicator type.
Case 3: ssdeep indicator type.
Case 4: sha1 indicator type.
Case 5: sha512 indicator type.
Case 6: invalid type.
"""
from CommonServerPython import detect_file_indicator_type
assert detect_file_indicator_type(indicator) == expected_result
def test_create_clickable_url():
"""
Given:
One URL and one text.
When:
Running create_clickable_url function.
Then:
Assert the function returns the expected result.
A URL with different text than the link.
"""
from CommonServerPython import create_clickable_url
assert create_clickable_url('https://example.com', 'click here') == '[click here](https://example.com)'
def test_create_clickable_url_one_url_without_text():
"""
Given:
One URL.
When:
Running create_clickable_url function.
Then:
Assert the function returns the expected result.
A clickable URL with the same text as the link.
"""
from CommonServerPython import create_clickable_url
assert create_clickable_url('https://example.com', None) == '[https://example.com](https://example.com)'
def test_create_clickable_url_list_of_urls_with_list_of_text():
"""
Given:
A list of URLs and a list of texts.
When:
Running create_clickable_url function.
Then:
Assert the function returns the expected result.
A list of URLs with different texts than the links.
"""
from CommonServerPython import create_clickable_url
expected = ['[click here1](https://example1.com)', '[click here2](https://example2.com)']
assert create_clickable_url(['https://example1.com', 'https://example2.com'], ['click here1', 'click here2']) == expected
def test_create_clickable_url_list_of_urls_without_text():
"""
Given:
A list of URLs without text.
When:
Running create_clickable_url function.
Then:
Assert the function returns the expected result.
A list URLs without texts as the links.
"""
from CommonServerPython import create_clickable_url
expected = ['[https://example1.com](https://example1.com)', '[https://example2.com](https://example2.com)']
assert create_clickable_url(['https://example1.com', 'https://example2.com'], None) == expected
def test_create_clickable_test_wrong_text_value():
"""
Given:
A list of links and texts (not in teh same length).
When:
Running create_clickable_url function.
Then:
Assert the function returns the expected error.
"""
from CommonServerPython import create_clickable_url
with pytest.raises(AssertionError) as e:
assert create_clickable_url(['https://example1.com', 'https://example2.com'], ['click here1'])
assert e.type == AssertionError
assert 'The URL list and the text list must be the same length.' in e.value.args
@pytest.mark.parametrize("request_log, expected_output", [
(
"send: b'GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\nmy_authorization: Bearer token123\\r\\n'",
"send: b'GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\nmy_authorization: Bearer \\r\\n'"
),
(
"send: b'GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\nSet_Cookie: session_id=123\\r\\n'",
"send: b'GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\nSet_Cookie: \\r\\n'"
),
(
"send: b'GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\nAuthorization: token123\\r\\n'",
"send: b'GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\nAuthorization: \\r\\n'"
),
(
"GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\nAuthorization: Bearer token123\\r\\n",
"GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\nAuthorization: Bearer \\r\\n"
),
(
"GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\nAuthorization: JWT token123\\r\\n",
"GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\nAuthorization: JWT \\r\\n"
),
(
"send: b'GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\nAuthorization: LOG token:signature=\\r\\n'",
"send: b'GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\nAuthorization: LOG \\r\\n'"
),
(
"send: b'GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\n'",
str("send: b'GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\n'")
),
(
"send: b'GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\apiKey: 1234\\r\\n'",
"send: b'GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\apiKey: \\r\\n'"
),
(
"send: b'GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\credentials: {'good':'day'}\\r\\n'",
"send: b'GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\credentials: \\r\\n'"
),
(
"send: b'GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\client_name: client\\r\\n'",
"send: b'GET /api/v1/users HTTP/1.1\\r\\nHost: example.com\\r\\client_name: \\r\\n'"
),],
ids=["Bearer", "Cookie", "Authorization", "Bearer", "JWT", "LOG prefix", "No change", "Key", "credential", "client"],)
def test_censor_request_logs(request_log, expected_output):
"""
Given:
A request log.
case 1: A request log with a sensitive data under the 'Authorization' header, but the 'Authorization' is not capitalized and within a string.
case 2: A request log with a sensitive data under the 'Cookie' header, but with a 'Set_Cookie' prefix.
case 3: A request log with a sensitive data under the 'Authorization' header, but with no 'Bearer' prefix.
case 4: A request log with a sensitive data under the 'Authorization' header, but with no 'send b' prefix at the beginning.
case 5: A request log with no sensitive data.
case 6: A request log with a sensitive data under the 'Authorization' header, with a "LOG" prefix (which used in cases
like HMAC signature authentication).
When:
Running censor_request_logs function.
Then:
Assert the function returns the exactly same log with the sensitive data masked.
"""
assert censor_request_logs(request_log) == expected_output
@pytest.mark.parametrize("request_log", [
('send: hello\n'),
('header: Authorization\n')
])
def test_logger_write__censor_request_logs_has_been_called(mocker, request_log):
"""
Given:
A request log that starts with 'send' or 'header' that may contains sensitive data.
When:
Running logger.write function when using debug-mode.
Then:
Assert the censor_request_logs function has been called.
"""
mocker.patch.object(demisto, 'params', return_value={
'credentials': {'password': 'my_password'},
})
mocker.patch.object(demisto, 'info')
mocker.patch('CommonServerPython.is_debug_mode', return_value=True)
mock_censor = mocker.patch('CommonServerPython.censor_request_logs')
mocker.patch('CommonServerPython.IntegrationLogger.build_curl')
ilog = IntegrationLogger()
ilog.set_buffering(False)
ilog.write(request_log)
assert mock_censor.call_count == 1
def test_replace_send_preffix(mocker):
"""
Given:
- A string that contains 'send: b"' in it.
When:
- The write function is called to add this string to the logs.
Then:
- Verify that the text 'send: b"' has been replaced with "send: b'" to standardize the log format for easier log handling.
"""
mocker.patch.object(demisto, 'params', return_value={
'credentials': {'password': 'my_password'},
})
mocker.patch.object(demisto, 'info')
mocker.patch('CommonServerPython.is_debug_mode', return_value=True)
mock_censor = mocker.patch('CommonServerPython.censor_request_logs')
mocker.patch('CommonServerPython.IntegrationLogger.build_curl')
ilog = IntegrationLogger()
ilog.set_buffering(False)
ilog.write('send: b"hello\n')
assert mock_censor.call_args[0][0] == "send: b\'hello"
@freeze_time(datetime(2024, 4, 10, 10, 0, 10))
def test_sleep_exceeds_ttl(mocker):
"""
Given: a sleep duration exceeding the remaining TTL.
When: The `sleep` method is called with that duration.
Then:
- A warning should be outputed indicating that the requested sleep exceeds the TTL.
"""
mocker.patch.object(demisto, 'callingContext', {"context": {"runDuration": 5}})
setattr(CommonServerPython, 'SAFE_SLEEP_START_TIME', datetime(2024, 4, 10, 10, 0, 0)) # Set stub in your_script
with pytest.raises(ValueError) as excinfo:
safe_sleep(duration_seconds=350)
assert str(excinfo.value) == "Requested a sleep of 350 seconds, but time left until docker timeout is 300 seconds."
def test_sleep_not_supported(mocker):
"""
Given: a sleep duration in not supported server version.
When: The `sleep` method is called with that duration.
Then:
- A warning should be outputed indicating that the requested sleep exceeds the TTL.
- Sleep the requested time.
"""
mocker.patch.object(demisto, 'callingContext', {"context": {}})
logger_mocker = mocker.patch.object(demisto, 'info')
sleep_mocker = mocker.patch('time.sleep')
safe_sleep(duration_seconds=50)
# Verify sleep duration based on mocked time difference
assert sleep_mocker.call_count == 1
assert logger_mocker.call_args[0][0] == "Safe sleep is not supported in this server version, sleeping for the requested time."
def test_sleep_mocked_time(mocker):
"""
Given: a method using sleep.
When: The `sleep` method is called with a specific duration.
Then:
- The sleep duration should be based on the difference between the mocked time calls.
- No exception should be raised if the sleep duration is within the remaining TTL based on mocked time.
"""
mocker.patch.object(demisto, 'callingContext', {"context": {"runDuration": 5}})
setattr(CommonServerPython, 'SAFE_SLEEP_START_TIME', datetime(2024, 4, 10, 10, 0, 0)) # Set stub in your_script
sleep_mocker = mocker.patch('time.sleep')
with freeze_time(datetime(2024, 4, 10, 10, 0, 10)):
safe_sleep(duration_seconds=5) # Sleep for 5 seconds
# Advance mocked time by the sleep duration
with freeze_time(datetime(2024, 4, 10, 10, 0, 25)):
safe_sleep(duration_seconds=50)
# 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}
@pytest.mark.skipif(not IS_PY3, reason='test not supported in py2')
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)'
@pytest.mark.parametrize('instance_name, expected_result',
[('instance_name1', 'engine_id'),
('instance_name2', '')
], ids=[
"Test-instanec-with-xsoar-engine-configures",
"Test-instanec-without-xsoar-engine-configures"
])
def test_is_integration_instance_running_on_engine(mocker, instance_name, expected_result):
""" Tests the 'is_integration_instance_running_on_engine' function's logic.
Given:
1. A name of an instance that has an engine configured (and relevant mocked responses).
2. A name of an instance that doesn't have an engine configured (and relevant mocked responses).
When:
- Running the 'is_integration_instance_running_on_engine' funcution.
Then:
- Verify that:
1. The result is the engine's id.
2. The result is an empty string.
"""
mock_response = {
'body': """{"instances": [
{"id": "1111", "name": "instance_name1", "engine": "engine_id"},
{"id": "2222", "name": "instance_name2", "engine": ""}
]}""",
}
mocker.patch.object(demisto, 'internalHttpRequest', return_value=mock_response)
mocker.patch.object(demisto, 'integrationInstance', return_value=instance_name)
res = is_integration_instance_running_on_engine()
assert res == expected_result
def test_get_engine_base_url(mocker):
""" Tests the 'get_engine_base_url' function's logic.
Given:
- Mocked response of the internalHttpRequest call for the '/engines' endpoint, including 2 engines.
- An id of an engine.
When:
- Running the 'is_integration_instance_running_on_engine' funcution.
Then:
- Verify that base url of the given engine id was returened.
"""
mock_response = {
'body': """{"engines": [
{"id": "1111", "baseUrl": "11.111.111.33:443"},
{"id": "2222", "baseUrl": "11.111.111.44:443"}
]}""",
}
mocker.patch.object(demisto, 'internalHttpRequest', return_value=mock_response)
res = get_engine_base_url('1111')
assert res == '11.111.111.33:443'
class TestMirrorObjectInitialization:
"""
Tests for the MirrorObject dataclass initialization and __post_init__ logic.
"""
def test_initialization_with_all_fields(self, mocker):
"""
Given: Valid object_url and ticket_id.
When: MirrorObject is initialized.
Then: The object is created with the correct attributes, and no debug message is logged.
"""
# Given
mock_demisto_debug = mocker.patch.object(demisto, 'debug')
object_url = "http://example.com/ticket/123"
object_id = "JIRA-123"
object_name = "TicketName"
# When
mirror_obj = MirrorObject(object_url=object_url, object_id=object_id, object_name=object_name)
# Then
assert mirror_obj.object_url == object_url, "object_url should be initialized correctly."
assert mirror_obj.object_id == object_id, "ticket_id should be initialized correctly."
mock_demisto_debug.assert_not_called()
def test_initialization_with_no_fields(self, mocker):
"""
Given: No object_url and no ticket_id (both None).
When: MirrorObject is initialized.
Then: The object is created with None attributes, and a debug message is logged for both missing fields.
"""
# Given
mock_demisto_debug = mocker.patch.object(demisto, 'debug')
expected_log_message = "MirrorObject: Initialized with missing mandatory fields: object_url, object_id, object_name"
# When
mirror_obj = MirrorObject(object_url=None, object_id=None, object_name=None)
# Then
assert mirror_obj.object_url is None, "object_url should be None."
assert mirror_obj.object_id is None, "ticket_id should be None."
assert mirror_obj.object_name is None, "object_name should be None."
mock_demisto_debug.assert_called_once_with(expected_log_message)
def test_initialization_with_only_ticket_url(self, mocker):
"""
Given: Only object_url is provided (ticket_id is None).
When: MirrorObject is initialized.
Then: The object is created, and a debug message is logged for the missing ticket_id.
"""
# Given
mock_demisto_debug = mocker.patch.object(demisto, 'debug')
object_url = "http://example.com/ticket/123"
expected_log_message = "MirrorObject: Initialized with missing mandatory fields: object_id, object_name"
# When
mirror_obj = MirrorObject(object_url=object_url, object_id=None)
# Then
assert mirror_obj.object_url == object_url
assert mirror_obj.object_id is None
mock_demisto_debug.assert_called_once_with(expected_log_message)
def test_initialization_with_only_ticket_id(self, mocker):
"""
Given: Only ticket_id is provided (object_url is None).
When: MirrorObject is initialized.
Then: The object is created, and a debug message is logged for the missing object_url.
"""
# Given
mock_demisto_debug = mocker.patch.object(demisto, 'debug')
object_id = "JIRA-123"
expected_log_message = "MirrorObject: Initialized with missing mandatory fields: object_url, object_name"
# When
mirror_obj = MirrorObject(object_url=None, object_id=object_id)
# Then
assert mirror_obj.object_url is None
assert mirror_obj.object_id == object_id
mock_demisto_debug.assert_called_once_with(expected_log_message)
@pytest.mark.parametrize(
"ticket_url_in, ticket_id_in, ticket_name_in, expected_missing_fields, should_log",
[
("http://url.com", "ID-1", "TestTicket", [], False),
(None, "ID-1", "TestTicket", ["object_url"], True),
("http://url.com", None, "TestTicket", ["object_id"], True),
(None, None, None, ["object_url", "object_id", "object_name"], True),
],
)
def test_post_init_logging_parametrized(
self,
mocker,
ticket_url_in,
ticket_id_in,
ticket_name_in,
expected_missing_fields,
should_log,
):
"""
Given: Various combinations of object_url and ticket_id.
When: MirrorObject is initialized.
Then: demisto.debug is called (or not) with the expected message based on missing fields.
"""
# Given
mock_demisto_debug = mocker.patch.object(demisto, 'debug')
# When
MirrorObject(object_url=ticket_url_in, object_id=ticket_id_in, object_name=ticket_name_in)
# Then
if should_log:
assert mock_demisto_debug.call_count == 1, "demisto.debug should have been called once."
logged_message = mock_demisto_debug.call_args[0][0]
assert "MirrorObject: Initialized with missing mandatory fields: {}".format(
", ".join(expected_missing_fields)
) == logged_message, "The debug log message was not as expected."
else:
mock_demisto_debug.assert_not_called()
class TestMirrorObjectToContext:
"""
Tests for the MirrorObject.to_context() method.
"""
@pytest.mark.parametrize(
"object_url, object_id, expected_dict",
[
(
"http://example.com/ticket/123",
"JIRA-123",
{"object_url": "http://example.com/ticket/123", "object_id": "JIRA-123", 'object_name': None},
),
(None, "JIRA-123", {"object_url": None, "object_id": "JIRA-123", 'object_name': None}),
("http://example.com/ticket/123", None, {"object_url": "http://example.com/ticket/123", "object_id": None, 'object_name': None}),
(None, None, {"object_url": None, "object_id": None, 'object_name': None}),
("", "", {"object_url": "", "object_id": "", 'object_name': None}), # Testing with empty strings
],
)
def test_to_context_various_inputs(
self,
mocker,
object_url,
object_id,
expected_dict,
):
"""
Given: A MirrorObject initialized with various combinations of object_url and ticket_id.
When: The to_context() method is called.
Then: A dictionary representation of the object is returned with the correct key-value pairs.
"""
# Given
mirror_obj = MirrorObject(object_url=object_url, object_id=object_id)
# When
context_dict = mirror_obj.to_context()
# Then
assert context_dict == expected_dict, "The context dictionary does not match the expected output."
def test_to_context_returns_dict(self, mocker):
"""
Given: A MirrorObject.
When: to_context() is called.
Then: The return type is a dict.
"""
# Given
mirror_obj = CommonServerPython.MirrorObject()
# When
result = mirror_obj.to_context()
# Then
assert isinstance(result, dict), "to_context() should return a dictionary."
class TestQuickActionPreviewInitialization:
"""
Test suite for the initialization and __post_init__ logic of the QuickActionPreview class.
"""
ALL_QA_PREVIEW_FIELD_NAMES = [
'id', 'title', 'description', 'status', 'assignee', 'creation_date', 'severity'
]
def test_initialization_with_all_fields_provided_non_none(self, mocker):
"""
Given: All fields are provided with non-None string values.
When: A QuickActionPreview instance is created.
Then: The instance's attributes are set correctly, and demisto.debug is not called.
"""
# Given
mock_demisto_debug = mocker.patch.object(demisto, 'debug')
field_values = {
"id": "ID123",
"title": "Sample Title",
"description": "Detailed description.",
"status": "Open",
"assignee": "John Doe",
"creation_date": "2023-01-01T12:00:00Z",
"severity": "High"
}
# When
preview = QuickActionPreview(**field_values)
# Then
for field_name, expected_value in field_values.items():
assert getattr(preview, field_name) == expected_value, "{} was not initialized correctly.".format(field_name)
mock_demisto_debug.assert_not_called()
def test_initialization_with_all_fields_none(self, mocker):
"""
Given: All fields default to None.
When: A QuickActionPreview instance is created.
Then: demisto.debug is called with a message containing all field names.
"""
# Given
mock_demisto_debug = mocker.patch.object(demisto, 'debug')
# When
QuickActionPreview() # All fields default to None
# Then
mock_demisto_debug.assert_called_once()
actual_log_message = mock_demisto_debug.call_args[0][0]
for field_name in self.ALL_QA_PREVIEW_FIELD_NAMES:
assert field_name in actual_log_message, "Expected '{}' to be in the log message.".format(field_name)
def test_initialization_with_some_fields_none(self, mocker):
"""
Given: Some fields are provided, and others are None.
When: A QuickActionPreview instance is created.
Then: demisto.debug is called with a message containing only the names of the None fields.
"""
# Given
mock_demisto_debug = mocker.patch.object(demisto, 'debug')
field_values = {
"id": "ID456",
"title": None,
"description": "Another description.",
"status": None,
"assignee": "Jane Roe",
"creation_date": None,
"severity": "Medium"
}
expected_missing = ["title", "status", "creation_date"]
expected_present = ["id", "description", "assignee", "severity"]
# When
QuickActionPreview(**field_values)
# Then
mock_demisto_debug.assert_called_once()
actual_log_message = mock_demisto_debug.call_args[0][0]
# Check that the names of missing fields are in the log
for field_name in expected_missing:
assert field_name in actual_log_message, "Expected '{}' to be logged as missing.".format(field_name)
# Check that the names of present fields are NOT in the log
for field_name in expected_present:
assert field_name not in actual_log_message, "Did not expect '{}' to be logged as missing.".format(field_name)
def test_initialization_with_empty_strings_not_logged_as_missing(self, mocker):
"""
Given: Some fields are empty strings, others are None.
When: A QuickActionPreview instance is created.
Then: Empty strings are not logged as missing; only None fields are.
"""
# Given
mock_demisto_debug = mocker.patch.object(demisto, 'debug')
field_values = {
"id": "",
"title": "A Title",
"description": None,
"status": "",
"assignee": "User",
"creation_date": "2023-02-02",
"severity": None
}
expected_missing = ["description", "severity"]
not_expected_missing = ["id", "title", "status", "assignee", "creation_date"]
# When
QuickActionPreview(**field_values)
# Then
mock_demisto_debug.assert_called_once()
actual_log_message = mock_demisto_debug.call_args[0][0]
# Check that only the None fields are mentioned in the log
for field_name in expected_missing:
assert field_name in actual_log_message, "Expected '{}' to be logged as missing.".format(field_name)
# Check that fields with values (even empty strings) are NOT in the log
for field_name in not_expected_missing:
assert field_name not in actual_log_message, "Did not expect '{}' to be logged as missing.".format(field_name)
@pytest.mark.parametrize(
"init_kwargs, expected_logged_missing_fields",
[
(
{"id": "1", "title": "t", "description": "d", "status": "s", "assignee": "a", "creation_date": "cd", "severity": "sev"},
[] # No missing fields
),
(
{"id": None, "title": "t", "description": "d", "status": "s", "assignee": "a", "creation_date": "cd", "severity": "sev"},
["id"]
),
(
{"id": "1", "title": None, "description": "d", "status": "s", "assignee": "a", "creation_date": "cd", "severity": None},
["title", "severity"] # Order matters
),
(
{"title": "Present", "status": "Also Present"}, # Others are None by default
["id", "description", "assignee", "creation_date", "severity"]
),
(
{}, # All fields are None by default
ALL_QA_PREVIEW_FIELD_NAMES
),
],
ids=[
"all_present",
"id_missing",
"title_and_severity_missing",
"only_title_and_status_present",
"all_missing_default_init",
]
)
def test_post_init_logging_parametrized(
self,
mocker,
init_kwargs,
expected_logged_missing_fields
):
"""
Given: Various combinations of field initializations for QuickActionPreview.
When: A QuickActionPreview instance is created.
Then: demisto.debug is called (or not called) with the correctly formatted message,
listing None fields in their definition order.
"""
# Given
mock_demisto_debug = mocker.patch.object(demisto, 'debug')
# When
QuickActionPreview(**init_kwargs) # type: ignore
# Then
if expected_logged_missing_fields:
actual_log_message = mock_demisto_debug.call_args[0][0]
for field_name in expected_logged_missing_fields:
assert field_name in actual_log_message, "Expected '{}' to be logged as missing.".format(field_name)
else:
mock_demisto_debug.assert_not_called()
class TestQuickActionPreviewToContext:
"""
Test suite for the to_context method of the QuickActionPreview class.
"""
@pytest.mark.parametrize(
"init_kwargs, expected_dict",
[
( # All fields present
{"id": "1", "title": "t", "description": "d", "status": "s", "assignee": "a", "creation_date": "cd", "severity": "sev"},
{"id": "1", "title": "t", "description": "d", "status": "s", "assignee": "a", "creation_date": "cd", "severity": "sev"}
),
( # Some fields None
{"id": "2", "title": None, "description": "desc", "status": "Open", "assignee": None, "creation_date": "date", "severity": "High"},
{"id": "2", "title": None, "description": "desc", "status": "Open", "assignee": None, "creation_date": "date", "severity": "High"}
),
( # All fields None (default initialization)
{},
{"id": None, "title": None, "description": None, "status": None, "assignee": None, "creation_date": None, "severity": None}
),
( # Fields with empty strings
{"id": "", "title": "A Title", "description": "", "status": "Done", "assignee": "", "creation_date": "Never", "severity": ""},
{"id": "", "title": "A Title", "description": "", "status": "Done", "assignee": "", "creation_date": "Never", "severity": ""}
),
],
ids=[
"all_values_present",
"some_values_none",
"all_values_none_default_init",
"empty_string_values",
]
)
def test_to_context_returns_correct_dictionary(
self,
mocker,
init_kwargs,
expected_dict
):
"""
Given: A QuickActionPreview initialized with various field values.
When: The to_context() method is called.
Then: A dictionary is returned that accurately represents the QuickActionPreview's attributes,
including None values and empty strings.
"""
# Given
mock_demisto_debug = mocker.patch.object(demisto, 'debug')
preview = QuickActionPreview(**init_kwargs) # type: ignore
# When
actual_context_dict = preview.to_context()
# Then
assert actual_context_dict == expected_dict, \
"The dictionary from to_context() does not match the expected output."
def test_to_context_return_type_is_dict(self, mocker):
"""
Given: Any QuickActionPreview instance.
When: The to_context() method is called.
Then: The returned type is a dictionary.
"""
# Given
mock_demisto_debug = mocker.patch.object(demisto, 'debug')
preview = QuickActionPreview()
# When
result = preview.to_context()
# Then
assert isinstance(result, dict), "The to_context() method should always return a dict."
@pytest.mark.parametrize('input_text, pattern, expected_output, call_count', [
pytest.param('invalid_grant: java.security.SignatureException: Invalid signature for token: 1234',
r'(token:\s*)(\S+)', '1234', 1, id='Match token value'),
pytest.param('invalid_grant: java.security.SignatureException: Invalid signature for token: 1234', r'(invalid_grant: java.security.SignatureException: Invalid signature for token: 1234)',
'invalid_grant: java.security.SignatureException: Invalid signature for token: 1234', 1, id='Match entire string')
])
def test_find_and_remove_sensitive_text__found_onc(input_text, pattern, expected_output, call_count, mocker):
"""
Given:
- Input text that includes sensitive information.
When:
- Invoking the `find_and_remove_sensitive_text` method with a regex pattern to search for sensitive information.
Then:
- Verify that the function responsible for removing sensitive information from the logs is called with the sensitive data as an argument.
- Verify that the function is called the correct number of times.
"""
input_text = 'invalid_grant: java.security.SignatureException: Invalid signature for token: 1234'
mock_remove_from_logs = mocker.patch('CommonServerPython.add_sensitive_log_strs', return_value=None)
find_and_remove_sensitive_text(input_text, pattern)
assert mock_remove_from_logs.call_count == call_count
assert mock_remove_from_logs.call_args[0][0] == expected_output
@pytest.mark.parametrize('pattern, expected_output, call_count', [
pytest.param(r'n', ['n', 'n', 'n', 'n', 'n', 'n', 'n'], 7, id='Match character "n"'),
pytest.param(r'(?i)invalid', ['invalid', 'Invalid'], 2, id='Match word "invalid" case insensitive')
])
def test_find_and_remove_sensitive_text__found_multiple(pattern, expected_output, call_count, mocker):
"""
Given:
- Input text that includes sensitive information.
When:
- Invoking the `find_and_remove_sensitive_text` method with a regex pattern to search for a sensitive information.
Then:
verify that the function responsible for removing sensitive information from the logs is called with the sensitive data as an argument.
verify that the function is called the correct number of times.
"""
input_text = 'invalid_grant: java.security.SignatureException: Invalid signature for token: 1234'
mock_remove_from_logs = mocker.patch('CommonServerPython.add_sensitive_log_strs', return_value=None)
find_and_remove_sensitive_text(input_text, pattern)
assert mock_remove_from_logs.call_count == call_count
for x in range(call_count):
assert mock_remove_from_logs.call_args_list[x][0][0] == expected_output[x]
def test_find_and_remove_sensitive_text__not_found(mocker):
"""
Given:
- Input text that does not contain any sensitive information (e.g., no word following "token:").
When:
- Invoking the `find_and_remove_sensitive_text` method with a regex pattern to search for a sensitive information (the word following "token:").
Then:
- Ensure that the function does not remove anything from the logs.
"""
input_text = 'invalid_grant: java.security.SignatureException: Invalid signature for text: 1234'
mock_remove_from_logs = mocker.patch('CommonServerPython.add_sensitive_log_strs', return_value=None)
find_and_remove_sensitive_text(input_text, r'(token:\s*)(\S+)')
mock_remove_from_logs.assert_not_called()
def test_execute_polling_command(mocker):
"""
Given:
- A polling command that returns a war room entry with a polling command and polling args.
When:
- Calling the `execute_polling_command` funtion.
Then:
- Ensure that `demisto.executeCommand` is called until the war room entry indicates that polling is done (twice in this case).
- Ensure that `demisto.executeCommand` is called with the correct command name and args.
- Ensure that `CommandResults` matches the war room entries (namely the context output and human-readable output).
"""
entries = []
for i in [1, 2]:
file_to_open = "test_data/polling_command_entry_{i}.json".format(i=i)
with open(file_to_open) as f:
entries.append(json.load(f))
def mock_polling_command_entry(command_name, args):
polling_arg = "action_id"
if not polling_arg in args:
return entries[0]
else:
return entries[1]
mock_execute_command = mocker.patch.object(demisto, "executeCommand", side_effect=mock_polling_command_entry)
command_name = "demo-polling-command"
original_call_args = {"endpoint_id": "my_endpoint", "interval_in_seconds": 1}
command_results = execute_polling_command(command_name, original_call_args)
# Check that command has been executed twice
assert mock_execute_command.call_count == 2
# Check inputs of first execution (expect original args to be used)
mock_first_call_inputs = mock_execute_command.call_args_list[0][0]
assert mock_first_call_inputs[0] == command_name
assert mock_first_call_inputs[1] == original_call_args
# Check inputs of second execution (expect polling args to be used)
mock_second_call_inputs = mock_execute_command.call_args_list[1][0]
assert mock_second_call_inputs[0] == command_name
assert mock_second_call_inputs[1] == entries[0][0]["Metadata"]["pollingArgs"]
# Check outputs of first execution
assert command_results[0].outputs == entries[0][0]["EntryContext"]
assert command_results[0].readable_output == entries[0][0]["HumanReadable"]
assert command_results[1].outputs == entries[1][0]["EntryContext"]
assert command_results[1].readable_output == entries[1][0]["HumanReadable"]
def test_stringEscapeMD():
"""
Given: A string with the char ~, that should be escaped.
When: Using the function stringEscapeMD.
Then: The special char is escaped.
"""
st = "somethig~issue~"
result = stringEscapeMD(st)
assert result == "somethig\~issue\~"
def test_arg_to_bool_or_none_with_none():
"""
Given: A None value is passed to arg_to_bool_or_none function.
When: The function processes the None input.
Then: It should return None without any conversion.
"""
result = arg_to_bool_or_none(None)
assert result is None
def test_arg_to_bool_or_none_with_string():
"""
Given: A string value 'true' is passed to arg_to_bool_or_none function.
When: The function processes the string input.
Then: It should return the boolean representation using argToBoolean.
"""
result = arg_to_bool_or_none("true")
assert result is True
CONSTANT_PACK_VERSION = '1.0.0'
def test_get_pack_version():
"""
Given: A global CONSTANT_PACK_VERSION
When: Using the function get_pack_version.
Then: assert verify the correct version is returned.
"""
with open('CommonServerPython.py', 'r') as f:
code = f.read()
exec(code, globals())
version = get_pack_version()
assert version == '1.0.0'
# The unit test below will fail if run on Windows systems due to limited signal handling capabilities compared to Unix systems
@pytest.mark.parametrize(
"sleep_time, expected_is_finished",
[
pytest.param(3, False, id="Slow execution"),
pytest.param(0, True, id="Fast execution"),
],
)
def test_execution_timeout_context_manager(sleep_time, expected_is_finished):
"""
Given:
- An execution timeout value of 2 seconds.
When:
- When using `ExecutionTimeout` context manager with a simulated "slow" and "fast" executions.
Assert:
- Case A (Slow): Ensure `is_finished` is False since `time.sleep` timed out (`sleep_time` > `execution_timeout`).
- Case B (Fast): Ensure `is_finished` is True since `time.sleep` finished in time (`sleep_time` < `execution_timeout`).
"""
execution_timeout = 2 # Slow: Sleep one second more than timeout. Fast: Don't sleep.
is_finished = False
with ExecutionTimeout(seconds=execution_timeout):
time.sleep(sleep_time)
is_finished = True # Slow: This line will not be reached. Fast: Line reached, variable updated.
assert is_finished == expected_is_finished
# The unit test below will fail if run on Windows systems due to limited signal handling capabilities compared to Unix systems
@pytest.mark.parametrize(
"sleep_time, expected_return_value",
[
pytest.param(3, "I TIMED OUT", id="Slow execution"),
pytest.param(0, "I AM DONE", id="Fast execution"),
],
)
def test_execution_timeout_decorator(sleep_time, expected_return_value):
"""
Given:
- An execution timeout value of 2 seconds.
When:
- When using `ExecutionTimeout.limit_time` decorator with a simulated "slow" and "fast" executions.
Assert:
- Case A (Slow): Ensure return value is "I TIMED OUT" since `do_logic` timed out (`sleep_time` > `execution_timeout`).
- Case B (Fast): Ensure return value is "I AM DONE" since `do_logic` finished in time (`sleep_time` < `execution_timeout`).
"""
execution_timeout = 2 # Slow: Sleep one second more than timeout. Fast: Don't sleep.
@ExecutionTimeout.limit_time(execution_timeout, default_return_value="I TIMED OUT")
def do_logic():
time.sleep(sleep_time)
return "I AM DONE"
assert do_logic() == expected_return_value
class TestTimeSensitive:
def test_http_request_time_sensitive(self, mocker):
"""
Given:
- A BaseClient object
- Time-sensitive mode is enabled
When:
- Calling _http_request
Then:
- Ensure the timeout is calculated correctly based on the remaining time.
"""
from CommonServerPython import BaseClient, is_time_sensitive
mocker.patch('CommonServerPython.is_time_sensitive', return_value=True)
mocker.patch('time.time', side_effect=[100, 105]) # First call for deadline, second for remaining time
client = BaseClient('http://example.com', timeout=60)
client._time_sensitive_total_timeout = 15
client._time_sensitive_deadline = 115 # 100 + 15
mock_session = mocker.patch.object(client, '_session')
mock_session.request.return_value.ok = True
mock_session.request.return_value.status_code = 200
client._http_request('get', 'test')
# The timeout passed to the request should be deadline - current_time
# 115 - 105 = 10
assert mock_session.request.call_args[1]['timeout'] == 10
def test_http_request_time_sensitive_budget_exceeded_before_call(self, mocker):
"""
Given:
- A BaseClient object
- Time-sensitive mode is enabled
- The time budget is already exceeded
When:
- Calling _http_request
Then:
- Ensure a DemistoException is raised.
"""
from CommonServerPython import BaseClient, DemistoException, is_time_sensitive
mocker.patch('CommonServerPython.is_time_sensitive', return_value=True)
mocker.patch('time.time', return_value=120)
client = BaseClient('http://example.com')
client._time_sensitive_total_timeout = 15
client._time_sensitive_deadline = 115 # 100 + 15
with pytest.raises(DemistoException, match="Time-sensitive command execution time limit .* reached before performing the API request"):
client._http_request('get', 'test')
def test_http_request_time_sensitive_timeout_during_call(self, mocker):
"""
Given:
- A BaseClient object
- Time-sensitive mode is enabled
When:
- Calling _http_request and it times out
Then:
- Ensure a DemistoException is raised with a time-sensitive budget exceeded message.
"""
from CommonServerPython import BaseClient, DemistoException, is_time_sensitive
mocker.patch('CommonServerPython.is_time_sensitive', return_value=True)
mocker.patch('time.time', side_effect=[100, 105]) # First for deadline, second for remaining time
client = BaseClient('http://example.com')
client._time_sensitive_total_timeout = 15
client._time_sensitive_deadline = 115 # 100 + 15
mocker.patch.object(client, '_session')
client._session.request.side_effect = requests.exceptions.ConnectTimeout
with pytest.raises(DemistoException, match="Time-sensitive command execution time limit .* exceeded"):
client._http_request('get', 'test')
class TestSanitizeHtmlOutput:
"""Tests for sanitize_html_output – output encoding."""
def test_escapes_html_tags_by_default(self):
"""Test that HTML special characters are escaped when no allow_tags specified."""
from CommonServerPython import sanitize_html_output
result = sanitize_html_output('
')
assert '<img' in result
assert '
alert(1)')
assert '