Skip to content

Commit

Permalink
[PLGN-611] Duo Admin Task Handle 403 errors and allow configuration o…
Browse files Browse the repository at this point in the history
…f event and log gathering (#2100) (#2107)

* Adding in handling for task 403 errors

* Removed unnecessary debug

* Updated to allow optional collection of events and logs for task

* Updated to allow optional collection of events and logs for task

* Updated to allow optional collection of events and logs for task

* Reverted unnecessary changes to help.md

* Fixed return code check for 403 in task

* Fixed return code check for 403 in task

* Fixed return code check for 403 in task

* Updated linting

* Updated unit tests due to change in response handling for task 403

* Removed unnecessary pylint ignore statement

* Update task inputs and outputs in help.md file

---------

Co-authored-by: Dympna Laverty <dympna_laverty@rapid7.com>
  • Loading branch information
dlaverty-r7 and Dympna Laverty authored Nov 10, 2023
1 parent 84ea3a3 commit 3d46e7f
Show file tree
Hide file tree
Showing 19 changed files with 193 additions and 88 deletions.
8 changes: 4 additions & 4 deletions plugins/duo_admin/.CHECKSUM
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"spec": "06747192e0d7ebbc68e4e74b2b2fa3db",
"manifest": "5b65aecc1f073d76de487003fd1021a4",
"setup": "45c71e3044251c1632883c44f708e2cf",
"spec": "af7f5046784c4bd506fbbc797d403782",
"manifest": "9aa65831ac901c7273abd96248e1a709",
"setup": "f61170938f3b53095c861c44ae7ee7e7",
"schemas": [
{
"identifier": "add_user/schema.py",
Expand Down Expand Up @@ -49,7 +49,7 @@
},
{
"identifier": "monitor_logs/schema.py",
"hash": "f8c9067346589ef6a11e81c6132015e2"
"hash": "ff4f7adf6cbae20cd793af79e763a06d"
}
]
}
2 changes: 1 addition & 1 deletion plugins/duo_admin/bin/komand_duo_admin
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ from sys import argv

Name = "Duo Admin API"
Vendor = "rapid7"
Version = "4.2.2"
Version = "4.3.0"
Description = "Duo is a trusted access solution for organizations. The Duo Admin plugin for Rapid7 InsightConnect allows users to manage and administrate their Duo organization"


Expand Down
17 changes: 15 additions & 2 deletions plugins/duo_admin/help.md
Original file line number Diff line number Diff line change
Expand Up @@ -707,13 +707,25 @@ This task is used to monitor administrator, authentication and trust monitor eve

##### Input

_This task does not contain any inputs._
|Name|Type|Default|Required|Description|Enum|Example|
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|collectTrustMonitorEvents|boolean|True|False|Whether to collect Trust Monitor events (note requires appropriate level of Duo Admin license)|None|False|
|collectAdminLogs|boolean|True|False|Whether to collect Admin logs (note requires appropriate level of Duo Admin license)|None|False|

Example input:

```
{
"collectAdminLogs": true,
"collectTrustMonitorEvents": true
}
```

##### Output

|Name|Type|Required|Description|Example|
|----|----|--------|-----------|-------|
|logs|[]log|True|List of administrator, authentication and trust monitor event logs within the specified time range|[]|
|logs|[]object|True|List of administrator, authentication and trust monitor event logs within the specified time range|[]|

Example output:

Expand Down Expand Up @@ -1021,6 +1033,7 @@ A User ID can be obtained by passing a username to the Get User Status action.

# Version History

* 4.3.0 - Monitor Logs task: Added inputs for collecting events and logs. Updated 403 error handling
* 4.2.2 - Monitor Logs task: updated unit tests
* 4.2.1 - Monitor Logs task: updated timestamp handling
* 4.2.0 - Monitor Logs task: removed formatting of task output
Expand Down
25 changes: 23 additions & 2 deletions plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ class Component:


class Input:
pass
COLLECTADMINLOGS = "collectAdminLogs"
COLLECTTRUSTMONITOREVENTS = "collectTrustMonitorEvents"


class State:
Expand All @@ -21,7 +22,27 @@ class Output:

class MonitorLogsInput(insightconnect_plugin_runtime.Input):
schema = json.loads(r"""
{}
{
"type": "object",
"title": "Variables",
"properties": {
"collectAdminLogs": {
"type": "boolean",
"title": "Collect Duo Admin Logs",
"description": "Whether to collect Admin logs (note requires appropriate level of Duo Admin license)",
"default": true,
"order": 2
},
"collectTrustMonitorEvents": {
"type": "boolean",
"title": "Collect Duo Trust Monitor Events",
"description": "Whether to collect Trust Monitor events (note requires appropriate level of Duo Admin license)",
"default": true,
"order": 1
}
},
"definitions": {}
}
""")

def __init__(self):
Expand Down
126 changes: 70 additions & 56 deletions plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import insightconnect_plugin_runtime
from insightconnect_plugin_runtime.exceptions import PluginException

from .schema import MonitorLogsInput, MonitorLogsOutput, MonitorLogsState, Component
from .schema import MonitorLogsInput, MonitorLogsOutput, MonitorLogsState, Component, Input

# Custom imports below
from komand_duo_admin.util.exceptions import ApiException
Expand Down Expand Up @@ -94,7 +94,6 @@ def get_parameters_for_query(self, log_type, now, last_log_timestamp, next_page_
self.logger.info(f"Retrieve data from {mintime} to {maxtime}. Get next page is set to {get_next_page}")
return mintime, maxtime, get_next_page

# pylint: disable=unused-argument
def run(self, params={}, state={}): # noqa: C901
self.connection.admin_api.toggle_rate_limiting = False
has_more_pages = False
Expand All @@ -105,6 +104,9 @@ def run(self, params={}, state={}): # noqa: C901
trust_monitor_next_page_params = state.get(self.TRUST_MONITOR_NEXT_PAGE_PARAMS)
auth_logs_next_page_params = state.get(self.AUTH_LOGS_NEXT_PAGE_PARAMS)
admin_logs_next_page_params = state.get(self.ADMIN_LOGS_NEXT_PAGE_PARAMS)
collect_trust_monitor_events = params.get(Input.COLLECTTRUSTMONITOREVENTS, True)
collect_admin_logs = params.get(Input.COLLECTADMINLOGS, True)

if last_collection_timestamp:
# Previously only one timestamp was held (the end of the collection window)
# This has been superceded by a latest timestamp per log type
Expand Down Expand Up @@ -134,67 +136,78 @@ def run(self, params={}, state={}): # noqa: C901
previous_auth_log_hashes = state.get(self.PREVIOUS_AUTH_LOG_HASHES, [])
new_trust_monitor_event_hashes, new_admin_log_hashes, new_auth_log_hashes = [], [], []

# Get trust monitor events
mintime, maxtime, get_next_page = self.get_parameters_for_query(
TRUST_MONITOR_EVENTS_LOG_TYPE,
now,
trust_monitor_last_log_timestamp,
trust_monitor_next_page_params,
backward_comp_first_run,
)

if (get_next_page and trust_monitor_next_page_params) or not get_next_page:
trust_monitor_events, trust_monitor_next_page_params = self.get_trust_monitor_event(
mintime, maxtime, trust_monitor_next_page_params
)
new_trust_monitor_events, new_trust_monitor_event_hashes = self.compare_hashes(
previous_trust_monitor_event_hashes, trust_monitor_events
)
new_logs.extend(new_trust_monitor_events)
state[self.TRUST_MONITOR_LAST_LOG_TIMESTAMP] = self.get_highest_timestamp(
if collect_trust_monitor_events:
# Get trust monitor events
mintime, maxtime, get_next_page = self.get_parameters_for_query(
TRUST_MONITOR_EVENTS_LOG_TYPE,
now,
trust_monitor_last_log_timestamp,
new_trust_monitor_events,
trust_monitor_next_page_params,
backward_comp_first_run,
TRUST_MONITOR_EVENTS_LOG_TYPE,
)
self.logger.info(f"{len(new_trust_monitor_events)} trust monitor events retrieved")
if new_trust_monitor_event_hashes:
state[self.PREVIOUS_TRUST_MONITOR_EVENT_HASHES] = new_trust_monitor_event_hashes

if trust_monitor_next_page_params:
state[self.TRUST_MONITOR_NEXT_PAGE_PARAMS] = trust_monitor_next_page_params
has_more_pages = True
elif state.get(self.TRUST_MONITOR_NEXT_PAGE_PARAMS):
state.pop(self.TRUST_MONITOR_NEXT_PAGE_PARAMS)

# Get admin logs
mintime, maxtime, get_next_page = self.get_parameters_for_query(
ADMIN_LOGS_LOG_TYPE,
now,
admin_logs_last_log_timestamp,
admin_logs_next_page_params,
backward_comp_first_run,
)

if (get_next_page and admin_logs_next_page_params) or not get_next_page:
admin_logs, admin_logs_next_page_params = self.get_admin_logs(
mintime, maxtime, admin_logs_next_page_params
)
new_admin_logs, new_admin_log_hashes = self.compare_hashes(previous_admin_log_hashes, admin_logs)
new_logs.extend(new_admin_logs)
state[self.ADMIN_LOGS_LAST_LOG_TIMESTAMP] = self.get_highest_timestamp(
admin_logs_last_log_timestamp, new_admin_logs, backward_comp_first_run, ADMIN_LOGS_LOG_TYPE
if (get_next_page and trust_monitor_next_page_params) or not get_next_page:
trust_monitor_events, trust_monitor_next_page_params = self.get_trust_monitor_event(
mintime, maxtime, trust_monitor_next_page_params
)
new_trust_monitor_events, new_trust_monitor_event_hashes = self.compare_hashes(
previous_trust_monitor_event_hashes, trust_monitor_events
)
new_logs.extend(new_trust_monitor_events)
state[self.TRUST_MONITOR_LAST_LOG_TIMESTAMP] = self.get_highest_timestamp(
trust_monitor_last_log_timestamp,
new_trust_monitor_events,
backward_comp_first_run,
TRUST_MONITOR_EVENTS_LOG_TYPE,
)
self.logger.info(f"{len(new_trust_monitor_events)} trust monitor events retrieved")
if new_trust_monitor_event_hashes:
state[self.PREVIOUS_TRUST_MONITOR_EVENT_HASHES] = new_trust_monitor_event_hashes

if trust_monitor_next_page_params:
state[self.TRUST_MONITOR_NEXT_PAGE_PARAMS] = trust_monitor_next_page_params
has_more_pages = True
elif state.get(self.TRUST_MONITOR_NEXT_PAGE_PARAMS):
state.pop(self.TRUST_MONITOR_NEXT_PAGE_PARAMS)
else:
self.logger.info(
f"Collect trust monitor events set to {collect_trust_monitor_events}. Do not attempt to collect trust monitor events"
)
self.logger.info(f"{len(new_admin_logs)} admin logs retrieved")

if new_admin_log_hashes:
state[self.PREVIOUS_ADMIN_LOG_HASHES] = new_admin_log_hashes
if collect_admin_logs:
# Get admin logs
mintime, maxtime, get_next_page = self.get_parameters_for_query(
ADMIN_LOGS_LOG_TYPE,
now,
admin_logs_last_log_timestamp,
admin_logs_next_page_params,
backward_comp_first_run,
)

if admin_logs_next_page_params:
state[self.ADMIN_LOGS_NEXT_PAGE_PARAMS] = admin_logs_next_page_params
has_more_pages = True
elif state.get(self.ADMIN_LOGS_NEXT_PAGE_PARAMS):
state.pop(self.ADMIN_LOGS_NEXT_PAGE_PARAMS)
if (get_next_page and admin_logs_next_page_params) or not get_next_page:
admin_logs, admin_logs_next_page_params = self.get_admin_logs(
mintime, maxtime, admin_logs_next_page_params
)
new_admin_logs, new_admin_log_hashes = self.compare_hashes(
previous_admin_log_hashes, admin_logs
)
new_logs.extend(new_admin_logs)
state[self.ADMIN_LOGS_LAST_LOG_TIMESTAMP] = self.get_highest_timestamp(
admin_logs_last_log_timestamp, new_admin_logs, backward_comp_first_run, ADMIN_LOGS_LOG_TYPE
)
self.logger.info(f"{len(new_admin_logs)} admin logs retrieved")

if new_admin_log_hashes:
state[self.PREVIOUS_ADMIN_LOG_HASHES] = new_admin_log_hashes
if admin_logs_next_page_params:
state[self.ADMIN_LOGS_NEXT_PAGE_PARAMS] = admin_logs_next_page_params
has_more_pages = True
elif state.get(self.ADMIN_LOGS_NEXT_PAGE_PARAMS):
state.pop(self.ADMIN_LOGS_NEXT_PAGE_PARAMS)
else:
self.logger.info(
f"Collect admin logs set to {collect_admin_logs}. Do not attempt to collect admin logs"
)

# Get auth logs
mintime, maxtime, get_next_page = self.get_parameters_for_query(
Expand Down Expand Up @@ -349,6 +362,7 @@ def get_trust_monitor_event(self, mintime: int, maxtime: int, next_page_params:
)
self.logger.info(f"Parameters for get trust monitor events set to {parameters}")
response = self.connection.admin_api.get_trust_monitor_events(parameters).get("response", {})
self.logger.info(f"Response returned from get trust monitor events: {response}")
offset = response.get("metadata", {}).get("next_offset")
if offset:
parameters["offset"] = offset
Expand Down
16 changes: 15 additions & 1 deletion plugins/duo_admin/komand_duo_admin/util/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
USER_ENDPOINT,
USER_PHONES_ENDPOINT,
USERS_ENDPOINT,
TASK_PATHS_ALLOW_403,
)


Expand Down Expand Up @@ -159,6 +160,15 @@ def make_request(self, method: str, path: str, params: dict = {}) -> requests.Re
params=params,
headers=self.get_headers(method=method.upper(), host=self.hostname, path=path, params=params),
)

if response.status_code == 403 and path in TASK_PATHS_ALLOW_403:
# Special case: A task user who only has permissions for certain endpoints should not error out.
# Log and return an empty response instead
self.logger.info(
f"Request to {path} returned 403 unauthorized. Not raising exception as may be authorized to hit other endpoint(s)"
)
self.logger.info(f"403 Response data returned for reference: {response.json()}")
return {}
self._handle_exceptions(response)
if 200 <= response.status_code < 300:
return response
Expand Down Expand Up @@ -218,6 +228,10 @@ def make_json_request(self, method: str, path: str, params: dict = {}) -> dict:
try:
self.logger.info(f"Request to path: {path}")
response = self.make_request(method=method, path=path, params=params)
return response.json()
if isinstance(response, requests.Response):
return response.json()
else:
return response
except json.decoder.JSONDecodeError as error:
self.logger.info(f"JSON error occurred decoding response from {path}")
raise PluginException(preset=PluginException.Preset.INVALID_JSON, data=error)
1 change: 1 addition & 0 deletions plugins/duo_admin/komand_duo_admin/util/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
USER_ENROLL_ENDPOINT = "/admin/v1/users/enroll"
USER_PHONES_ENDPOINT = "/admin/v1/users/{user_id}/phones"
USERS_ENDPOINT = "/admin/v1/users"
TASK_PATHS_ALLOW_403 = [ADMINISTRATOR_LOGS_ENDPOINT, TRUST_MONITOR_EVENTS_ENDPOINT]
17 changes: 16 additions & 1 deletion plugins/duo_admin/plugin.spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ sdk:
version: 5
user: nobody
description: Duo is a trusted access solution for organizations. The Duo Admin plugin for Rapid7 InsightConnect allows users to manage and administrate their Duo organization
version: 4.2.2
version: 4.3.0
connection_version: 4
resources:
source_url: https://github.com/rapid7/insightconnect-plugins/tree/master/plugins/duo_admin
Expand Down Expand Up @@ -1267,6 +1267,21 @@ tasks:
monitor_logs:
title: Monitor Logs
description: Monitor administrator, authentication and trust monitor event logs
input:
collectTrustMonitorEvents:
title: Collect Duo Trust Monitor Events
description: Whether to collect Trust Monitor events (note requires appropriate level of Duo Admin license)
type: boolean
required: false
default: true
example: false
collectAdminLogs:
title: Collect Duo Admin Logs
description: Whether to collect Admin logs (note requires appropriate level of Duo Admin license)
type: boolean
required: false
default: true
example: false
output:
logs:
title: Logs
Expand Down
2 changes: 1 addition & 1 deletion plugins/duo_admin/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@


setup(name="duo_admin-rapid7-plugin",
version="4.2.2",
version="4.3.0",
description="Duo is a trusted access solution for organizations. The Duo Admin plugin for Rapid7 InsightConnect allows users to manage and administrate their Duo organization",
author="rapid7",
author_email="",
Expand Down
11 changes: 8 additions & 3 deletions plugins/duo_admin/unit_test/test_add_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@


@patch("requests.request", side_effect=Util.mock_request)
@patch("komand_duo_admin.util.api.isinstance", return_value=True)
class TestAddUser(TestCase):
@classmethod
def setUpClass(cls) -> None:
Expand All @@ -33,7 +34,7 @@ def setUpClass(cls) -> None:
],
]
)
def test_add_user(self, mock_request, test_name, input_params, expected):
def test_add_user(self, mock_request, mock_request_instance, test_name, input_params, expected):
actual = self.action.run(input_params)
self.assertEqual(actual, expected)

Expand All @@ -53,7 +54,9 @@ def test_add_user(self, mock_request, test_name, input_params, expected):
],
]
)
def test_add_user_raise_api_exception(self, mock_request, test_name, input_parameters, cause, assistance):
def test_add_user_raise_api_exception(
self, mock_request, mock_request_instance, test_name, input_parameters, cause, assistance
):
with self.assertRaises(ApiException) as error:
self.action.run(input_parameters)
self.assertEqual(error.exception.cause, cause)
Expand All @@ -69,7 +72,9 @@ def test_add_user_raise_api_exception(self, mock_request, test_name, input_param
]
]
)
def test_add_user_raise_plugin_exception(self, mock_request, test_name, input_parameters, cause, assistance):
def test_add_user_raise_plugin_exception(
self, mock_request, mock_request_instance, test_name, input_parameters, cause, assistance
):
with self.assertRaises(PluginException) as error:
self.action.run(input_parameters)
self.assertEqual(error.exception.cause, cause)
Expand Down
Loading

0 comments on commit 3d46e7f

Please sign in to comment.