Skip to content

Commit

Permalink
Saved search (#113)
Browse files Browse the repository at this point in the history
Co-authored-by: Alan Grgic <alan.grgic@code42.com>
  • Loading branch information
kiran-chaudhary and alanag13 committed May 28, 2020
1 parent 9ca9d9a commit 585dae6
Show file tree
Hide file tree
Showing 17 changed files with 369 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- master
tags:
- v*
pull_request:
pull_request:

jobs:
build:
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
The intended audience of this file is for py42 consumers -- as such, changes that don't affect
how a consumer would use the library (e.g. adding unit tests, updating documentation, etc) are not captured here.

## Unreleased

- `SavedSearchClient` available at `sdk.securitydata.savedsearches` with the following functions:
- `get()`
- `get_by_id()`
- `execute()`

## 1.2.0 - 2020-05-18

### Added
Expand Down
8 changes: 8 additions & 0 deletions docs/methoddocs/filleeventqueries.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
:show-inheritance:
```

## Saved Searches

```eval_rst
.. autoclass:: py42.clients.savedsearch.SavedSearchClient
:members:
:show-inheritance:
```

## Filter Classes

The following classes construct filters for file event queries. Each filter class corresponds to a file event detail.
Expand Down
37 changes: 25 additions & 12 deletions src/py42/_internal/client_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
from requests import HTTPError

from py42._internal.clients import alerts, archive, key_value_store, securitydata
from py42._internal.clients.alertrules import AlertRulesClient
from py42._internal.clients.detection_list_user import DetectionListUserClient
from py42.clients import administration, devices, legalhold, orgs, users
from py42._internal.clients.alertrules import AlertRulesClient
from py42.clients.detectionlists.departing_employee import DepartingEmployeeClient
from py42.clients.detectionlists.high_risk_employee import HighRiskEmployeeClient
from py42.clients.file_event import FileEventClient
from py42.clients.savedsearch import SavedSearchClient
from py42.exceptions import Py42FeatureUnavailableError, Py42SessionInitializationError


Expand Down Expand Up @@ -62,6 +63,8 @@ def __init__(
self._detection_list_user_client = None
self._ecm_session = None
self._alert_rules_client = None
self._saved_search_client = None
self._file_event_session = None

def get_alerts_client(self):
if not self._alerts_client:
Expand All @@ -71,35 +74,28 @@ def get_alerts_client(self):

def get_departing_employee_client(self):
if not self._departing_employee_client:
if not self._ecm_session:
self._ecm_session = self._get_jwt_session(u"employeecasemanagement-API_URL")
self._departing_employee_client = DepartingEmployeeClient(
self._ecm_session, self._user_context, self.get_detection_list_user_client()
self._get_ecm_session(), self._user_context, self.get_detection_list_user_client()
)
return self._departing_employee_client

def get_file_event_client(self):
if not self._file_event_client:
session = self._get_jwt_session(u"FORENSIC_SEARCH-API_URL")
self._file_event_client = FileEventClient(session)
self._file_event_client = FileEventClient(self._get_file_event_session())
return self._file_event_client

def get_high_risk_employee_client(self):
if not self._high_risk_employee_client:
if not self._ecm_session:
self._ecm_session = self._get_jwt_session(u"employeecasemanagement-API_URL")
self._high_risk_employee_client = HighRiskEmployeeClient(
self._ecm_session, self._user_context, self.get_detection_list_user_client()
self._get_ecm_session(), self._user_context, self.get_detection_list_user_client()
)
return self._high_risk_employee_client

def get_detection_list_user_client(self):
if not self._detection_list_user_client:
if not self._ecm_session:
self._ecm_session = self._get_jwt_session(u"employeecasemanagement-API_URL")
user_client = self._user_client
self._detection_list_user_client = DetectionListUserClient(
self._ecm_session, self._user_context, user_client
self._get_ecm_session(), self._user_context, user_client
)
return self._detection_list_user_client

Expand All @@ -111,10 +107,27 @@ def get_alert_rules_client(self):
)
return self._alert_rules_client

def get_saved_search_client(self):
if not self._saved_search_client:
self._saved_search_client = SavedSearchClient(
self._get_file_event_session(), self.get_file_event_client()
)
return self._saved_search_client

def _get_jwt_session(self, key):
url = self._get_stored_value(key)
return self._session_factory.create_jwt_session(url, self._root_session)

def _get_ecm_session(self):
if not self._ecm_session:
self._ecm_session = self._get_jwt_session(u"employeecasemanagement-API_URL")
return self._ecm_session

def _get_file_event_session(self):
if not self._file_event_session:
self._file_event_session = self._get_jwt_session(u"FORENSIC_SEARCH-API_URL")
return self._file_event_session

def _get_stored_value(self, key):
if not self._key_value_store_client:
url = _hacky_get_microservice_url(self._root_session, u"simple-key-value-store")
Expand Down
1 change: 0 additions & 1 deletion src/py42/_internal/clients/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from py42._internal.compat import str
from py42.clients import BaseClient
from py42.clients.util import get_all_pages

from py42.sdk.queries.query_filter import create_eq_filter_group


Expand Down
49 changes: 49 additions & 0 deletions src/py42/clients/savedsearch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from py42.clients import BaseClient
from py42.sdk.queries.fileevents.file_event_query import FileEventQuery


class SavedSearchClient(BaseClient):
"""A client to interact with saved search APIs."""

_version = u"v1"
_resource = u"/forensic-search/queryservice/api/{}/saved".format(_version)

def __init__(self, session, file_event_client):
super(SavedSearchClient, self).__init__(session)
self._file_event_client = file_event_client

def get(self):
"""Fetch details of existing saved searches.
Returns:
:class:`py42.response.Py42Response`
"""
uri = u"{}".format(self._resource)
return self._session.get(uri)

def get_by_id(self, search_id):
"""Fetch the details of a saved search by its given search Id.
Args:
search_id (str): Unique search Id of the saved search.
Returns:
:class:`py42.response.Py42Response`
"""
uri = u"{}/{}".format(self._resource, search_id)
return self._session.get(uri)

def execute(self, search_id, pg_num=1, pg_size=10000):
"""
Execute a saved search for given search Id and return its results.
Args:
search_id (str): Unique search Id of the saved search.
pg_num (int, optional): The consecutive group of results of size pg_size in the result set to return. Defaults to 1.
pg_size (int, optional): The maximum number of results to be returned. Defaults to 10,000.
Returns:
:class:`py42.response.Py42Response`
"""
response = self.get_by_id(search_id)
search = response[u"searches"][0]
query = FileEventQuery.from_dict(search)
return self._file_event_client.search(query)
9 changes: 9 additions & 0 deletions src/py42/modules/securitydata.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ def __init__(self, security_client, storage_client_factory, microservices_client
self._client_cache = {}
self._client_cache_lock = Lock()

@property
def savedsearches(self):
"""A collection of methods related to retrieving forensic search data.
Returns:
:class: `py42._internal.clients.securitydata.SavedSearchClient`
"""
return self._microservices_client_factory.get_saved_search_client()

def get_security_plan_storage_info_list(self, user_uid):
"""Gets IDs (plan UID, node GUID, and destination GUID) for the storage nodes containing
the file activity event data for the user with the given UID.
Expand Down
10 changes: 10 additions & 0 deletions src/py42/sdk/queries/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from py42.sdk.queries.query_filter import FilterGroup


class BaseQuery(object):
def __init__(self, *args, **kwargs):
self._filter_group_list = list(args)
Expand All @@ -9,6 +12,13 @@ def __init__(self, *args, **kwargs):
self.page_number = None
self.sort_key = None

@classmethod
def from_dict(cls, _dict, group_clause=u"AND"):
filter_groups = [
FilterGroup.from_dict(item, item[u"filterClause"]) for item in _dict[u"groups"]
]
return cls(*filter_groups, group_clause=group_clause)

@classmethod
def any(cls, *args):
return cls(*args, group_clause=u"OR")
Expand Down
14 changes: 14 additions & 0 deletions src/py42/sdk/queries/alerts/alert_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,17 @@ def __str__(self):
self.sort_key,
)
return json

def __iter__(self):
filter_group_list = [dict(item) for item in self._filter_group_list]
output_dict = {
u"tenantId": None,
u"groupClause": self._group_clause,
u"groups": filter_group_list,
u"pgNum": self.page_number,
u"pgSize": self.page_size,
u"srtDirection": self.sort_direction,
u"srtKey": self.sort_key,
}
for key in output_dict:
yield (key, output_dict[key])
13 changes: 13 additions & 0 deletions src/py42/sdk/queries/fileevents/file_event_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ def __str__(self):
)
return json

def __iter__(self):
filter_group_list = [dict(item) for item in self._filter_group_list]
output_dict = {
u"groupClause": self._group_clause,
u"groups": filter_group_list,
u"pgNum": self.page_number,
u"pgSize": self.page_size,
u"srtDir": self.sort_direction,
u"srtKey": self.sort_key,
}
for key in output_dict:
yield (key, output_dict[key])


def create_exists_filter_group(term):
filter_list = [create_query_filter(term, u"EXISTS")]
Expand Down
48 changes: 48 additions & 0 deletions src/py42/sdk/queries/query_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ class QueryFilter(object):
When :func:`str()` is called on a :class:`QueryFilter` instance, the (``term``, ``operator``,
``value``) attribute combination is transformed into a JSON string to be used as part of a
Forensic Search or Alert query.
When :func:`dict()` is called on a :class:`QueryFilter` instance, the (``term``, ``operator``,
``value``) attribute combination is transformed into the Python `dict` equivalent of their JSON representation. This can be useful
for programmatically manipulating a :class:`QueryFilter` after it's been created.
"""

_term = None
Expand All @@ -176,12 +180,33 @@ def __init__(self, term, operator, value=None):
self._operator = operator
self._value = value

@classmethod
def from_dict(cls, _dict):
return cls(_dict[u"term"], _dict[u"operator"], value=_dict.get(u"value"))

@property
def term(self):
return self._term

@property
def operator(self):
return self._operator

@property
def value(self):
return self._value

def __str__(self):
value = u"null" if self._value is None else u'"{0}"'.format(self._value)
return u'{{"operator":"{0}", "term":"{1}", "value":{2}}}'.format(
self._operator, self._term, value
)

def __iter__(self):
output_dict = {u"operator": self._operator, u"term": self._term, u"value": self._value}
for key in output_dict:
yield (key, output_dict[key])


class FilterGroup(object):
"""Class for constructing a logical sub-group of related filters from a list of QueryFilter
Expand All @@ -190,14 +215,37 @@ class FilterGroup(object):
When :func:`str()` is called on a :class:`FilterGroup` instance, the combined filter items are
transformed into a JSON string to be used as part of a Forensic Search or Alert query.
When :func:`dict()` is called on a :class:`FilterGroup` instance, the combined filter items are
transformed into the Python `dict` equivalent of their JSON representation. This can be useful
for programmatically manipulating a :class:`FilterGroup` after it's been created.
"""

def __init__(self, filter_list, filter_clause=u"AND"):
self._filter_list = filter_list
self._filter_clause = filter_clause

@classmethod
def from_dict(cls, _dict, filter_clause=u"AND"):
filter_list = [QueryFilter.from_dict(item) for item in _dict[u"filters"]]
return cls(filter_list, filter_clause=filter_clause)

@property
def filter_list(self):
return self._filter_list

@property
def filter_clause(self):
return self._filter_clause

def __str__(self):
filters_string = u",".join(str(filter_item) for filter_item in self._filter_list)
return u'{{"filterClause":"{0}", "filters":[{1}]}}'.format(
self._filter_clause, filters_string
)

def __iter__(self):
filter_list = [dict(item) for item in self._filter_list]
output_dict = {u"filterClause": self._filter_clause, u"filters": filter_list}
for key in output_dict:
yield (key, output_dict[key])
15 changes: 15 additions & 0 deletions tests/_internal/test_client_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,18 @@ def test_get_file_event_client_returns_same_intance_on_multiple_calls(
client2 = factory.get_file_event_client()

assert client1 is client2

def test_get_saved_search_client_calls_creates_client_with_expected_url(
self, mock_session, key_value_store_client, user_context, user_client, session_factory
):
key_value_store_client.get_stored_value.return_value.text = FILE_EVENTS_URL
factory = MicroserviceClientFactory(
TEST_ROOT_URL,
mock_session,
session_factory,
user_context,
user_client,
key_value_store_client,
)
factory.get_saved_search_client()
session_factory.create_jwt_session.assert_called_once_with(FILE_EVENTS_URL, mock_session)

0 comments on commit 585dae6

Please sign in to comment.