Skip to content

Commit

Permalink
Feature/saved search all events (#359)
Browse files Browse the repository at this point in the history
* implementing page token when executing a saved search

* adding execute_get_all unit tests

* 'execute saved search with page token' unit tests

* adding feature comment

* improving method names'

* cleaned up unit tests

* Update CHANGELOG.md

* removing search_all_file_events() and adding documentation

* documentation

* improving docstrings
  • Loading branch information
tora-kozic committed Aug 18, 2021
1 parent 7bff397 commit da8142c
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 28 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta

## Unreleased

### Added

- New alias method `sdk.securitydata.savedsearches.search_file_events()` for existing method `sdk.securitydata.savedsearches.execute()`.

### Changed

- Updated minimum version of `requests` library to 2.4.2
Expand Down
22 changes: 3 additions & 19 deletions src/py42/clients/securitydata.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import re

from py42.exceptions import Py42ChecksumNotFoundError
from py42.exceptions import Py42Error
from py42.sdk.queries.fileevents.file_event_query import FileEventQuery
from py42.sdk.queries.fileevents.filters.file_filter import MD5
from py42.sdk.queries.fileevents.filters.file_filter import SHA256
from py42.services.util import escape_quote_chars


class SecurityDataClient:
Expand Down Expand Up @@ -56,10 +55,10 @@ def search_all_file_events(self, query, page_token=""):
field ``nextPgToken``. Defaults to empty string.
Returns:
:class:`py42.response.Py42Response`: A response containing page of events.
:class:`py42.response.Py42Response`: A response containing a page of events.
"""

query.page_token = _escape_quote_chars_in_token(page_token)
query.page_token = escape_quote_chars(page_token)
response = self._file_event_service.search(query)
return response

Expand Down Expand Up @@ -214,18 +213,3 @@ def _get_first_matching_version(versions, md5_hash):
exact_match = next((x for x in versions if x["fileMD5"] == md5_hash), None)
if exact_match:
return exact_match


def _escape_quote_chars_in_token(token):
"""
The `nextPgToken` returned in Forensic Search requests with > 10k results is the eventId
of the last event returned in the response. Some eventIds have double-quote chars in
them, which need to be escaped when passing the token in the next search request.
"""
unescaped_quote_pattern = r'[^\\]"'

return re.sub(
pattern=unescaped_quote_pattern,
repl=lambda match: match.group().replace('"', r"\""),
string=token,
)
19 changes: 18 additions & 1 deletion src/py42/services/savedsearch.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def get_query(self, search_id, page_number=None, page_size=None):

def execute(self, search_id, page_number=None, page_size=None):
"""
Execute a saved search for given search Id and return its results.
Executes a saved search for given search Id, returns up to the first 10,000 events.
Args:
search_id (str): Unique search Id of the saved search.
Expand All @@ -61,3 +61,20 @@ def execute(self, search_id, page_number=None, page_size=None):
"""
query = self.get_query(search_id, page_number=page_number, page_size=page_size)
return self._file_event_client.search(query)

def search_file_events(self, search_id, page_number=None, page_size=None):
"""
Alias method for :meth:`~execute()`. Executes a saved search for given search Id, returns up to the first 10,000 events.
To view more than the first 10,000 events:
* pass the :data:`search_id` to :meth:`~get_query()`
* pass the resulting query (:class:`~py42.sdk.queries.fileevents.file_event_query.FileEventQuery`) to :meth:`~py42.clients.securitydata.SecurityDataClient.search_all_file_events()`, use that method as normal.
Args:
search_id (str): Unique search Id of the saved search.
page_number (int, optional): The consecutive group of results of size page_size in the result set to return. Defaults to None.
page_size (int, optional): The maximum number of results to be returned. Defaults to None.
Returns:
:class:`py42.response.Py42Response`
"""
return self.execute(search_id, page_number=page_number, page_size=page_size)
17 changes: 17 additions & 0 deletions src/py42/services/util.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re

import py42.settings as settings


Expand All @@ -13,3 +15,18 @@ def get_all_pages(func, key, *args, **kwargs):
yield response
page_items = response[key]
item_count = len(page_items)


def escape_quote_chars(token):
"""
The `nextPgToken` returned in Forensic Search requests with > 10k results is the eventId
of the last event returned in the response. Some eventIds have double-quote chars in
them, which need to be escaped when passing the token in the next search request.
"""
unescaped_quote_pattern = r'[^\\]"'

return re.sub(
pattern=unescaped_quote_pattern,
repl=lambda match: match.group().replace('"', r"\""),
string=token,
)
17 changes: 9 additions & 8 deletions tests/services/test_savedsearch.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
SAVED_SEARCH_GET_RESPONSE = """
{"searches": [{"groups": [] }]}
"""
FILE_EVENT_URI = "/forensic-search/queryservice/api/v1/fileevent"


class TestSavedSearchService:
Expand All @@ -25,14 +26,14 @@ def test_get_by_id_calls_get_with_expected_uri(self, mock_connection, mocker):
mock_connection.get.return_value = create_mock_response(mocker, "{}")
file_event_service = FileEventService(mock_connection)
saved_search_service = SavedSearchService(mock_connection, file_event_service)
saved_search_service.get_by_id("TEst-id")
saved_search_service.get_by_id("test-id")
assert (
mock_connection.get.call_args[0][0]
== "/forensic-search/queryservice/api/v1/saved/TEst-id"
== "/forensic-search/queryservice/api/v1/saved/test-id"
)

def test_execute_calls_post_with_expected_uri(self, mock_connection, mocker):
response = create_mock_response(mocker, '{"searches": [{"groups": []}]}')
response = create_mock_response(mocker, SAVED_SEARCH_GET_RESPONSE)
mock_connection.post.return_value = response
file_event_service = FileEventService(mock_connection)
saved_search_service = SavedSearchService(mock_connection, file_event_service)
Expand Down Expand Up @@ -64,8 +65,8 @@ def test_execute_calls_post_with_expected_setting_page_param(

response = create_mock_response(mocker, SAVED_SEARCH_GET_RESPONSE)
mock_connection.get.return_value = response
file_event_client = FileEventService(mock_connection)
saved_search_client = SavedSearchService(mock_connection, file_event_client)
file_event_service = FileEventService(mock_connection)
saved_search_client = SavedSearchService(mock_connection, file_event_service)
saved_search_client.execute(
"test-id", page_number=test_custom_page_num,
)
Expand All @@ -87,8 +88,8 @@ def test_execute_calls_post_with_expected_page_params(

response = create_mock_response(mocker, SAVED_SEARCH_GET_RESPONSE)
mock_connection.get.return_value = response
file_event_client = FileEventService(mock_connection)
saved_search_client = SavedSearchService(mock_connection, file_event_client)
file_event_service = FileEventService(mock_connection)
saved_search_client = SavedSearchService(mock_connection, file_event_service)
saved_search_client.execute(
"test-id",
page_number=test_custom_page_num,
Expand All @@ -104,7 +105,7 @@ def test_execute_calls_post_with_expected_page_params(
)

def test_get_query_calls_get_with_expected_uri(self, mock_connection, mocker):
response = create_mock_response(mocker, '{"searches": [{"groups": []}]}')
response = create_mock_response(mocker, SAVED_SEARCH_GET_RESPONSE)
mock_connection.post.return_value = response
file_event_service = FileEventService(mock_connection)
saved_search_service = SavedSearchService(mock_connection, file_event_service)
Expand Down

0 comments on commit da8142c

Please sign in to comment.