Skip to content

Commit

Permalink
INTEG-1038/alert detail json parsing (#107)
Browse files Browse the repository at this point in the history
* convert observation stringed_json to objects

* update changelog

* - a u
- version bump
  • Loading branch information
timabrmsn committed May 12, 2020
1 parent ab3747d commit 3e01765
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 7 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ 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.

## 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.

## 1.1.2 - 2020-05-11

### Added
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.2"
__version__ = "1.1.3"
14 changes: 13 additions & 1 deletion src/py42/_internal/clients/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ def get_details(self, alert_ids, tenant_id=None):
tenant_id = tenant_id if tenant_id else self._user_context.get_current_tenant_id()
uri = self._uri_prefix.format(u"query-details")
data = {u"tenantId": tenant_id, u"alertIds": alert_ids}
return self._session.post(uri, data=json.dumps(data))
results = self._session.post(uri, data=json.dumps(data))
return _convert_observation_json_strings_to_objects(results)

def resolve(self, alert_ids, tenant_id=None, reason=None):
if type(alert_ids) is not list:
Expand Down Expand Up @@ -85,3 +86,14 @@ def get_rules_by_name(self, rule_name):
if rule_name.lower() == rule[u"name"].lower():
matched_rules.append(rule)
return matched_rules


def _convert_observation_json_strings_to_objects(results):
for alert in results[u"alerts"]:
if u"observations" in alert:
for observation in alert[u"observations"]:
try:
observation[u"data"] = json.loads(observation[u"data"])
except:
continue
return results
88 changes: 83 additions & 5 deletions tests/clients/test_alerts.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import json

import pytest
from requests import Response

from py42._internal.clients.alerts import AlertClient
from py42._internal.session import Py42Session
from py42.response import Py42Response
from py42.exceptions import Py42Error
from py42.sdk.queries.alerts.alert_query import AlertQuery
from py42.sdk.queries.alerts.filters import AlertState
from tests.conftest import TENANT_ID_FROM_RESPONSE
Expand All @@ -14,6 +17,46 @@
, "totalCount": 1, "problems": []}
"""

TEST_PARSEABLE_ALERT_DETAIL_RESPONSE = """
{
"type$": "ALERT_DETAILS_RESPONSE",
"alerts": [
{
"type$": "ALERT_DETAILS",
"observations": [
{
"type$": "OBSERVATION",
"id": "example_obsv_id",
"observedAt": "2020-01-01T00:00:00.0000000Z",
"type": "FedEndpointExfiltration",
"data": "{\\"example_key\\":\\"example_string_value\\",\\"example_list\\":[\\"example_list_item_1\\",\\"example_list_item_2\\"]}"
}
]
}
]
}
"""

TEST_NON_PARSEABLE_ALERT_DETAIL_RESPONSE = """
{
"type$": "ALERT_DETAILS_RESPONSE",
"alerts": [
{
"type$": "ALERT_DETAILS",
"observations": [
{
"type$": "OBSERVATION",
"id": "example_obsv_id",
"observedAt": "2020-01-01T00:00:00.0000000Z",
"type": "FedEndpointExfiltration",
"data": "{\\"invalid_json\\": ][ }"
}
]
}
]
}
"""


@pytest.fixture
def mock_get_all_session(mocker, py42_response):
Expand Down Expand Up @@ -55,9 +98,10 @@ def test_search_posts_to_expected_url(self, mock_session, user_context, successf
assert mock_session.post.call_args[0][0] == u"/svc/api/v1/query-alerts"

def test_get_details_when_not_given_tenant_id_posts_expected_data(
self, mock_session, user_context, successful_response
self, mock_session, user_context, py42_response
):
mock_session.post.return_value = successful_response
py42_response.text = TEST_PARSEABLE_ALERT_DETAIL_RESPONSE
mock_session.post.return_value = py42_response
alert_client = AlertClient(mock_session, user_context)
alert_ids = ["ALERT_ID_1", "ALERT_ID_2"]
alert_client.get_details(alert_ids)
Expand All @@ -69,8 +113,10 @@ def test_get_details_when_not_given_tenant_id_posts_expected_data(
)

def test_get_details_when_given_single_alert_id_posts_expected_data(
self, mock_session, user_context, successful_post
self, mock_session, user_context, successful_post, py42_response
):
py42_response.text = TEST_PARSEABLE_ALERT_DETAIL_RESPONSE
mock_session.post.return_value = py42_response
alert_client = AlertClient(mock_session, user_context)
alert_client.get_details("ALERT_ID_1")
post_data = json.loads(mock_session.post.call_args[1]["data"])
Expand All @@ -80,8 +126,10 @@ def test_get_details_when_given_single_alert_id_posts_expected_data(
)

def test_get_details_when_given_tenant_id_posts_expected_data(
self, mock_session, user_context, successful_post
self, mock_session, user_context, successful_post, py42_response
):
py42_response.text = TEST_PARSEABLE_ALERT_DETAIL_RESPONSE
mock_session.post.return_value = py42_response
alert_client = AlertClient(mock_session, user_context)
alert_ids = ["ALERT_ID_1", "ALERT_ID_2"]
alert_client.get_details(alert_ids, "some-tenant-id")
Expand All @@ -92,12 +140,42 @@ def test_get_details_when_given_tenant_id_posts_expected_data(
and post_data["alertIds"][1] == "ALERT_ID_2"
)

def test_get_details_posts_to_expected_url(self, mock_session, user_context, successful_post):
def test_get_details_posts_to_expected_url(
self, mock_session, user_context, successful_post, py42_response
):
py42_response.text = TEST_PARSEABLE_ALERT_DETAIL_RESPONSE
mock_session.post.return_value = py42_response
alert_client = AlertClient(mock_session, user_context)
alert_ids = ["ALERT_ID_1", "ALERT_ID_2"]
alert_client.get_details(alert_ids)
assert mock_session.post.call_args[0][0] == "/svc/api/v1/query-details"

def test_get_details_converts_json_observation_strings_to_objects(
self, mocker, mock_session, user_context
):
requests_response = mocker.MagicMock(spec=Response)
requests_response.text = TEST_PARSEABLE_ALERT_DETAIL_RESPONSE
py42_response = Py42Response(requests_response)
mock_session.post.return_value = py42_response
alert_client = AlertClient(mock_session, user_context)
response = alert_client.get_details("alert_id")
observation_data = response["alerts"][0]["observations"][0]["data"]
assert observation_data["example_key"] == "example_string_value"
assert type(observation_data["example_list"]) is list

def test_get_details_when_observation_data_not_parseable_remains_unchanged(
self, mocker, mock_session, user_context
):
requests_response = mocker.MagicMock(spec=Response)
requests_response.text = TEST_NON_PARSEABLE_ALERT_DETAIL_RESPONSE
py42_response = Py42Response(requests_response)
mock_session.post.return_value = py42_response
alert_client = AlertClient(mock_session, user_context)
response = alert_client.get_details("alert_id")
observation_data = response["alerts"][0]["observations"][0]["data"]
expected_observation_data = '{"invalid_json": ][ }'
assert observation_data == expected_observation_data

def test_resolve_when_not_given_tenant_id_posts_expected_data(
self, mock_session, user_context, successful_post
):
Expand Down

0 comments on commit 3e01765

Please sign in to comment.