From 052593cb60e4e8338a5af99a7b36a5f06571108b Mon Sep 17 00:00:00 2001 From: lcwiklinski-r7 Date: Tue, 14 Jan 2025 13:46:56 +0100 Subject: [PATCH 1/9] Duo admin Improve handling of 429s in task code --- plugins/duo_admin/.CHECKSUM | 6 +- plugins/duo_admin/help.md | 2 +- .../actions/get_logs/schema.py | 5 +- .../tasks/monitor_logs/schema.py | 4 +- .../tasks/monitor_logs/task.py | 55 +++++++++++++++++-- .../komand_duo_admin/util/constants.py | 1 + .../duo_admin/komand_duo_admin/util/util.py | 7 +++ plugins/duo_admin/plugin.spec.yaml | 3 +- .../expected/monitor_logs_rate_limit.json.exp | 10 ++++ .../monitor_logs_with_rate_limit.json.inp | 6 ++ .../duo_admin/unit_test/test_monitor_logs.py | 10 +++- 11 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 plugins/duo_admin/komand_duo_admin/util/util.py create mode 100644 plugins/duo_admin/unit_test/expected/monitor_logs_rate_limit.json.exp create mode 100644 plugins/duo_admin/unit_test/inputs/monitor_logs_with_rate_limit.json.inp diff --git a/plugins/duo_admin/.CHECKSUM b/plugins/duo_admin/.CHECKSUM index 67ebe691da..a60a868759 100644 --- a/plugins/duo_admin/.CHECKSUM +++ b/plugins/duo_admin/.CHECKSUM @@ -1,5 +1,5 @@ { - "spec": "4a94a77c37f17820e8768bb2850f6ee5", + "spec": "21815acfef998298925be284831306ce", "manifest": "672f0da4df4edb87ab669d69e435c5c7", "setup": "8a8919e13bd1afe4849427d3dae6dbf4", "schemas": [ @@ -17,7 +17,7 @@ }, { "identifier": "get_logs/schema.py", - "hash": "3502cb177351d18ff8a31266a49db228" + "hash": "412d7f456b6aba9f4a7b87092212e7d3" }, { "identifier": "get_phones_by_user_id/schema.py", @@ -49,7 +49,7 @@ }, { "identifier": "monitor_logs/schema.py", - "hash": "26f03015b329bc573a7e6f3a688fb861" + "hash": "ff4f7adf6cbae20cd793af79e763a06d" } ] } \ No newline at end of file diff --git a/plugins/duo_admin/help.md b/plugins/duo_admin/help.md index b665dc020a..2f8ae6e8b5 100644 --- a/plugins/duo_admin/help.md +++ b/plugins/duo_admin/help.md @@ -946,7 +946,7 @@ Example output: ## Troubleshooting -* Many actions in this plugin take a User ID as input. A User ID is not the username - instead it's a unique identifier e.g. DU9I6T0F7R2S1J4XZHHA. A User ID can be obtained by passing a username to the Get User Status action. +Many actions in this plugin take a User ID as input. A User ID is not the username - instead it's a unique identifier e.g. DU9I6T0F7R2S1J4XZHHA. A User ID can be obtained by passing a username to the Get User Status action. # Version History diff --git a/plugins/duo_admin/komand_duo_admin/actions/get_logs/schema.py b/plugins/duo_admin/komand_duo_admin/actions/get_logs/schema.py index 1e93938303..0ff701033c 100755 --- a/plugins/duo_admin/komand_duo_admin/actions/get_logs/schema.py +++ b/plugins/duo_admin/komand_duo_admin/actions/get_logs/schema.py @@ -4,7 +4,10 @@ class Component: - DESCRIPTION = "This action is used to get auth logs, limited to past 180 days.[Currentmillis.com](https://currentmillis.com/) is useful for finding a usable UNIX timestamp.Available inputs for parameters can be found in [Duo Admin API docs](https://duo.com/docs/adminapi#logs:~:text=The%20factor%20or%20method%20used%20for%20an%20authentication%20attempt.%20One%20of%3A)" + DESCRIPTION = "This action is used to get auth logs, limited to past 180 days. +[Currentmillis.com](https://currentmillis.com/) is useful for finding a usable UNIX timestamp. + +Available inputs for parameters can be found in [Duo Admin API docs](https://duo.com/docs/adminapi#logs:~:text=The%20factor%20or%20method%20used%20for%20an%20authentication%20attempt.%20One%20of%3A)" class Input: diff --git a/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/schema.py b/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/schema.py index fc0dd65573..10d4b377e5 100755 --- a/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/schema.py +++ b/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/schema.py @@ -64,7 +64,9 @@ class MonitorLogsOutput(insightconnect_plugin_runtime.Output): "type": "array", "title": "Logs", "description": "List of administrator, authentication and trust monitor event logs within the specified time range", - "items": {}, + "items": { + "$ref": {} + }, "required": [ "logs" ], diff --git a/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py b/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py index f6db9f23e9..297a1a56b4 100755 --- a/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py +++ b/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py @@ -1,14 +1,17 @@ -from typing import Dict +from datetime import datetime, timedelta, timezone +from hashlib import sha1 +from time import time +from typing import Dict, Tuple, Any import insightconnect_plugin_runtime from insightconnect_plugin_runtime.exceptions import PluginException -from .schema import MonitorLogsInput, MonitorLogsOutput, MonitorLogsState, Component, Input - # Custom imports below +from komand_duo_admin.util.constants import Assistance from komand_duo_admin.util.exceptions import ApiException -from datetime import datetime, timedelta, timezone -from hashlib import sha1 +from komand_duo_admin.util.util import Utils + +from .schema import MonitorLogsInput, MonitorLogsOutput, MonitorLogsState, Component, Input ADMIN_LOGS_LOG_TYPE = "Admin logs" AUTH_LOGS_LOG_TYPE = "Auth logs" @@ -16,6 +19,7 @@ INITIAL_CUTOFF_HOURS = 24 MAX_CUTOFF_HOURS = 168 API_CUTOFF_HOURS = 4320 +RATE_LIMIT_DELAY = 600 class MonitorLogs(insightconnect_plugin_runtime.Task): @@ -30,6 +34,7 @@ class MonitorLogs(insightconnect_plugin_runtime.Task): PREVIOUS_ADMIN_LOG_HASHES = "previous_admin_log_hashes" PREVIOUS_AUTH_LOG_HASHES = "previous_auth_log_hashes" PREVIOUS_TRUST_MONITOR_EVENT_HASHES = "previous_trust_monitor_event_hashes" + RATE_LIMIT_DATETIME = "rate_limit_datetime" def __init__(self): super(self.__class__, self).__init__( @@ -103,7 +108,44 @@ def get_parameters_for_query( 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 + def check_rate_limit(self, state: Dict): + rate_limited = state.get(self.RATE_LIMIT_DATETIME) + now = time() + if rate_limited: + rate_limit_string = Utils.convert_epoch_to_readable(rate_limited) + log_msg = f"Rate limit value stored in state: {rate_limit_string}. " + if rate_limited > now: + log_msg += "Still within rate limiting period, skipping task execution..." + self.logger.info(log_msg) + error = PluginException( + cause=PluginException.causes.get(PluginException.Preset.RATE_LIMIT), + assistance=Assistance.RATE_LIMIT, + ) + return error + + log_msg += "However no longer in rate limiting period, so task can be executed..." + del state[self.RATE_LIMIT_DATETIME] + self.logger.info(log_msg) + + def check_rate_limit_error(self, error: ApiException, status_code: int, state: dict, rate_limit_delay: int) -> Tuple[int, Any]: + if status_code == 429: + new_run_time = time() + rate_limit_delay # default to wait 10 minutes before the next run + try: + new_run_time_string = Utils.convert_epoch_to_readable(new_run_time) + self.logger.error(f"A rate limit error has occurred, task will resume after {new_run_time_string}") + state[self.RATE_LIMIT_DATETIME] = new_run_time + except Exception as err: + self.logger.error( + f"Unable to calculate new run time, no rate limiting applied to the state. Error: {repr(err)}", + exc_info=True, + ) + return 200, None + return status_code, error + def run(self, params={}, state={}, custom_config={}): # noqa: C901 + rate_limit_delay = custom_config.get("rate_limit_delay", RATE_LIMIT_DELAY) + if rate_limited := self.check_rate_limit(state): + return [], state, False, 429, rate_limited self.connection.admin_api.toggle_rate_limiting = False has_more_pages = False backward_comp_first_run = False @@ -257,7 +299,8 @@ def run(self, params={}, state={}, custom_config={}): # noqa: C901 state[self.PREVIOUS_TRUST_MONITOR_EVENT_HASHES] = [] state[self.PREVIOUS_ADMIN_LOG_HASHES] = [] state[self.PREVIOUS_AUTH_LOG_HASHES] = [] - return [], state, False, error.status_code, error + status_code, error = self.check_rate_limit_error(error, error.status_code, state, rate_limit_delay) + return [], state, False, status_code, error except Exception as error: self.logger.info(f"An Exception has been raised. Error: {error}") state[self.PREVIOUS_TRUST_MONITOR_EVENT_HASHES] = [] diff --git a/plugins/duo_admin/komand_duo_admin/util/constants.py b/plugins/duo_admin/komand_duo_admin/util/constants.py index 76a42056c5..ca1c86c749 100644 --- a/plugins/duo_admin/komand_duo_admin/util/constants.py +++ b/plugins/duo_admin/komand_duo_admin/util/constants.py @@ -19,6 +19,7 @@ class Assistance: VERIFY_INPUT = ( "Verify your input is correct and not malformed and try again. If the issue persists, please contact support." ) + RATE_LIMIT = "Task will resume collection of logs after the rate limiting period has expired." class PossibleInputs: diff --git a/plugins/duo_admin/komand_duo_admin/util/util.py b/plugins/duo_admin/komand_duo_admin/util/util.py new file mode 100644 index 0000000000..2507dd328c --- /dev/null +++ b/plugins/duo_admin/komand_duo_admin/util/util.py @@ -0,0 +1,7 @@ +from datetime import datetime + + +class Utils: + @staticmethod + def convert_epoch_to_readable(epoch_time: float) -> str: + return datetime.utcfromtimestamp(epoch_time).strftime("%Y-%m-%d %H:%M:%S") diff --git a/plugins/duo_admin/plugin.spec.yaml b/plugins/duo_admin/plugin.spec.yaml index b63e28d6eb..9380a8ea3b 100644 --- a/plugins/duo_admin/plugin.spec.yaml +++ b/plugins/duo_admin/plugin.spec.yaml @@ -47,8 +47,7 @@ links: - "[Duo Security](https://duo.com/)" references: - "[Duo Admin API](https://duo.com/docs/adminapi)" -troubleshooting: - - "Many actions in this plugin take a User ID as input. A User ID is not the username - instead it's a unique identifier e.g. DU9I6T0F7R2S1J4XZHHA. A User ID can be obtained by passing a username to the Get User Status action." +troubleshooting: "Many actions in this plugin take a User ID as input. A User ID is not the username - instead it's a unique identifier e.g. DU9I6T0F7R2S1J4XZHHA. A User ID can be obtained by passing a username to the Get User Status action." version_history: - "5.0.2 - Updated SDK to the latest version (v6.2.2) | Address vulnerabilities" - "5.0.1 - Update to enable Plugin as FedRAMP ready | Update SDK (`6.1.2`)" diff --git a/plugins/duo_admin/unit_test/expected/monitor_logs_rate_limit.json.exp b/plugins/duo_admin/unit_test/expected/monitor_logs_rate_limit.json.exp new file mode 100644 index 0000000000..c3ea7bf5c2 --- /dev/null +++ b/plugins/duo_admin/unit_test/expected/monitor_logs_rate_limit.json.exp @@ -0,0 +1,10 @@ +{ + "logs": [], + "state": { + "previous_admin_log_hashes": [], + "previous_auth_log_hashes": [], + "rate_limit_datetime": 3050853836.506636, + "previous_trust_monitor_event_hashes": [] +}, + "status_code": 429 +} diff --git a/plugins/duo_admin/unit_test/inputs/monitor_logs_with_rate_limit.json.inp b/plugins/duo_admin/unit_test/inputs/monitor_logs_with_rate_limit.json.inp new file mode 100644 index 0000000000..5731e01b98 --- /dev/null +++ b/plugins/duo_admin/unit_test/inputs/monitor_logs_with_rate_limit.json.inp @@ -0,0 +1,6 @@ +{ + "previous_admin_log_hashes": [], + "previous_auth_log_hashes": [], + "rate_limit_datetime": 3050853836.506636, + "previous_trust_monitor_event_hashes": [] +} diff --git a/plugins/duo_admin/unit_test/test_monitor_logs.py b/plugins/duo_admin/unit_test/test_monitor_logs.py index f74215a19e..e204bcaae8 100644 --- a/plugins/duo_admin/unit_test/test_monitor_logs.py +++ b/plugins/duo_admin/unit_test/test_monitor_logs.py @@ -21,7 +21,7 @@ class TestMonitorLogs(TestCase): @classmethod def setUpClass(cls) -> None: - cls.action = Util.default_connector(MonitorLogs()) + cls.task = Util.default_connector(MonitorLogs()) cls.custom_config = {"cutoff": {"date": "2023-04-30T08:34:46.000Z"}, "lookback": "2023-04-30T08:34:46.000Z"} @parameterized.expand( @@ -37,6 +37,12 @@ def setUpClass(cls) -> None: "lookback": "2023-04-30T08:34:46.000Z", }, ], + [ + "with_rate_limit", + Util.read_file_to_dict("inputs/monitor_logs_with_rate_limit.json.inp"), + Util.read_file_to_dict("expected/monitor_logs_rate_limit.json.exp"), + {}, + ], [ "with_state", Util.read_file_to_dict("inputs/monitor_logs_with_state.json.inp"), @@ -94,7 +100,7 @@ def test_monitor_logs( expected, config, ): - actual, actual_state, has_more_pages, status_code, _ = self.action.run( + actual, actual_state, has_more_pages, status_code, _ = self.task.run( state=current_state, custom_config=config ) self.assertEqual(actual, expected.get("logs")) From 33adf77daed7af4f35eb46ea5ba4e944e3f2909e Mon Sep 17 00:00:00 2001 From: lcwiklinski-r7 Date: Tue, 14 Jan 2025 13:48:52 +0100 Subject: [PATCH 2/9] Linting the code --- plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py | 4 +++- plugins/duo_admin/unit_test/test_monitor_logs.py | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py b/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py index 297a1a56b4..9147bc57b3 100755 --- a/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py +++ b/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py @@ -127,7 +127,9 @@ def check_rate_limit(self, state: Dict): del state[self.RATE_LIMIT_DATETIME] self.logger.info(log_msg) - def check_rate_limit_error(self, error: ApiException, status_code: int, state: dict, rate_limit_delay: int) -> Tuple[int, Any]: + def check_rate_limit_error( + self, error: ApiException, status_code: int, state: dict, rate_limit_delay: int + ) -> Tuple[int, Any]: if status_code == 429: new_run_time = time() + rate_limit_delay # default to wait 10 minutes before the next run try: diff --git a/plugins/duo_admin/unit_test/test_monitor_logs.py b/plugins/duo_admin/unit_test/test_monitor_logs.py index e204bcaae8..18ce03c91f 100644 --- a/plugins/duo_admin/unit_test/test_monitor_logs.py +++ b/plugins/duo_admin/unit_test/test_monitor_logs.py @@ -100,9 +100,7 @@ def test_monitor_logs( expected, config, ): - actual, actual_state, has_more_pages, status_code, _ = self.task.run( - state=current_state, custom_config=config - ) + actual, actual_state, has_more_pages, status_code, _ = self.task.run(state=current_state, custom_config=config) self.assertEqual(actual, expected.get("logs")) self.assertEqual(actual_state, expected.get("state")) self.assertEqual(status_code, expected.get("status_code")) From 1d974b5b22811cefa076ffd797e20e61082b3690 Mon Sep 17 00:00:00 2001 From: lcwiklinski-r7 Date: Tue, 14 Jan 2025 13:52:11 +0100 Subject: [PATCH 3/9] Version bump --- plugins/duo_admin/.CHECKSUM | 6 +++--- plugins/duo_admin/bin/komand_duo_admin | 2 +- plugins/duo_admin/help.md | 1 + plugins/duo_admin/plugin.spec.yaml | 3 ++- plugins/duo_admin/setup.py | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/duo_admin/.CHECKSUM b/plugins/duo_admin/.CHECKSUM index a60a868759..f5de971400 100644 --- a/plugins/duo_admin/.CHECKSUM +++ b/plugins/duo_admin/.CHECKSUM @@ -1,7 +1,7 @@ { - "spec": "21815acfef998298925be284831306ce", - "manifest": "672f0da4df4edb87ab669d69e435c5c7", - "setup": "8a8919e13bd1afe4849427d3dae6dbf4", + "spec": "8368d943b289ee91de79aa9a3f7f4ecf", + "manifest": "b4029998fe0d9bcc85c8016d009d19b3", + "setup": "5965f3fd331d7855550e5afad6ea8956", "schemas": [ { "identifier": "add_user/schema.py", diff --git a/plugins/duo_admin/bin/komand_duo_admin b/plugins/duo_admin/bin/komand_duo_admin index 884065b098..6ace0bb9a4 100755 --- a/plugins/duo_admin/bin/komand_duo_admin +++ b/plugins/duo_admin/bin/komand_duo_admin @@ -6,7 +6,7 @@ from sys import argv Name = "Duo Admin API" Vendor = "rapid7" -Version = "5.0.2" +Version = "5.0.3" Description = "[Duo](https://duo.com/)'s Trusted Access platform verifies the identity of your users with two-factor authentication and security health of their devices before they connect to the apps they use. Using the Duo plugin for InsightConnect will allow Duo user management within automation workflows" diff --git a/plugins/duo_admin/help.md b/plugins/duo_admin/help.md index 2f8ae6e8b5..ee5a9c51c9 100644 --- a/plugins/duo_admin/help.md +++ b/plugins/duo_admin/help.md @@ -950,6 +950,7 @@ Many actions in this plugin take a User ID as input. A User ID is not the userna # Version History +* 5.0.3 - Update Task `monitor_logs` to delay retry if a rate limit error is returned from Duo Admin * 5.0.2 - Updated SDK to the latest version (v6.2.2) | Address vulnerabilities * 5.0.1 - Update to enable Plugin as FedRAMP ready | Update SDK (`6.1.2`) * 5.0.0 - Updated to include latest SDK v5.5.5 | Removing Unused fields from User Object diff --git a/plugins/duo_admin/plugin.spec.yaml b/plugins/duo_admin/plugin.spec.yaml index 9380a8ea3b..8570661fa2 100644 --- a/plugins/duo_admin/plugin.spec.yaml +++ b/plugins/duo_admin/plugin.spec.yaml @@ -29,7 +29,7 @@ key_features: requirements: - "Two secret keys - `integration key` and `secret key`" - "`API hostname`" -version: 5.0.2 +version: 5.0.3 connection_version: 4 resources: source_url: https://github.com/rapid7/insightconnect-plugins/tree/master/plugins/duo_admin @@ -49,6 +49,7 @@ references: - "[Duo Admin API](https://duo.com/docs/adminapi)" troubleshooting: "Many actions in this plugin take a User ID as input. A User ID is not the username - instead it's a unique identifier e.g. DU9I6T0F7R2S1J4XZHHA. A User ID can be obtained by passing a username to the Get User Status action." version_history: +- "5.0.3 - Update Task `monitor_logs` to delay retry if a rate limit error is returned from Duo Admin" - "5.0.2 - Updated SDK to the latest version (v6.2.2) | Address vulnerabilities" - "5.0.1 - Update to enable Plugin as FedRAMP ready | Update SDK (`6.1.2`)" - "5.0.0 - Updated to include latest SDK v5.5.5 | Removing Unused fields from User Object" diff --git a/plugins/duo_admin/setup.py b/plugins/duo_admin/setup.py index e89795d678..cc3257229c 100644 --- a/plugins/duo_admin/setup.py +++ b/plugins/duo_admin/setup.py @@ -3,7 +3,7 @@ setup(name="duo_admin-rapid7-plugin", - version="5.0.2", + version="5.0.3", description="[Duo](https://duo.com/)'s Trusted Access platform verifies the identity of your users with two-factor authentication and security health of their devices before they connect to the apps they use. Using the Duo plugin for InsightConnect will allow Duo user management within automation workflows", author="rapid7", author_email="", From cec7785e1c1e15403d547f96fc78d056d6ef95ef Mon Sep 17 00:00:00 2001 From: lcwiklinski-r7 Date: Tue, 14 Jan 2025 13:58:01 +0100 Subject: [PATCH 4/9] Adjusting styling --- .../inputs/monitor_logs_with_rate_limit.json.inp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/duo_admin/unit_test/inputs/monitor_logs_with_rate_limit.json.inp b/plugins/duo_admin/unit_test/inputs/monitor_logs_with_rate_limit.json.inp index 5731e01b98..04f01ee209 100644 --- a/plugins/duo_admin/unit_test/inputs/monitor_logs_with_rate_limit.json.inp +++ b/plugins/duo_admin/unit_test/inputs/monitor_logs_with_rate_limit.json.inp @@ -1,6 +1,6 @@ { - "previous_admin_log_hashes": [], - "previous_auth_log_hashes": [], - "rate_limit_datetime": 3050853836.506636, - "previous_trust_monitor_event_hashes": [] + "previous_admin_log_hashes": [], + "previous_auth_log_hashes": [], + "rate_limit_datetime": 3050853836.506636, + "previous_trust_monitor_event_hashes": [] } From 0b0e6810934f6f38201fe833c2f814f2d30a4445 Mon Sep 17 00:00:00 2001 From: lcwiklinski-r7 Date: Tue, 14 Jan 2025 14:21:12 +0100 Subject: [PATCH 5/9] Resolving static code analysis --- plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py b/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py index 9147bc57b3..5515e6967b 100755 --- a/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py +++ b/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py @@ -330,7 +330,7 @@ def add_log_type_field(logs: list, value: str) -> list: @staticmethod def sha1(log: dict) -> str: - hash_ = sha1() # nosec B303 + hash_ = sha1(usedforsecurity=False) # nosec B303 for key, value in log.items(): hash_.update(f"{key}{value}".encode("utf-8")) return hash_.hexdigest() From 70b8b10dbdcbf4f76e7703de3d8a21d93c790ebe Mon Sep 17 00:00:00 2001 From: lcwiklinski-r7 Date: Tue, 14 Jan 2025 14:37:53 +0100 Subject: [PATCH 6/9] Resolving plugin's validation --- plugins/duo_admin/.CHECKSUM | 4 ++-- plugins/duo_admin/help.md | 7 ++----- .../duo_admin/komand_duo_admin/actions/get_logs/schema.py | 5 +---- plugins/duo_admin/plugin.spec.yaml | 2 +- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/plugins/duo_admin/.CHECKSUM b/plugins/duo_admin/.CHECKSUM index f5de971400..eb6a25e4f1 100644 --- a/plugins/duo_admin/.CHECKSUM +++ b/plugins/duo_admin/.CHECKSUM @@ -1,5 +1,5 @@ { - "spec": "8368d943b289ee91de79aa9a3f7f4ecf", + "spec": "fad15a03e907369003cfbd533d509bdc", "manifest": "b4029998fe0d9bcc85c8016d009d19b3", "setup": "5965f3fd331d7855550e5afad6ea8956", "schemas": [ @@ -17,7 +17,7 @@ }, { "identifier": "get_logs/schema.py", - "hash": "412d7f456b6aba9f4a7b87092212e7d3" + "hash": "c00274766932d81a15c5afe9a50d4501" }, { "identifier": "get_phones_by_user_id/schema.py", diff --git a/plugins/duo_admin/help.md b/plugins/duo_admin/help.md index ee5a9c51c9..4e685a46d3 100644 --- a/plugins/duo_admin/help.md +++ b/plugins/duo_admin/help.md @@ -176,11 +176,8 @@ Example output: #### Get Authentication Logs -This action is used to get auth logs, limited to past 180 days. -[Currentmillis.com](https://currentmillis.com/) is -useful for finding a usable UNIX timestamp. - -Available inputs for parameters can be found in [Duo Admin API +This action is used to get auth logs, limited to past 180 days. [Currentmillis.com](https://currentmillis.com/) is +useful for finding a usable UNIX timestamp. Available inputs for parameters can be found in [Duo Admin API docs](https://duo.com/docs/adminapi#logs:~:text=The%20factor%20or%20method%20used%20for%20an%20authentication%20attempt.%20One%20of%3A) ##### Input diff --git a/plugins/duo_admin/komand_duo_admin/actions/get_logs/schema.py b/plugins/duo_admin/komand_duo_admin/actions/get_logs/schema.py index 0ff701033c..5d7f1ec352 100755 --- a/plugins/duo_admin/komand_duo_admin/actions/get_logs/schema.py +++ b/plugins/duo_admin/komand_duo_admin/actions/get_logs/schema.py @@ -4,10 +4,7 @@ class Component: - DESCRIPTION = "This action is used to get auth logs, limited to past 180 days. -[Currentmillis.com](https://currentmillis.com/) is useful for finding a usable UNIX timestamp. - -Available inputs for parameters can be found in [Duo Admin API docs](https://duo.com/docs/adminapi#logs:~:text=The%20factor%20or%20method%20used%20for%20an%20authentication%20attempt.%20One%20of%3A)" + DESCRIPTION = "This action is used to get auth logs, limited to past 180 days. [Currentmillis.com](https://currentmillis.com/) is useful for finding a usable UNIX timestamp. Available inputs for parameters can be found in [Duo Admin API docs](https://duo.com/docs/adminapi#logs:~:text=The%20factor%20or%20method%20used%20for%20an%20authentication%20attempt.%20One%20of%3A)" class Input: diff --git a/plugins/duo_admin/plugin.spec.yaml b/plugins/duo_admin/plugin.spec.yaml index 8570661fa2..4a2f59a0b3 100644 --- a/plugins/duo_admin/plugin.spec.yaml +++ b/plugins/duo_admin/plugin.spec.yaml @@ -1075,7 +1075,7 @@ actions: example: { "alias1": "alias1", "alias2": "alias2", "alias3": "alias3", "alias4": "alias4", "aliases": { "alias1": "alias1", "alias2": "alias2", "alias3": "alias3", "alias4": "alias4" }, "created": 1684765611, "email": "user@example.com", "isEnrolled": false, "notes": "Example", "realname": "Example", "status": "active", "userId": "DUCUULF6HBMZ43IG9MBH", "username": "Example" } get_logs: title: Get Authentication Logs - description: "This action is used to get auth logs, limited to past 180 days.\n[Currentmillis.com](https://currentmillis.com/) is useful for finding a usable UNIX timestamp.\n\nAvailable inputs for parameters can be found in [Duo Admin API docs](https://duo.com/docs/adminapi#logs:~:text=The%20factor%20or%20method%20used%20for%20an%20authentication%20attempt.%20One%20of%3A)" + description: "This action is used to get auth logs, limited to past 180 days. [Currentmillis.com](https://currentmillis.com/) is useful for finding a usable UNIX timestamp. Available inputs for parameters can be found in [Duo Admin API docs](https://duo.com/docs/adminapi#logs:~:text=The%20factor%20or%20method%20used%20for%20an%20authentication%20attempt.%20One%20of%3A)" input: mintime: title: Mintime From acdb94c80f8dd6297446923f7fccc5e2733799f9 Mon Sep 17 00:00:00 2001 From: lcwiklinski-r7 Date: Wed, 15 Jan 2025 14:23:05 +0100 Subject: [PATCH 7/9] CR fixes --- plugins/duo_admin/.CHECKSUM | 6 +++--- plugins/duo_admin/help.md | 9 ++++++--- .../komand_duo_admin/actions/get_logs/schema.py | 2 +- .../komand_duo_admin/tasks/monitor_logs/schema.py | 4 +--- .../komand_duo_admin/tasks/monitor_logs/task.py | 15 ++++----------- plugins/duo_admin/plugin.spec.yaml | 5 +++-- 6 files changed, 18 insertions(+), 23 deletions(-) diff --git a/plugins/duo_admin/.CHECKSUM b/plugins/duo_admin/.CHECKSUM index eb6a25e4f1..f98dbe2f32 100644 --- a/plugins/duo_admin/.CHECKSUM +++ b/plugins/duo_admin/.CHECKSUM @@ -1,5 +1,5 @@ { - "spec": "fad15a03e907369003cfbd533d509bdc", + "spec": "95c3a26e6ef8734d0a1abb73a67c12d6", "manifest": "b4029998fe0d9bcc85c8016d009d19b3", "setup": "5965f3fd331d7855550e5afad6ea8956", "schemas": [ @@ -17,7 +17,7 @@ }, { "identifier": "get_logs/schema.py", - "hash": "c00274766932d81a15c5afe9a50d4501" + "hash": "3502cb177351d18ff8a31266a49db228" }, { "identifier": "get_phones_by_user_id/schema.py", @@ -49,7 +49,7 @@ }, { "identifier": "monitor_logs/schema.py", - "hash": "ff4f7adf6cbae20cd793af79e763a06d" + "hash": "26f03015b329bc573a7e6f3a688fb861" } ] } \ No newline at end of file diff --git a/plugins/duo_admin/help.md b/plugins/duo_admin/help.md index 4e685a46d3..2d5bbd6fac 100644 --- a/plugins/duo_admin/help.md +++ b/plugins/duo_admin/help.md @@ -176,8 +176,11 @@ Example output: #### Get Authentication Logs -This action is used to get auth logs, limited to past 180 days. [Currentmillis.com](https://currentmillis.com/) is -useful for finding a usable UNIX timestamp. Available inputs for parameters can be found in [Duo Admin API +This action is used to get auth logs, limited to past 180 days. +[Currentmillis.com](https://currentmillis.com/) is +useful for finding a usable UNIX timestamp. + +Available inputs for parameters can be found in [Duo Admin API docs](https://duo.com/docs/adminapi#logs:~:text=The%20factor%20or%20method%20used%20for%20an%20authentication%20attempt.%20One%20of%3A) ##### Input @@ -943,7 +946,7 @@ Example output: ## Troubleshooting -Many actions in this plugin take a User ID as input. A User ID is not the username - instead it's a unique identifier e.g. DU9I6T0F7R2S1J4XZHHA. A User ID can be obtained by passing a username to the Get User Status action. +* Many actions in this plugin take a User ID as input. A User ID is not the username - instead it's a unique identifier e.g. DU9I6T0F7R2S1J4XZHHA. A User ID can be obtained by passing a username to the Get User Status action. # Version History diff --git a/plugins/duo_admin/komand_duo_admin/actions/get_logs/schema.py b/plugins/duo_admin/komand_duo_admin/actions/get_logs/schema.py index 5d7f1ec352..1e93938303 100755 --- a/plugins/duo_admin/komand_duo_admin/actions/get_logs/schema.py +++ b/plugins/duo_admin/komand_duo_admin/actions/get_logs/schema.py @@ -4,7 +4,7 @@ class Component: - DESCRIPTION = "This action is used to get auth logs, limited to past 180 days. [Currentmillis.com](https://currentmillis.com/) is useful for finding a usable UNIX timestamp. Available inputs for parameters can be found in [Duo Admin API docs](https://duo.com/docs/adminapi#logs:~:text=The%20factor%20or%20method%20used%20for%20an%20authentication%20attempt.%20One%20of%3A)" + DESCRIPTION = "This action is used to get auth logs, limited to past 180 days.[Currentmillis.com](https://currentmillis.com/) is useful for finding a usable UNIX timestamp.Available inputs for parameters can be found in [Duo Admin API docs](https://duo.com/docs/adminapi#logs:~:text=The%20factor%20or%20method%20used%20for%20an%20authentication%20attempt.%20One%20of%3A)" class Input: diff --git a/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/schema.py b/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/schema.py index 10d4b377e5..fc0dd65573 100755 --- a/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/schema.py +++ b/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/schema.py @@ -64,9 +64,7 @@ class MonitorLogsOutput(insightconnect_plugin_runtime.Output): "type": "array", "title": "Logs", "description": "List of administrator, authentication and trust monitor event logs within the specified time range", - "items": { - "$ref": {} - }, + "items": {}, "required": [ "logs" ], diff --git a/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py b/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py index 5515e6967b..e8da409535 100755 --- a/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py +++ b/plugins/duo_admin/komand_duo_admin/tasks/monitor_logs/task.py @@ -1,10 +1,10 @@ from datetime import datetime, timedelta, timezone -from hashlib import sha1 from time import time -from typing import Dict, Tuple, Any +from typing import Any, Dict, Tuple, Union import insightconnect_plugin_runtime from insightconnect_plugin_runtime.exceptions import PluginException +from insightconnect_plugin_runtime.helper import hash_sha1 # Custom imports below from komand_duo_admin.util.constants import Assistance @@ -108,7 +108,7 @@ def get_parameters_for_query( 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 - def check_rate_limit(self, state: Dict): + def check_rate_limit(self, state: Dict) -> Union[PluginException, None]: rate_limited = state.get(self.RATE_LIMIT_DATETIME) now = time() if rate_limited: @@ -328,18 +328,11 @@ def add_log_type_field(logs: list, value: str) -> list: log["log_type"] = value return logs - @staticmethod - def sha1(log: dict) -> str: - hash_ = sha1(usedforsecurity=False) # nosec B303 - for key, value in log.items(): - hash_.update(f"{key}{value}".encode("utf-8")) - return hash_.hexdigest() - def compare_hashes(self, previous_logs_hashes: list, new_logs: list): new_logs_hashes = [] logs_to_return = [] for log in new_logs: - hash_ = self.sha1(log) + hash_ = hash_sha1(log) if hash_ not in previous_logs_hashes: new_logs_hashes.append(hash_) logs_to_return.append(log) diff --git a/plugins/duo_admin/plugin.spec.yaml b/plugins/duo_admin/plugin.spec.yaml index 4a2f59a0b3..e0855e12cf 100644 --- a/plugins/duo_admin/plugin.spec.yaml +++ b/plugins/duo_admin/plugin.spec.yaml @@ -47,7 +47,8 @@ links: - "[Duo Security](https://duo.com/)" references: - "[Duo Admin API](https://duo.com/docs/adminapi)" -troubleshooting: "Many actions in this plugin take a User ID as input. A User ID is not the username - instead it's a unique identifier e.g. DU9I6T0F7R2S1J4XZHHA. A User ID can be obtained by passing a username to the Get User Status action." +troubleshooting: + - "Many actions in this plugin take a User ID as input. A User ID is not the username - instead it's a unique identifier e.g. DU9I6T0F7R2S1J4XZHHA. A User ID can be obtained by passing a username to the Get User Status action." version_history: - "5.0.3 - Update Task `monitor_logs` to delay retry if a rate limit error is returned from Duo Admin" - "5.0.2 - Updated SDK to the latest version (v6.2.2) | Address vulnerabilities" @@ -1075,7 +1076,7 @@ actions: example: { "alias1": "alias1", "alias2": "alias2", "alias3": "alias3", "alias4": "alias4", "aliases": { "alias1": "alias1", "alias2": "alias2", "alias3": "alias3", "alias4": "alias4" }, "created": 1684765611, "email": "user@example.com", "isEnrolled": false, "notes": "Example", "realname": "Example", "status": "active", "userId": "DUCUULF6HBMZ43IG9MBH", "username": "Example" } get_logs: title: Get Authentication Logs - description: "This action is used to get auth logs, limited to past 180 days. [Currentmillis.com](https://currentmillis.com/) is useful for finding a usable UNIX timestamp. Available inputs for parameters can be found in [Duo Admin API docs](https://duo.com/docs/adminapi#logs:~:text=The%20factor%20or%20method%20used%20for%20an%20authentication%20attempt.%20One%20of%3A)" + description: "This action is used to get auth logs, limited to past 180 days.\n[Currentmillis.com](https://currentmillis.com/) is useful for finding a usable UNIX timestamp.\n\nAvailable inputs for parameters can be found in [Duo Admin API docs](https://duo.com/docs/adminapi#logs:~:text=The%20factor%20or%20method%20used%20for%20an%20authentication%20attempt.%20One%20of%3A)" input: mintime: title: Mintime From 0b215884d9f999569fb8ab0a7678cad0f3d4a17f Mon Sep 17 00:00:00 2001 From: lcwiklinski-r7 Date: Thu, 16 Jan 2025 11:51:30 +0100 Subject: [PATCH 8/9] Additional test case --- .../duo_admin/unit_test/test_monitor_logs.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/plugins/duo_admin/unit_test/test_monitor_logs.py b/plugins/duo_admin/unit_test/test_monitor_logs.py index 18ce03c91f..21b717f5d3 100644 --- a/plugins/duo_admin/unit_test/test_monitor_logs.py +++ b/plugins/duo_admin/unit_test/test_monitor_logs.py @@ -1,5 +1,6 @@ import sys import os +from time import time sys.path.append(os.path.abspath("../")) @@ -104,3 +105,21 @@ def test_monitor_logs( self.assertEqual(actual, expected.get("logs")) self.assertEqual(actual_state, expected.get("state")) self.assertEqual(status_code, expected.get("status_code")) + + + def test_monitor_logs_with_rate_limit_whole_flow(self, mock_request, mock_request_instance, mock_get_headers, mock_get_time): + future_time_state = {"rate_limit_datetime": time() + 600} + passed_time_state = {"rate_limit_datetime": time() - 600} + + actual, new_state, has_more_pages, status_code, _ = self.task.run(state=future_time_state, custom_config={}) + + self.assertEqual(actual, []) + self.assertEqual(future_time_state, new_state) + self.assertEqual(has_more_pages, False) + self.assertEqual(status_code, 429) + + actual_2, new_state_2, _, status_code_2, _ = self.task.run(state=passed_time_state, custom_config={}) + + self.assertTrue(actual_2) + self.assertTrue(new_state_2) + self.assertEqual(status_code_2, 200) From 5070e5f075609a4ebaeefe8af08bb88c1b8053d7 Mon Sep 17 00:00:00 2001 From: lcwiklinski-r7 Date: Thu, 16 Jan 2025 12:19:33 +0100 Subject: [PATCH 9/9] Bumping the SDK and linting the code --- plugins/duo_admin/.CHECKSUM | 2 +- plugins/duo_admin/Dockerfile | 2 +- plugins/duo_admin/help.md | 2 +- plugins/duo_admin/plugin.spec.yaml | 4 ++-- plugins/duo_admin/unit_test/test_monitor_logs.py | 5 +++-- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/plugins/duo_admin/.CHECKSUM b/plugins/duo_admin/.CHECKSUM index f98dbe2f32..f7cc03f671 100644 --- a/plugins/duo_admin/.CHECKSUM +++ b/plugins/duo_admin/.CHECKSUM @@ -1,5 +1,5 @@ { - "spec": "95c3a26e6ef8734d0a1abb73a67c12d6", + "spec": "814e81dd40fcf4d884c984e0c451201e", "manifest": "b4029998fe0d9bcc85c8016d009d19b3", "setup": "5965f3fd331d7855550e5afad6ea8956", "schemas": [ diff --git a/plugins/duo_admin/Dockerfile b/plugins/duo_admin/Dockerfile index 12be6872d5..cf8394a81e 100644 --- a/plugins/duo_admin/Dockerfile +++ b/plugins/duo_admin/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=linux/amd64 rapid7/insightconnect-python-3-plugin:6.2.2 +FROM --platform=linux/amd64 rapid7/insightconnect-python-3-plugin:6.2.3 LABEL organization=rapid7 LABEL sdk=python diff --git a/plugins/duo_admin/help.md b/plugins/duo_admin/help.md index 2d5bbd6fac..a55a6d9141 100644 --- a/plugins/duo_admin/help.md +++ b/plugins/duo_admin/help.md @@ -950,7 +950,7 @@ Example output: # Version History -* 5.0.3 - Update Task `monitor_logs` to delay retry if a rate limit error is returned from Duo Admin +* 5.0.3 - Bump the SDK to version 6.2.3 | Update Task `monitor_logs` to delay retry if a rate limit error is returned from Duo Admin * 5.0.2 - Updated SDK to the latest version (v6.2.2) | Address vulnerabilities * 5.0.1 - Update to enable Plugin as FedRAMP ready | Update SDK (`6.1.2`) * 5.0.0 - Updated to include latest SDK v5.5.5 | Removing Unused fields from User Object diff --git a/plugins/duo_admin/plugin.spec.yaml b/plugins/duo_admin/plugin.spec.yaml index e0855e12cf..b5dde51c90 100644 --- a/plugins/duo_admin/plugin.spec.yaml +++ b/plugins/duo_admin/plugin.spec.yaml @@ -11,7 +11,7 @@ status: [] supported_versions: ["Duo Admin API 2024-09-17"] sdk: type: full - version: 6.2.2 + version: 6.2.3 user: nobody description: "[Duo](https://duo.com/)'s Trusted Access platform verifies the identity of your users with two-factor authentication and security health of their devices before they connect to the apps they use. Using the Duo plugin for InsightConnect will allow Duo user management within automation workflows" @@ -50,7 +50,7 @@ references: troubleshooting: - "Many actions in this plugin take a User ID as input. A User ID is not the username - instead it's a unique identifier e.g. DU9I6T0F7R2S1J4XZHHA. A User ID can be obtained by passing a username to the Get User Status action." version_history: -- "5.0.3 - Update Task `monitor_logs` to delay retry if a rate limit error is returned from Duo Admin" +- "5.0.3 - Bump the SDK to version 6.2.3 | Update Task `monitor_logs` to delay retry if a rate limit error is returned from Duo Admin" - "5.0.2 - Updated SDK to the latest version (v6.2.2) | Address vulnerabilities" - "5.0.1 - Update to enable Plugin as FedRAMP ready | Update SDK (`6.1.2`)" - "5.0.0 - Updated to include latest SDK v5.5.5 | Removing Unused fields from User Object" diff --git a/plugins/duo_admin/unit_test/test_monitor_logs.py b/plugins/duo_admin/unit_test/test_monitor_logs.py index 21b717f5d3..f867aa1aba 100644 --- a/plugins/duo_admin/unit_test/test_monitor_logs.py +++ b/plugins/duo_admin/unit_test/test_monitor_logs.py @@ -106,8 +106,9 @@ def test_monitor_logs( self.assertEqual(actual_state, expected.get("state")) self.assertEqual(status_code, expected.get("status_code")) - - def test_monitor_logs_with_rate_limit_whole_flow(self, mock_request, mock_request_instance, mock_get_headers, mock_get_time): + def test_monitor_logs_with_rate_limit_whole_flow( + self, mock_request, mock_request_instance, mock_get_headers, mock_get_time + ): future_time_state = {"rate_limit_datetime": time() + 600} passed_time_state = {"rate_limit_datetime": time() - 600}