diff --git a/plugins/okta/.CHECKSUM b/plugins/okta/.CHECKSUM index 4be3320957..d3311adba9 100644 --- a/plugins/okta/.CHECKSUM +++ b/plugins/okta/.CHECKSUM @@ -1,7 +1,7 @@ { - "spec": "2a124bebc54c6ed4bbefa8caa038ce0f", - "manifest": "96a067bc255b4bd4eab6fb3dfae79354", - "setup": "f41cee058863b3ed32c6048d876f1ea1", + "spec": "3ad1604efd5761d7129ee19e728efb5d", + "manifest": "ae6c3d90c00d8b25576f218a99b62763", + "setup": "f0f0aa00f602f9aa8da621bfe91e7107", "schemas": [ { "identifier": "add_user_to_group/schema.py", diff --git a/plugins/okta/bin/komand_okta b/plugins/okta/bin/komand_okta index 44abefd671..fb929e9ab8 100755 --- a/plugins/okta/bin/komand_okta +++ b/plugins/okta/bin/komand_okta @@ -6,7 +6,7 @@ from sys import argv Name = "Okta" Vendor = "rapid7" -Version = "4.2.0" +Version = "4.2.1" Description = "Secure identity management and single sign-on to any application" diff --git a/plugins/okta/help.md b/plugins/okta/help.md index 45013ec488..15a6281272 100644 --- a/plugins/okta/help.md +++ b/plugins/okta/help.md @@ -1612,6 +1612,7 @@ by Okta themselves, or constructed by the plugin based on the information it has # Version History +* 4.2.1 - Monitor Logs task: filter previously returned log events | only update time checkpoint when an event is returned | update timestamp format | set cutoff time of 24 hours. * 4.2.0 - Monitor Logs task: return raw logs data without cleaning and use last log time as checkpoint in time for next run. * 4.1.1 - Monitor Logs task: strip http/https in hostname * 4.1.0 - New action Get User Groups | Update to latest SDK version diff --git a/plugins/okta/komand_okta/tasks/monitor_logs/task.py b/plugins/okta/komand_okta/tasks/monitor_logs/task.py index 392ffe5ca1..3fc632bf4f 100755 --- a/plugins/okta/komand_okta/tasks/monitor_logs/task.py +++ b/plugins/okta/komand_okta/tasks/monitor_logs/task.py @@ -28,19 +28,23 @@ def run(self, params={}, state={}): # pylint: disable=unused-argument parameters = {} try: now = self.get_current_time() - timedelta(minutes=1) # allow for latency of this being triggered - now_iso = now.isoformat() + now_iso = self.get_iso(now) + last_24_hours = self.get_iso(now - timedelta(hours=24)) # cut off point - never query beyond 24 hours next_page_link = state.get(self.NEXT_PAGE_LINK) if not state: self.logger.info("First run") - last_24_hours = now - timedelta(hours=24) - parameters = {"since": last_24_hours.isoformat(), "until": now_iso, "limit": 1000} - state[self.LAST_COLLECTION_TIMESTAMP] = now_iso + parameters = {"since": last_24_hours, "until": now_iso, "limit": 1000} + state[self.LAST_COLLECTION_TIMESTAMP] = last_24_hours # we only change this once we get new events else: if next_page_link: state.pop(self.NEXT_PAGE_LINK) self.logger.info("Getting the next page of results...") else: - parameters = {"since": state.get(self.LAST_COLLECTION_TIMESTAMP), "until": now_iso, "limit": 1000} + parameters = { + "since": self.get_since(state, last_24_hours), + "until": now_iso, + "limit": 1000, + } self.logger.info("Subsequent run...") try: self.logger.info(f"Calling Okta with parameters={parameters} and next_page={next_page_link}") @@ -49,11 +53,12 @@ def run(self, params={}, state={}): # pylint: disable=unused-argument if not next_page_link else self.connection.api_client.get_next_page(next_page_link) ) - next_page_link, new_logs = self.get_next_page_link(new_logs_resp.headers), new_logs_resp.json() + next_page_link = self.get_next_page_link(new_logs_resp.headers) + new_logs = self.get_events(new_logs_resp.json(), state.get(self.LAST_COLLECTION_TIMESTAMP)) if next_page_link: state[self.NEXT_PAGE_LINK] = next_page_link has_more_pages = True - state[self.LAST_COLLECTION_TIMESTAMP] = self.get_last_collection_timestamp(now_iso, new_logs) + state[self.LAST_COLLECTION_TIMESTAMP] = self.get_last_collection_timestamp(new_logs, state) return new_logs, state, has_more_pages, 200, None except ApiException as error: return [], state, False, error.status_code, error @@ -64,8 +69,41 @@ def run(self, params={}, state={}): # pylint: disable=unused-argument def get_current_time(): return datetime.now(timezone.utc) + @staticmethod + def get_iso(time: datetime) -> str: + """ + Match the timestamp format used in old collector code and the format that Okta uses for 'published' to allow + comparison in `get_events`. e.g. '2023-10-02T15:43:51.450Z' + :param time: newly formatted time string value. + :return: formatted time string. + """ + return time.isoformat("T", "milliseconds").replace("+00:00", "Z") + + def get_since(self, state: dict, cut_off: str) -> str: + """ + If the customer has paused this task for an extended amount of time we don't want start polling events that + exceed 24 hours ago. Check if the saved state is beyond this and revert to use the last 24 hours time. + :param state: saved state to check and update the time being used. + :param cut_off: string time of now - 24 hours. + :return: updated time string to use in the parameters. + """ + saved_time = state.get(self.LAST_COLLECTION_TIMESTAMP) + + if saved_time < cut_off: + self.logger.info( + f"Saved state {saved_time} exceeds the cut off (24 hours)." f" Reverting to use time: {cut_off}" + ) + state[self.LAST_COLLECTION_TIMESTAMP] = cut_off + + return state[self.LAST_COLLECTION_TIMESTAMP] + @staticmethod def get_next_page_link(headers: dict) -> str: + """ + Find the next page of results link from the response headers. Header example: `link: ; rel="next"` + :param headers: response headers from the request to Okta. + :return: next page link if available. + """ links = headers.get("link").split(", ") next_link = None for link in links: @@ -73,13 +111,52 @@ def get_next_page_link(headers: dict) -> str: next_link = matched_link.group(1) if matched_link else None return next_link - def get_last_collection_timestamp(self, now: str, new_logs: list) -> str: + def get_events(self, logs: list, time: str) -> list: + """ + In the collector code we would iterate over all events and drop any that match the 'since' parameter to make + sure that we don't double ingest the same event from the previous run (see `get_last_collection_timestamp`). + :param logs: response json including all returned events from Okta. + :param time: 'since' parameter being used to query Okta. + :return: filtered_logs: removed any events that matched the query start time. + """ + + # If Okta returns only 1 event (no new events occurred) returned in a previous run we want to remove this. + if len(logs) == 1 and logs[0].get("published") == time: + self.logger.info("No new events found since last execution.") + return [] + + log = "Returning {filtered} log event(s) from this iteration." + pop_index, filtered_logs = 0, logs + + for index, event in enumerate(logs): + published = event.get("published") + if published and published > time: + pop_index = index + break + if pop_index: + filtered_logs = logs[pop_index:] + log += f" Removed {pop_index} event log(s) that should have been returned in previous iteration." + self.logger.info(log.format(filtered=len(filtered_logs))) + return filtered_logs + + def get_last_collection_timestamp(self, new_logs: list, state: dict) -> str: + """ + Mirror the behaviour in collector code to save the TS of the last parsed event as the 'since' time checkpoint. + If no new events found then we want to keep the current checkpoint the same. + :param new_logs: event logs returned from Okta. + :param state: access state dictionary to get the current checkpoint in time if no new logs. + :return: new time value to save as the checkpoint to query 'since' on the next run. + """ new_ts = "" - # Mirror the behaviour in collector code to save the TS of the last parsed event as the 'since' time checkpoint. if new_logs: # make sure that logs were returned from Okta otherwise will get index error new_ts = new_logs[-1].get("published") + self.logger.info(f"Saving the last record's published timestamp ({new_ts}) as checkpoint.") if not new_ts: - self.logger.warn(f'No published record to use as last timestamp, reverting to use "now" ({now})') - new_ts = now + state_time = state.get(self.LAST_COLLECTION_TIMESTAMP) + self.logger.warning( + f"No record to use as last timestamp, will not move checkpoint forward. " + f"Keeping value of {state_time}" + ) + new_ts = state_time return new_ts diff --git a/plugins/okta/plugin.spec.yaml b/plugins/okta/plugin.spec.yaml index d6097d2b58..88fe418d11 100644 --- a/plugins/okta/plugin.spec.yaml +++ b/plugins/okta/plugin.spec.yaml @@ -13,7 +13,7 @@ sdk: version: 5 user: nobody description: Secure identity management and single sign-on to any application -version: 4.2.0 +version: 4.2.1 connection_version: 4 resources: source_url: https://github.com/rapid7/insightconnect-plugins/tree/master/plugins/okta diff --git a/plugins/okta/setup.py b/plugins/okta/setup.py index 192fe11219..386c130e4a 100644 --- a/plugins/okta/setup.py +++ b/plugins/okta/setup.py @@ -3,7 +3,7 @@ setup(name="okta-rapid7-plugin", - version="4.2.0", + version="4.2.1", description="Secure identity management and single sign-on to any application", author="rapid7", author_email="", diff --git a/plugins/okta/unit_test/expected/get_logs.json.exp b/plugins/okta/unit_test/expected/get_logs.json.exp index efe18ce0ab..9ee23e4358 100644 --- a/plugins/okta/unit_test/expected/get_logs.json.exp +++ b/plugins/okta/unit_test/expected/get_logs.json.exp @@ -26,7 +26,7 @@ "outcome": { "result": "SUCCESS" }, - "published": "2023-04-27T07:49:21.764Z", + "published": "2023-04-27T08:49:21.764Z", "securityContext": { "asNumber": 123456, "asOrg": "test", @@ -97,7 +97,7 @@ "outcome": { "result": "SUCCESS" }, - "published": "2023-04-27T07:49:21.777Z", + "published": "2023-04-27T09:49:21.777Z", "securityContext": { "asNumber": 12345, "asOrg": "test", @@ -144,7 +144,7 @@ } ], "state": { - "last_collection_timestamp": "2023-04-27T07:49:21.777Z", + "last_collection_timestamp": "2023-04-27T09:49:21.777Z", "next_page_link": "https://example.com/nextLink?q=next" }, "has_more_pages": true, diff --git a/plugins/okta/unit_test/expected/get_logs_empty_resp.json.exp b/plugins/okta/unit_test/expected/get_logs_empty_resp.json.exp new file mode 100644 index 0000000000..48f65ed363 --- /dev/null +++ b/plugins/okta/unit_test/expected/get_logs_empty_resp.json.exp @@ -0,0 +1,8 @@ +{ + "logs": [], + "state": { + "last_collection_timestamp": "2023-04-27T08:33:46.123Z" + }, + "has_more_pages": false, + "status_code": 200 +} diff --git a/plugins/okta/unit_test/expected/get_logs_filtered.json.exp b/plugins/okta/unit_test/expected/get_logs_filtered.json.exp new file mode 100644 index 0000000000..f174d74b04 --- /dev/null +++ b/plugins/okta/unit_test/expected/get_logs_filtered.json.exp @@ -0,0 +1,81 @@ +{ + "logs": [ + { + "actor": { + "id": "12345", + "type": "User", + "alternateId": "user@example.com", + "displayName": "User 2" + }, + "client": { + "userAgent": { + "rawUserAgent": "python-requests/2.26.0", + "os": "Unknown", + "browser": "UNKNOWN" + }, + "zone": "null", + "device": "Unknown", + "ipAddress": "198.51.100.1", + "geographicalContext": {} + }, + "authenticationContext": { + "externalSessionId": "12345" + }, + "displayMessage": "Clear user session", + "eventType": "user.session.clear", + "outcome": { + "result": "SUCCESS" + }, + "published": "2023-04-27T09:49:21.777Z", + "securityContext": { + "asNumber": 12345, + "asOrg": "test", + "isp": "test", + "domain": "example.com", + "isProxy": false + }, + "severity": "INFO", + "debugContext": { + "debugData": { + "requestId": "12345", + "dtHash": "11111ecd0ecfb444ee1fcb9687ba8b174a3c8d251ce927e6016b871bc0222222", + "requestUri": "/api/v1/users/12345/lifecycle/suspend", + "url": "/api/v1/users/12345/lifecycle/suspend?" + } + }, + "legacyEventType": "core.user_auth.session_clear", + "transaction": { + "type": "WEB", + "id": "12345", + "detail": { + "requestApiTokenId": "12345" + } + }, + "uuid": "9de5069c-5afe-602b-2ea0-a04b66beb2c0", + "version": "0", + "request": { + "ipChain": [ + { + "ip": "198.51.100.1", + "geographicalContext": {}, + "version": "V4" + } + ] + }, + "target": [ + { + "id": "12345", + "type": "User", + "alternateId": "user@example.com", + "displayName": "User Test123" + } + ] + } + ], + "state": { + "last_collection_timestamp": "2023-04-27T09:49:21.777Z", + "next_page_link": "https://example.com/nextLink?q=next" + }, + "has_more_pages": true, + "status_code": 200 +} diff --git a/plugins/okta/unit_test/expected/get_logs_next_empty.json.exp b/plugins/okta/unit_test/expected/get_logs_next_empty.json.exp new file mode 100644 index 0000000000..7655ef74dd --- /dev/null +++ b/plugins/okta/unit_test/expected/get_logs_next_empty.json.exp @@ -0,0 +1,8 @@ +{ + "logs": [], + "state": { + "last_collection_timestamp": "2023-04-27T07:49:21.777Z" + }, + "has_more_pages": false, + "status_code": 200 +} diff --git a/plugins/okta/unit_test/inputs/monitor_logs_next_page.json.inp b/plugins/okta/unit_test/inputs/monitor_logs_next_page.json.inp index ead3d06433..04df883b1a 100644 --- a/plugins/okta/unit_test/inputs/monitor_logs_next_page.json.inp +++ b/plugins/okta/unit_test/inputs/monitor_logs_next_page.json.inp @@ -1,4 +1,4 @@ { - "last_collection_timestamp": "2023-04-27T08:34:46", + "last_collection_timestamp": "2023-04-27T07:49:21.777Z", "next_page_link": "https://example.com/nextLink?q=next" } diff --git a/plugins/okta/unit_test/inputs/monitor_logs_with_state.json.inp b/plugins/okta/unit_test/inputs/monitor_logs_with_state.json.inp index fad9bd4857..26e58bb0f3 100644 --- a/plugins/okta/unit_test/inputs/monitor_logs_with_state.json.inp +++ b/plugins/okta/unit_test/inputs/monitor_logs_with_state.json.inp @@ -1,3 +1,3 @@ { - "last_collection_timestamp": "2023-04-27T08:33:46" + "last_collection_timestamp": "2023-04-27T08:33:46.123Z" } diff --git a/plugins/okta/unit_test/responses/get_logs.json.resp b/plugins/okta/unit_test/responses/get_logs.json.resp index c25bae2aa3..05d79f58e0 100644 --- a/plugins/okta/unit_test/responses/get_logs.json.resp +++ b/plugins/okta/unit_test/responses/get_logs.json.resp @@ -25,7 +25,7 @@ "outcome": { "result": "SUCCESS" }, - "published": "2023-04-27T07:49:21.764Z", + "published": "2023-04-27T08:49:21.764Z", "securityContext": { "asNumber": 123456, "asOrg": "test", @@ -96,7 +96,7 @@ "outcome": { "result": "SUCCESS" }, - "published": "2023-04-27T07:49:21.777Z", + "published": "2023-04-27T09:49:21.777Z", "securityContext": { "asNumber": 12345, "asOrg": "test", diff --git a/plugins/okta/unit_test/responses/get_logs_empty_response.resp b/plugins/okta/unit_test/responses/get_logs_empty_response.resp new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/plugins/okta/unit_test/responses/get_logs_empty_response.resp @@ -0,0 +1 @@ +[] diff --git a/plugins/okta/unit_test/responses/get_logs_single_event.json.resp b/plugins/okta/unit_test/responses/get_logs_single_event.json.resp new file mode 100644 index 0000000000..c2caad00a0 --- /dev/null +++ b/plugins/okta/unit_test/responses/get_logs_single_event.json.resp @@ -0,0 +1,73 @@ +[ + { + "actor": { + "id": "12345", + "type": "User", + "alternateId": "user@example.com", + "displayName": "User 2" + }, + "client": { + "userAgent": { + "rawUserAgent": "python-requests/2.26.0", + "os": "Unknown", + "browser": "UNKNOWN" + }, + "zone": "null", + "device": "Unknown", + "ipAddress": "198.51.100.1", + "geographicalContext": {} + }, + "authenticationContext": { + "externalSessionId": "12345" + }, + "displayMessage": "Clear user session", + "eventType": "user.session.clear", + "outcome": { + "result": "SUCCESS" + }, + "published": "2023-04-27T07:49:21.777Z", + "securityContext": { + "asNumber": 12345, + "asOrg": "test", + "isp": "test", + "domain": "example.com", + "isProxy": false + }, + "severity": "INFO", + "debugContext": { + "debugData": { + "requestId": "12345", + "dtHash": "11111ecd0ecfb444ee1fcb9687ba8b174a3c8d251ce927e6016b871bc0222222", + "requestUri": "/api/v1/users/12345/lifecycle/suspend", + "url": "/api/v1/users/12345/lifecycle/suspend?" + } + }, + "legacyEventType": "core.user_auth.session_clear", + "transaction": { + "type": "WEB", + "id": "12345", + "detail": { + "requestApiTokenId": "12345" + } + }, + "uuid": "9de5069c-5afe-602b-2ea0-a04b66beb2c0", + "version": "0", + "request": { + "ipChain": [ + { + "ip": "198.51.100.1", + "geographicalContext": {}, + "version": "V4" + } + ] + }, + "target": [ + { + "id": "12345", + "type": "User", + "alternateId": "user@example.com", + "displayName": "User Test123" + } + ] + } +] diff --git a/plugins/okta/unit_test/test_monitor_logs.py b/plugins/okta/unit_test/test_monitor_logs.py index dd431d52e0..b20efd255d 100644 --- a/plugins/okta/unit_test/test_monitor_logs.py +++ b/plugins/okta/unit_test/test_monitor_logs.py @@ -1,21 +1,22 @@ -import sys -import os - -sys.path.append(os.path.abspath("../")) - from unittest import TestCase from komand_okta.tasks.monitor_logs.task import MonitorLogs from util import Util -from unittest.mock import patch +from unittest.mock import patch, call from parameterized import parameterized -from datetime import datetime +from datetime import datetime, timezone + +import sys +import os + +sys.path.append(os.path.abspath("../")) @patch( "komand_okta.tasks.monitor_logs.task.MonitorLogs.get_current_time", - return_value=datetime.strptime("2023-04-28T08:34:46", "%Y-%m-%dT%H:%M:%S"), + return_value=datetime(2023, 4, 28, 8, 34, 46, 123156, timezone.utc), ) @patch("requests.request", side_effect=Util.mock_request) +@patch("logging.Logger.warning") class TestMonitorLogs(TestCase): @classmethod def setUpClass(cls) -> None: @@ -38,10 +39,108 @@ def setUpClass(cls) -> None: Util.read_file_to_dict("inputs/monitor_logs_next_page.json.inp"), Util.read_file_to_dict("expected/get_logs_next_page.json.exp"), ], + [ + "next_page_no_results", + Util.read_file_to_dict("inputs/monitor_logs_next_page.json.inp"), + Util.read_file_to_dict("expected/get_logs_next_empty.json.exp"), + ], + [ + "without_state_no_results", + Util.read_file_to_dict("inputs/monitor_logs_without_state.json.inp"), + Util.read_file_to_dict("expected/get_logs_empty_resp.json.exp"), + ], ] ) - def test_monitor_logs(self, mock_request, mock_get_time, test_name, current_state, expected): + def test_monitor_logs(self, mocked_warn, mock_request, _mock_get_time, test_name, current_state, expected): + # Tests and their workflow descriptions: + # 1. without_state - first run, query from 24 hours ago until now and results returned. + # 2. with_state - queries using the saved 'last_collection_timestamp' to pull new logs. + # 3. next_page - state has `next_page_link` which returns more logs to parse. + # 4. next_page_no_results -`next_page_link` but the output of this is no logs - we don't move the TS forward. + # 5. without_state_no_results - first run but no results returned - save state as the 'since' parameter value + + if test_name in ["next_page_no_results", "without_state_no_results"]: + mock_request.side_effect = Util.mock_empty_response + actual, actual_state, has_more_pages, status_code, error = self.action.run(state=current_state) self.assertEqual(actual, expected.get("logs")) self.assertEqual(actual_state, expected.get("state")) self.assertEqual(has_more_pages, expected.get("has_more_pages")) + + # Check errors returned and logger warning only applied in tests 4 and 5. + self.assertEqual(error, None) + if mocked_warn.called: + log_call = call( + "No record to use as last timestamp, will not move checkpoint forward. " + f"Keeping value of {expected.get('state').get('last_collection_timestamp')}" + ) + self.assertIn(log_call, mocked_warn.call_args_list) + + @patch("logging.Logger.info") + def test_monitor_logs_filters_events(self, mocked_logger, *_mocks): + # Test the filtering of events returned in a previous iteration. Workflow being tested: + # 1. C2C executed and queried for events until 8am however the last event time was '2023-04-27T08:49:21.764Z' + # 2. The next execution will use this timestamp, meaning the last event will be returned again from Okta. + # 3. This duplicate event should be removed so that it is not returned to IDR again. + + current_state = {"last_collection_timestamp": "2023-04-27T08:49:21.764Z"} + expected = Util.read_file_to_dict("expected/get_logs_filtered.json.exp") + actual, actual_state, has_more_pages, status_code, error = self.action.run(state=current_state) + self.assertEqual(actual_state, expected.get("state")) + self.assertEqual(has_more_pages, expected.get("has_more_pages")) + + # make sure that the mocked response contained 2 log entries and that 1 is filtered out in `get_events` + expected_logs = expected.get("logs") + logger_call = call( + "Returning 1 log event(s) from this iteration. Removed 1 event log(s) that should have " + "been returned in previous iteration." + ) + + self.assertIn(logger_call, mocked_logger.call_args_list) + self.assertEqual(len(actual), len(expected_logs)) + self.assertEqual(actual, expected_logs) + + @patch("logging.Logger.info") + def test_monitor_logs_filters_single_event(self, mocked_info_log, mocked_warn_log, *mocks): + # Test filtering when a single event is returned that was in the previous iteration. + + # temp change mocked timestamp to be within the cutoff time without changing mocked response data. + mocks[1].return_value = datetime(2023, 4, 27, 8, 45, 46, 123156, timezone.utc) + now = "2023-04-28T08:33:46.123Z" # Mocked value of 'now' - 1 minute + current_state = {"last_collection_timestamp": "2023-04-27T07:49:21.777Z"} # TS of the event in mocked response + actual, actual_state, has_more_pages, status_code, error = self.action.run(state=current_state) + self.assertEqual(actual_state, current_state) # state has not changed because no new events. + self.assertNotEqual(actual_state.get("last_collection_timestamp"), now) # we have not moved the TS forward. + self.assertEqual(has_more_pages, False) # empty results so no next pages. + + # ensure sure that the mocked response contained a single entry that we discarded and logged this happening + logger_info_call = call("No new events found since last execution.") + logger_warn_call = call( + f"No record to use as last timestamp, will not move checkpoint forward. " + f'Keeping value of {current_state.get("last_collection_timestamp")}' + ) + + self.assertIn(logger_info_call, mocked_info_log.call_args_list) + self.assertIn(logger_warn_call, mocked_warn_log.call_args_list) + self.assertEqual(actual, []) # no events returned after filtering + + @patch("logging.Logger.info") + def test_monitor_logs_applies_cut_off(self, mocked_info_log, *_mocks): + # Test the scenario that a customer has paused the collector for an extended amount of time to test when they + # resume the task we should cut off, at a max 24 hours ago for since parameter. + expected = Util.read_file_to_dict("expected/get_logs.json.exp") # logs from last 24 hours + + paused_time = "2022-04-27T07:49:21.777Z" + current_state = {"last_collection_timestamp": paused_time} # Task has been paused for 1 year+ + actual, state, _has_more_pages, _status_code, _error = self.action.run(state=current_state) + + # Basic check that we match the same as a first test/run which returns logs from the last 24 hours + self.assertEqual(actual, expected.get("logs")) + self.assertEqual(state, expected.get("state")) + + # Check we called with the current parameters by looking at the info log + logger = call( + f"Saved state {paused_time} exceeds the cut off (24 hours). " + f"Reverting to use time: 2023-04-27T08:33:46.123Z" + ) + self.assertIn(logger, mocked_info_log.call_args_list) diff --git a/plugins/okta/unit_test/util.py b/plugins/okta/unit_test/util.py index af5ccb2b04..728af919b6 100644 --- a/plugins/okta/unit_test/util.py +++ b/plugins/okta/unit_test/util.py @@ -39,18 +39,15 @@ def read_file_to_dict(filename: str) -> dict: return json.loads(Util.read_file_to_string(filename)) @staticmethod - def mock_request(*args, **kwargs): - class MockResponse: - def __init__(self, status_code: int, filename: str = None, headers: dict = {}): - self.status_code = status_code - self.text = "" - self.headers = headers - if filename: - self.text = Util.read_file_to_string(f"responses/{filename}") + def mock_wrapper(url=""): + return Util.mock_request(url=url) - def json(self): - return json.loads(self.text) + @staticmethod + def mock_empty_response(**kwargs): + return MockResponse(200, "get_logs_empty_response.resp", {"link": ""}) + @staticmethod + def mock_request(*args, **kwargs): method = kwargs.get("method") url = kwargs.get("url") params = kwargs.get("params") @@ -58,10 +55,14 @@ def json(self): global first_request if url == "https://example.com/api/v1/logs": - if params == {"since": "2023-04-27T08:33:46", "until": "2023-04-28T08:33:46", "limit": 1000}: - return MockResponse( - 200, "get_logs.json.resp", {"link": ' rel="next"'} - ) + resp_args = { + "status_code": 200, + "filename": "get_logs.json.resp", + "headers": {"link": ' rel="next"'}, + } + if params.get("since") == "2023-04-27T07:49:21.777Z": + resp_args["filename"], resp_args["headers"] = "get_logs_single_event.json.resp", {"link": ""} + return MockResponse(**resp_args) if url == "https://example.com/nextLink?q=next": return MockResponse(200, "get_logs_next_page.json.resp", {"link": ""}) if url == "https://example.com/api/v1/groups/12345/users" and first_request: @@ -216,3 +217,15 @@ def json(self): return MockResponse(404) raise NotImplementedError("Not implemented", kwargs) + + +class MockResponse: + def __init__(self, status_code: int, filename: str = None, headers: dict = {}): + self.status_code = status_code + self.text = "" + self.headers = headers + if filename: + self.text = Util.read_file_to_string(f"responses/{filename}") + + def json(self): + return json.loads(self.text)