Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crowd strike indicator feed - Get correct threat actor names #33607

Merged
merged 26 commits into from Apr 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9856689
no pre-commit- fix actors
tcarmeli1 Mar 27, 2024
6b7eba3
no pre-commit- fix actors2
tcarmeli1 Mar 27, 2024
66d0a8d
Merge remote-tracking branch 'origin' into crowd-strike-enhance
tcarmeli1 Mar 27, 2024
eb36a86
no pre-commit- fix pre
tcarmeli1 Mar 27, 2024
24a9ae7
no pre-commit- fix pre
tcarmeli1 Mar 27, 2024
05cbaa2
fix test
tcarmeli1 Mar 28, 2024
997fbf3
Merge remote-tracking branch 'origin' into crowd-strike-enhance
tcarmeli1 Mar 28, 2024
01cc8ec
release note
tcarmeli1 Mar 28, 2024
52c2ccf
Merge remote-tracking branch 'origin' into crowd-strike-enhance
tcarmeli1 Mar 28, 2024
8a3beee
fix
tcarmeli1 Mar 28, 2024
ab2ecc7
Merge remote-tracking branch 'origin' into crowd-strike-enhance
tcarmeli1 Mar 28, 2024
f45d574
fix response
tcarmeli1 Mar 31, 2024
f6eb257
fix response format
tcarmeli1 Mar 31, 2024
0021b0d
Merge remote-tracking branch 'origin' into crowd-strike-enhance
tcarmeli1 Apr 2, 2024
644e68c
fix pre
tcarmeli1 Apr 2, 2024
ffc6689
fix
tcarmeli1 Apr 2, 2024
abe0e26
Apply suggestions from code review
tcarmeli1 Apr 3, 2024
e924a2f
Merge remote-tracking branch 'origin' into crowd-strike-enhance
tcarmeli1 Apr 3, 2024
33147ef
try pre-commit fix
tcarmeli1 Apr 3, 2024
6aca11f
fix pre commit
tcarmeli1 Apr 3, 2024
5e702b6
Merge remote-tracking branch 'origin' into crowd-strike-enhance
tcarmeli1 Apr 3, 2024
5f6baeb
Merge remote-tracking branch 'origin' into crowd-strike-enhance
tcarmeli1 Apr 4, 2024
8c69dc2
Merge remote-tracking branch 'origin' into crowd-strike-enhance
tcarmeli1 Apr 7, 2024
52667f5
Merge remote-tracking branch 'origin' into crowd-strike-enhance
tcarmeli1 Apr 7, 2024
e6ea1d4
rn fix
tcarmeli1 Apr 7, 2024
40c4458
rn
tcarmeli1 Apr 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -105,7 +105,7 @@ def __init__(self, credentials, base_url, include_deleted, type, limit, tlp_colo
params = assign_params(credentials=credentials,
server_url=base_url,
insecure=insecure,
ok_codes=tuple(),
ok_codes=(),
proxy=proxy,
timeout=timeout)
super().__init__(params)
Expand All @@ -129,6 +129,16 @@ def get_indicators(self, params):
)
return response

def get_actors_names_request(self, params_string):
response = self._http_request(
method='GET',
url_suffix=f'intel/entities/actors/v1?{params_string}',
timeout=30
)
if 'resources' not in response:
raise DemistoException("Get actors request completed. Parse error: could not find resources in response.")
return response['resources']

def fetch_indicators(self,
limit: int,
offset: int = 0,
Expand Down Expand Up @@ -195,9 +205,11 @@ def fetch_indicators(self,
demisto.info(f'set last_run to: {new_last_marker_time}')

indicators.extend(self.create_indicators_from_response(response,
self.get_actors_names_request,
self.tlp_color,
self.feed_tags,
self.create_relationships))
self.create_relationships,
))
return indicators

def handle_first_fetch_context_or_pre_2_1_0(self, filter_string: str) -> tuple[str, list[dict]]:
Expand Down Expand Up @@ -236,6 +248,7 @@ def handle_first_fetch_context_or_pre_2_1_0(self, filter_string: str) -> tuple[s
demisto.debug(f'Importing the indicator marker in first time: {_marker=}')
last_run = f"_marker:>'{_marker}'"
parse_indicator = self.create_indicators_from_response(response,
self.get_actors_names_request,
self.tlp_color,
self.feed_tags,
self.create_relationships)
Expand Down Expand Up @@ -264,7 +277,8 @@ def get_last_run() -> str:
return params

@staticmethod
def create_indicators_from_response(raw_response, tlp_color=None, feed_tags=None, create_relationships=True) -> list:
def create_indicators_from_response(raw_response, get_actors_names_request_func, tlp_color=None, feed_tags=None,
create_relationships=True) -> list:
""" Builds indicators from API raw response

Args:
Expand Down Expand Up @@ -308,7 +322,7 @@ def create_indicators_from_response(raw_response, tlp_color=None, feed_tags=None
if feed_tags:
indicator['fields']['tags'].extend(feed_tags)
if create_relationships:
relationships = create_and_add_relationships(indicator, resource)
relationships = create_and_add_relationships(indicator, resource, get_actors_names_request_func)
indicator['relationships'] = relationships
parsed_indicators.append(indicator)

Expand All @@ -327,7 +341,7 @@ def build_type_fql(types_list: list) -> str:

if 'ALL' in types_list:
# Replaces "ALL" for all types supported on XSOAR.
crowdstrike_types = [f"type:'{type}'" for type in CROWDSTRIKE_TO_XSOAR_TYPES.keys()]
crowdstrike_types = [f"type:'{type}'" for type in CROWDSTRIKE_TO_XSOAR_TYPES]
else:
crowdstrike_types = [f"type:'{XSOAR_TYPES_TO_CROWDSTRIKE.get(type.lower())}'" for type in types_list if
type.lower() in XSOAR_TYPES_TO_CROWDSTRIKE]
Expand All @@ -336,7 +350,7 @@ def build_type_fql(types_list: list) -> str:
return result


def create_and_add_relationships(indicator: dict, resource: dict) -> list:
def create_and_add_relationships(indicator: dict, resource: dict, get_actors_names_request_func) -> list:
"""
Creates and adds relationships to indicators for each CrowdStrike relationships type.

Expand All @@ -352,12 +366,14 @@ def create_and_add_relationships(indicator: dict, resource: dict) -> list:

for field in CROWDSTRIKE_INDICATOR_RELATION_FIELDS:
if field in resource and resource[field]:
relationships.extend(create_relationships(field, indicator, resource))
relationships.extend(create_relationships(field, indicator, resource, get_actors_names_request_func))

return relationships


def create_relationships(field: str, indicator: dict, resource: dict) -> list:
def create_relationships(
field: str, indicator: dict, resource: dict, get_actors_names_request_func
) -> List:
"""
Creates indicator relationships.

Expand All @@ -370,6 +386,8 @@ def create_relationships(field: str, indicator: dict, resource: dict) -> list:
List of relationships objects.
"""
relationships = []
if field == 'actors' and resource['actors']:
resource['actors'] = change_actors_from_id_to_name(resource['actors'], get_actors_names_request_func)
for relation in resource[field]:
if field == 'relations' and not CROWDSTRIKE_TO_XSOAR_TYPES.get(relation.get('type')):
demisto.debug(f"The related indicator type {relation.get('type')} is not supported in XSOAR.")
Expand All @@ -392,10 +410,30 @@ def create_relationships(field: str, indicator: dict, resource: dict) -> list:
).to_indicator()

relationships.append(indicator_relation)

return relationships


def change_actors_from_id_to_name(indicator_actors_array: List[str], get_name_of_actors__func):
integration_context = get_integration_context()
actors_to_convert = []
converted_actors_array = []
for actor in indicator_actors_array:
if converted_actor := integration_context.get(actor, None):
converted_actors_array.append(converted_actor)
else:
actors_to_convert.append(actor)
if actors_to_convert:
actor_ids_params = 'ids=' + '&ids='.join(actors_to_convert) + '&fields=name'
actors_response = get_name_of_actors__func(actor_ids_params)
converted_actors_from_request = []
for actor_dict in actors_response:
converted_actors_from_request.append(actor_dict.get('name'))
zipped_actors_list_to_context = dict(zip(actors_to_convert, converted_actors_from_request))
update_integration_context(zipped_actors_list_to_context)
converted_actors_array += converted_actors_from_request
return converted_actors_array


def auto_detect_indicator_type_from_cs(value: str, crowdstrike_resource_type: str) -> str | None:
'''
The function determines the type of indicator according to two cases::
Expand Down
@@ -1,11 +1,10 @@
import json
import io
import demistomock as demisto
import pytest


def util_load_json(path):
with io.open(path, mode='r', encoding='utf-8') as f:
with open(path, encoding='utf-8') as f:
return json.loads(f.read())


Expand All @@ -27,7 +26,8 @@ def test_crowdstrike_indicators_list_command(requests_mock):
mock_response = util_load_json('test_data/crowdstrike_indicators_list_command.json')
requests_mock.post('https://api.crowdstrike.com/oauth2/token', json={'access_token': '12345'})
requests_mock.get(url='https://api.crowdstrike.com/intel/combined/indicators/v1', json=mock_response)

requests_mock.get(url='https://api.crowdstrike.com/intel/entities/actors/v1?ids=GOBLINPANDA&fields=name',
json={'resources': [{'name': 'GOBLIN PANDA'}]})
feed_tags = ['Tag1', 'Tag2']
client = Client(base_url='https://api.crowdstrike.com/', credentials={'identifier': '123', 'password': '123'},
type='Domain', include_deleted='false', limit=2, feed_tags=feed_tags)
Expand All @@ -39,7 +39,8 @@ def test_crowdstrike_indicators_list_command(requests_mock):
assert len(response.raw_response) == 3
assert "Indicators from CrowdStrike Falcon Intel" in response.readable_output
assert "domain_abc" in response.readable_output
assert feed_tags[0] and feed_tags[1] in response.raw_response[0]['fields']['tags']
assert feed_tags[0]
assert feed_tags[1] in response.raw_response[0]['fields']['tags']


@pytest.mark.parametrize(
Expand All @@ -66,7 +67,7 @@ def test_build_type_fql(types_list, expected):
assert res == expected


def test_create_indicators_from_response():
def test_create_indicators_from_response(requests_mock, mocker):
"""Tests build_type_fql function
Given
- Indicator types that were chosen by the user.
Expand All @@ -76,10 +77,10 @@ def test_create_indicators_from_response():
- validate result as expected
"""
from CrowdStrikeIndicatorFeed import Client

mocker.patch('CrowdStrikeIndicatorFeed.get_integration_context', return_value={'GOBLINPANDA': 'GOBLIN PANDA'})
raw_response = util_load_json('test_data/crowdstrike_indicators_list_command.json')
expected_result = util_load_json('test_data/create_indicators_from_response.json')
res = Client.create_indicators_from_response(raw_response)
res = Client.create_indicators_from_response(raw_response, Client.get_actors_names_request)
assert res == expected_result


Expand Down Expand Up @@ -123,8 +124,9 @@ def test_create_relationships_unknown_key(field, indicator, resource, expected_r
Then
- validate that no Key Error exception was thrown, and that only 1 relationship was created.
"""
from CrowdStrikeIndicatorFeed import Client
from CrowdStrikeIndicatorFeed import create_relationships
rs_ls = create_relationships(field, indicator, resource)
rs_ls = create_relationships(field, indicator, resource, Client.get_actors_names_request)
assert rs_ls == expected_results
assert len(rs_ls) == 1

Expand Down Expand Up @@ -293,6 +295,8 @@ def test_handling_first_fetch_and_old_integration_context(mocker,
client = Client(base_url='https://api.crowdstrike.com/', credentials={'identifier': '123', 'password': '123'},
type='ALL', include_deleted='false', limit=2, first_fetch=first_fetch)
mocker.patch('CrowdStrikeIndicatorFeed.demisto.getIntegrationContext', return_value=integration_context)
requests_mock.get(url='https://api.crowdstrike.com/intel/entities/actors/v1?ids=DOPPELSPIDER&fields=name',
json={'resources': [{'name': 'DOPPEL SPIDER'}]})
get_indicator_call = mocker.patch.object(client, 'get_indicators', return_value=get_indicators_response)

results = client.handle_first_fetch_context_or_pre_2_1_0(filter)
Expand Down Expand Up @@ -327,3 +331,101 @@ def test_auto_detect_indicator_type_from_cs(indicator: dict, expected_results: s
from CrowdStrikeIndicatorFeed import auto_detect_indicator_type_from_cs

assert auto_detect_indicator_type_from_cs(indicator['indicator'], indicator['type']) == expected_results


def test_get_actors_names_request_check_output(mocker, requests_mock):
"""
Given
- params for get_actors_names_request http request
When
- calling get_actors_names_request after fetching/listing indicators
Then
- Ensure the result is correct
"""
from CrowdStrikeIndicatorFeed import Client

mock_response = util_load_json('test_data/crowdstrike_indicators_list_command.json')
requests_mock.post('https://api.crowdstrike.com/oauth2/token', json={'access_token': '12345'})
requests_mock.get(url='https://api.crowdstrike.com/intel/combined/indicators/v1', json=mock_response)
crowdstrike_client = Client(base_url='https://api.crowdstrike.com/', credentials={'identifier': '123', 'password': '123'},
type='Domain', include_deleted='false', limit=2)
requests_mock.get(url='https://api.crowdstrike.com/intel/entities/actors/v1?', json={'resources': ''})
requests_mock.get(url='https://api.crowdstrike.com/intel/entities/actors/v1?ids=123&fields=name',
json={'resources': {'name': 'TEST TEST'}})
res = crowdstrike_client.get_actors_names_request(params_string='ids=123&fields=name')
assert res == {'name': 'TEST TEST'}


def test_get_actors_names_request_called_with(mocker, requests_mock):
"""
Given
- params for get_actors_names_request http request
When
- calling get_actors_names_request after fetching/listing indicators
Then
- Ensure the request is called with the right args
"""
from CrowdStrikeIndicatorFeed import Client
mock_response = util_load_json('test_data/crowdstrike_indicators_list_command.json')
requests_mock.post('https://api.crowdstrike.com/oauth2/token', json={'access_token': '12345'})
requests_mock.get(url='https://api.crowdstrike.com/intel/combined/indicators/v1', json=mock_response)
crowdstrike_client = Client(base_url='https://api.crowdstrike.com/', credentials={'identifier': '123', 'password': '123'},
type='Domain', include_deleted='false', limit=2)
requests_mock.get(url='https://api.crowdstrike.com/intel/entities/actors/v1?', json={'resources': ''})
http_request_mock = mocker.patch.object(crowdstrike_client, '_http_request',
return_value={'resources': {'name': 'TEST TEST'}})
crowdstrike_client.get_actors_names_request(params_string='ids=123&fields=name')
http_request_mock.assert_called_once_with(method='GET', url_suffix='intel/entities/actors/v1?ids=123&fields=name', timeout=30)


def test_crowdstrike_indicators_list_command_check_actors_convert(mocker, requests_mock):
"""
Given
- params for crowdstrike_indicators_list_command http request
When
- calling get_actors_names_request after fetching/listing indicators
Then
- Ensure the response is correct
"""
from CrowdStrikeIndicatorFeed import Client, crowdstrike_indicators_list_command
mock_response = util_load_json('test_data/crowdstrike_test_actors_convert.json')
requests_mock.post('https://api.crowdstrike.com/oauth2/token', json={'access_token': '12345'})
requests_mock.get(url='https://api.crowdstrike.com/intel/combined/indicators/v1', json=mock_response)
requests_mock.get(url='https://api.crowdstrike.com/intel/entities/actors/v1?ids=TESTTEST&fields=name',
json={'resources': [{'name': 'TEST TEST'}]})
mocker.patch('CrowdStrikeIndicatorFeed.get_integration_context', return_value={})
mocker.patch('CrowdStrikeIndicatorFeed.update_integration_context')
feed_tags = ['Tag1', 'Tag2']
crowdstrike_client = Client(base_url='https://api.crowdstrike.com/', credentials={'identifier': '123', 'password': '123'},
type='Domain', include_deleted='false', limit=2, feed_tags=feed_tags)
args = {
'limit': '2'
}
response = crowdstrike_indicators_list_command(crowdstrike_client, args)
assert len(response.outputs) == 1
assert len(response.raw_response) == 1
assert len(response.raw_response[0].get('relationships', None)) == 2
assert response.raw_response[0].get('relationships', None)[1].get('entityB', None) == 'TEST TEST'


def test_change_actors_from_id_to_name(mocker, requests_mock):
"""
Given
- params for change_actors_from_id_to_name http request
When
- calling get_actors_names_request after fetching/listing indicators
Then
- Ensure the response is correct
"""
from CrowdStrikeIndicatorFeed import Client, change_actors_from_id_to_name
requests_mock.post('https://api.crowdstrike.com/oauth2/token', json={'access_token': '12345'})
crowdstrike_client = Client(base_url='https://api.crowdstrike.com/', credentials={'identifier': '123', 'password': '123'},
type='Domain', include_deleted='false', limit=2)
actors_unparsed_array = ['TEST', 'TEST1', 'TEST2']
mocker.patch('CrowdStrikeIndicatorFeed.get_integration_context', return_value={'TEST': 'WAS IN CONTEXT'})
requests_mock.get(url='https://api.crowdstrike.com/intel/entities/actors/v1?ids=TEST1&ids=TEST2&fields=name',
json={'resources': [{'name': 'Changedtest1'}, {'name': 'Changedtest2'}]})
result = change_actors_from_id_to_name(actors_unparsed_array, crowdstrike_client.get_actors_names_request)
assert result[0] == "WAS IN CONTEXT"
assert result[1] == "Changedtest1"
assert result[2] == "Changedtest2"