Skip to content

Commit

Permalink
Feature/alert rule lookups (#109)
Browse files Browse the repository at this point in the history
  • Loading branch information
alanag13 committed May 18, 2020
1 parent e8b5f30 commit 65c88f6
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 43 deletions.
24 changes: 16 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,30 @@ 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
## 1.2.0 - 2020-05-18

### Added

- `sdk.alerts.rules.get_by_observer_id()` to look up an alert rule by its observer id.

### Changed

- The following methods that required either a single str or list of string argument can now also accept a tuple of strings:
- `py42._internal.clients.alerts.AlertClient.get_details`
- `py42._internal.clients.alerts.AlertClient.resolve`
- `py42._internal.clients.alerts.AlertClient.reopen`
- `py42._internal.clients.detection_list_user.DetectionListUserClient.add_risk_tags`
- `py42._internal.clients.detection_list_user.DetectionListUserClient.remove_risk_tags`
- `sdk.alerts.get_details()`
- `sdk.alerts.resolve()`
- `sdk.alerts.reopen()`
- `sdk.detectionlists.add_risk_tags()`
- `sdk.detectionlists.remove_risk_tags()`

#### Removed

- `sdk.alerts.rules.get_by_name()`. Use `sdk.alerts.rules.get_all_by_name()` instead. It functions identically except for that it returns a generator of `Py42Response` objects rather than a list.

## 1.1.3 - 2020-05-12

### Changed

- `py42._internal.clients.alerts.AlertClient.get_details` now attempts to parse the "observation data" json string from the response data automatically.
- `sdk.alerts.get_details()` now attempts to parse the "observation data" json string from the response data automatically.

## 1.1.2 - 2020-05-11

Expand All @@ -33,7 +41,7 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta

- `Py42Response` items now have `__setitem__` support in order facilitate manipulating a response. For example, `response["users"][0]["username"] = "something else` is now possible.

- `Py42Resonse` items can now be iterated over multiple times.
- `Py42Response` items can now be iterated over multiple times.

## 1.1.1 - 2020-05-04

Expand Down
2 changes: 1 addition & 1 deletion src/py42/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# py42

__version__ = "1.1.3"
__version__ = "1.2.0"
44 changes: 33 additions & 11 deletions src/py42/_internal/clients/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from py42.clients import BaseClient
from py42.clients.util import get_all_pages

from py42.sdk.queries.query_filter import create_eq_filter_group


class AlertClient(BaseClient):
_uri_prefix = u"/svc/api/v1/{0}"
Expand Down Expand Up @@ -53,11 +55,17 @@ def _add_tenant_id_if_missing(self, query):
return str(query)

def _get_alert_rules(
self, tenant_id, sort_key=None, sort_direction=None, page_num=None, page_size=None
self,
tenant_id,
groups=None,
sort_key=None,
sort_direction=None,
page_num=None,
page_size=None,
):
data = {
u"tenantId": tenant_id,
u"groups": [],
u"groups": groups or [],
u"groupClause": u"AND",
u"pgNum": page_num - 1, # Minus 1, as this API expects first page to start with zero.
u"pgSize": page_size,
Expand All @@ -73,19 +81,33 @@ def get_all_rules(self, sort_key=u"CreatedAt", sort_direction=u"DESC"):
self._get_alert_rules,
u"ruleMetadata",
tenant_id=tenant_id,
groups=None,
sort_key=sort_key,
sort_direction=sort_direction,
)

def get_rules_by_name(self, rule_name):
rule_pages = self.get_all_rules()
matched_rules = []
for rule_page in rule_pages:
rules = rule_page[u"ruleMetadata"]
for rule in rules:
if rule_name.lower() == rule[u"name"].lower():
matched_rules.append(rule)
return matched_rules
def get_all_rules_by_name(self, rule_name, sort_key=u"CreatedAt", sort_direction=u"DESC"):
tenant_id = self._user_context.get_current_tenant_id()
return get_all_pages(
self._get_alert_rules,
u"ruleMetadata",
tenant_id=tenant_id,
groups=[json.loads(str(create_eq_filter_group(u"Name", rule_name)))],
sort_key=sort_key,
sort_direction=sort_direction,
)

def get_rule_by_observer_id(self, observer_id, sort_key=u"CreatedAt", sort_direction=u"DESC"):
tenant_id = self._user_context.get_current_tenant_id()
results = get_all_pages(
self._get_alert_rules,
u"ruleMetadata",
tenant_id=tenant_id,
groups=[json.loads(str(create_eq_filter_group(u"ObserverRuleId", observer_id)))],
sort_key=sort_key,
sort_direction=sort_direction,
)
return next(results)


def _convert_observation_json_strings_to_objects(results):
Expand Down
19 changes: 16 additions & 3 deletions src/py42/modules/alertrules.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,27 @@ def get_all(self, sort_key="CreatedAt", sort_direction="DESC"):
alerts_client = self.microservice_client_factory.get_alerts_client()
return alerts_client.get_all_rules(sort_key=sort_key, sort_direction=sort_direction)

def get_by_name(self, rule_name):
def get_all_by_name(self, rule_name):
"""Search for matching rules by name.
Args:
rule_name (str): Rule name to search for, case insensitive search.
Returns:
generator: An object that iterates over :class:`py42.response.Py42Response` objects
that each contain a page of rules with the given name.
"""
alerts_client = self.microservice_client_factory.get_alerts_client()
return alerts_client.get_all_rules_by_name(rule_name)

def get_by_observer_id(self, observer_id):
"""Get the rule with the matching observer ID.
Args:
observer_id (str): The observer ID of the rule to return.
Returns
:list: List of dictionary containing rule-details.
:class:`py42.response.Py42Response`
"""
alerts_client = self.microservice_client_factory.get_alerts_client()
return alerts_client.get_rules_by_name(rule_name)
return alerts_client.get_rule_by_observer_id(observer_id)
63 changes: 47 additions & 16 deletions tests/clients/test_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ def test_reopen_posts_to_expected_url(self, mock_session, user_context, successf
alert_client.reopen(alert_ids, "some-tenant-id")
assert mock_session.post.call_args[0][0] == "/svc/api/v1/reopen-alert"

def test_get_all_posts_expected_data(self, mock_session, user_context):
def test_get_all_rules_posts_expected_data(self, mock_session, user_context):
alert_client = AlertClient(mock_session, user_context)

for _ in alert_client.get_all_rules(sort_key="key", sort_direction="ASC"):
Expand All @@ -284,21 +284,52 @@ def test_get_all_posts_expected_data(self, mock_session, user_context):
and posted_data["srtDirection"] == "ASC"
)

def test_get_by_name_filters_correct_record(self, mock_get_all_session, user_context):
alert_client = AlertClient(mock_get_all_session, user_context)
rule = alert_client.get_rules_by_name(u"TESTNAME")
assert len(rule) == 2

def test_get_by_name_filters_correct_record_case_insenstive_search(
self, mock_get_all_session, user_context
def test_get_all_rules_by_name_posts_expected_data(
self, mock_session, user_context, successful_post
):
alert_client = AlertClient(mock_get_all_session, user_context)
rule = alert_client.get_rules_by_name(u"TestName")
assert len(rule) == 2
alert_client = AlertClient(mock_session, user_context)
for _ in alert_client.get_all_rules_by_name(
"testname", sort_key="key", sort_direction="ASC"
):
break

def test_get_by_name_raises_exception_when_name_does_not_match(
self, mock_get_all_session, user_context
assert mock_session.post.call_count == 1
posted_data = json.loads(mock_session.post.call_args[1]["data"])
filter_group = posted_data["groups"][0]["filters"][0]

assert filter_group["term"] == "Name"
assert filter_group["operator"] == "IS"
assert filter_group["value"] == "testname"
assert mock_session.post.call_args[0][0] == "/svc/api/v1/rules/query-rule-metadata"
assert (
posted_data["tenantId"] == user_context.get_current_tenant_id()
and posted_data["groupClause"] == "AND"
and posted_data["pgNum"] == 0
and posted_data["pgSize"] == 1000
and posted_data["srtKey"] == "key"
and posted_data["srtDirection"] == "ASC"
)

def test_get_rule_by_observer_id_posts_expected_data(
self, mock_session, user_context, successful_post
):
alert_client = AlertClient(mock_get_all_session, user_context)
rule = alert_client.get_rules_by_name(u"TESTNAME2")
assert len(rule) == 0
alert_client = AlertClient(mock_session, user_context)
for _ in alert_client.get_rule_by_observer_id("testid"):
break

assert mock_session.post.call_count == 1
posted_data = json.loads(mock_session.post.call_args[1]["data"])
filter_group = posted_data["groups"][0]["filters"][0]

assert filter_group["term"] == "ObserverRuleId"
assert filter_group["operator"] == "IS"
assert filter_group["value"] == "testid"
assert mock_session.post.call_args[0][0] == "/svc/api/v1/rules/query-rule-metadata"
assert (
posted_data["tenantId"] == user_context.get_current_tenant_id()
and posted_data["groupClause"] == "AND"
and posted_data["pgNum"] == 0
and posted_data["pgSize"] == 1000
and posted_data["srtKey"] == "CreatedAt"
and posted_data["srtDirection"] == "DESC"
)
15 changes: 12 additions & 3 deletions tests/modules/test_alertrules.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,20 @@ def test_alert_rules_module_calls_get_all_with_expected_value(
alert_rules_module.get_all()
assert mock_alerts_client.get_all_rules.call_count == 1

def test_alert_rules_module_calls_get_by_name_with_expected_value(
def test_alert_rules_module_calls_get_all_by_name_with_expected_value(
self, mock_microservice_client_factory, mock_alerts_client
):
rule_name = u"test rule"
mock_microservice_client_factory.get_alerts_client.return_value = mock_alerts_client
alert_rules_module = AlertRulesModule(mock_microservice_client_factory)
alert_rules_module.get_by_name(rule_name)
mock_alerts_client.get_rules_by_name.assert_called_once_with(rule_name)
alert_rules_module.get_all_by_name(rule_name)
mock_alerts_client.get_all_rules_by_name.assert_called_once_with(rule_name)

def test_alert_rules_module_calls_get_rules_by_observer_id_with_expected_value(
self, mock_microservice_client_factory, mock_alerts_client
):
rule_id = u"test-rule-id"
mock_microservice_client_factory.get_alerts_client.return_value = mock_alerts_client
alert_rules_module = AlertRulesModule(mock_microservice_client_factory)
alert_rules_module.get_by_observer_id(rule_id)
mock_alerts_client.get_rule_by_observer_id.assert_called_once_with(rule_id)
6 changes: 6 additions & 0 deletions tests/modules/test_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from py42._internal.client_factories import MicroserviceClientFactory
from py42._internal.clients.alerts import AlertClient
from py42.modules.alerts import AlertsModule
from py42.modules.alertrules import AlertRulesModule
from py42.sdk.queries.fileevents.file_event_query import FileEventQuery


Expand All @@ -25,6 +26,11 @@ class TestAlertsModule(object):

_alert_ids = [u"test-id1", u"test-id2"]

def test_rules_returns_rules_module(self, mock_microservice_client_factory, mock_alerts_client):
mock_microservice_client_factory.get_alerts_client.return_value = mock_alerts_client
alert_module = AlertsModule(mock_microservice_client_factory)
assert type(alert_module.rules) == AlertRulesModule

def test_alerts_module_calls_search_with_expected_value(
self, mock_microservice_client_factory, mock_alerts_client, mock_file_event_query
):
Expand Down
7 changes: 6 additions & 1 deletion tests/sdk/test_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from py42._internal.session import Py42Session
from py42._internal.session_factory import SessionFactory
from py42.clients import administration, devices, legalhold, orgs, users
from py42.modules import archive as arch_mod, detectionlists, securitydata as sec_mod
from py42.modules import alerts, archive as arch_mod, detectionlists, securitydata as sec_mod
from py42.sdk import SDKClient
from py42.usercontext import UserContext

Expand Down Expand Up @@ -41,6 +41,11 @@ def test_has_device_client_set(self, mock_session_factory, success_requests_sess
sdk = SDKClient(deps)
assert type(sdk.devices) == devices.DeviceClient

def test_has_alert_module_set(self, mock_session_factory, success_requests_session):
deps = SDKDependencies(HOST_ADDRESS, mock_session_factory, success_requests_session)
sdk = SDKClient(deps)
assert type(sdk.alerts) == alerts.AlertsModule

def test_has_detection_lists_module_set(self, mock_session_factory, success_requests_session):
deps = SDKDependencies(HOST_ADDRESS, mock_session_factory, success_requests_session)
sdk = SDKClient(deps)
Expand Down

0 comments on commit 65c88f6

Please sign in to comment.