diff --git a/Packs/Okta/Integrations/OktaEventCollector/OktaEventCollector.py b/Packs/Okta/Integrations/OktaEventCollector/OktaEventCollector.py index 93a4cb78de37..d31c54b3b387 100644 --- a/Packs/Okta/Integrations/OktaEventCollector/OktaEventCollector.py +++ b/Packs/Okta/Integrations/OktaEventCollector/OktaEventCollector.py @@ -1,6 +1,6 @@ from http import HTTPStatus from typing import cast - +from dateutil.parser import parse from CommonServerPython import * VENDOR = "okta" @@ -131,7 +131,14 @@ def get_last_run(events: List[dict], last_run_after, next_link) -> dict: if event.get('published') != last_time: break ids.append(event.get('uuid')) - last_time = datetime.strptime(str(last_time).lower().replace('z', ''), '%Y-%m-%dt%H:%M:%S.%f') + try: + last_time = datetime.strptime(str(last_time).lower().replace('z', ''), '%Y-%m-%dt%H:%M:%S.%f') + except ValueError: + last_time = parse(str(last_time).lower().replace('z', '')) + except Exception as e: # General exception + demisto.error(f'Unexpected error parsing published date from event: {e}') + return {} + return {'after': last_time.isoformat(), 'ids': ids, 'next_link': next_link} @@ -160,7 +167,7 @@ def fetch_events(client: Client, return events, next_link -def main(): # pragma: no cover +def main(): try: start_time_epoch = int(time.time()) demisto_params = demisto.params() @@ -179,7 +186,7 @@ def main(): # pragma: no cover get_events_command(client, events_limit, since=after.isoformat()) demisto.results('ok') - if command == 'okta-get-events': + elif command == 'okta-get-events': after = cast(datetime, dateparser.parse(demisto_args.get('from_date').strip())) events, _, _ = get_events_command(client, events_limit, since=after.isoformat()) command_results = CommandResults( @@ -205,10 +212,14 @@ def main(): # pragma: no cover last_run_after=last_run_after, last_object_ids=last_object_ids, next_link=next_link) demisto.debug(f'sending_events_to_xsiam: {len(events)}') send_events_to_xsiam(events[:events_limit], vendor=VENDOR, product=PRODUCT) - demisto.setLastRun(get_last_run(events, last_run_after, next_link)) + last_run = get_last_run(events, last_run_after, next_link) + if last_run: + demisto.setLastRun(get_last_run(events, last_run_after, next_link)) + else: + return_error('Unrecognized command: ' + demisto.command()) except Exception as e: - return_error(f'Failed to execute {demisto.command()} command. Error: {str(e)}') + return_error(f'Failed to execute {demisto.command()} command. Error: {e}') if __name__ in ('__main__', '__builtin__', 'builtins'): diff --git a/Packs/Okta/Integrations/OktaEventCollector/OktaEventCollector.yml b/Packs/Okta/Integrations/OktaEventCollector/OktaEventCollector.yml index c99544759b8e..2aa7f6b606b0 100644 --- a/Packs/Okta/Integrations/OktaEventCollector/OktaEventCollector.yml +++ b/Packs/Okta/Integrations/OktaEventCollector/OktaEventCollector.yml @@ -73,7 +73,7 @@ script: required: false description: Manual command to fetch events and display them. name: okta-get-events - dockerimage: demisto/fastapi:0.115.4.115067 + dockerimage: demisto/fastapi:0.115.5.117397 isfetchevents: true subtype: python3 marketplaces: diff --git a/Packs/Okta/Integrations/OktaEventCollector/OktaEventCollector_test.py b/Packs/Okta/Integrations/OktaEventCollector/OktaEventCollector_test.py index da96791f0dfe..3f85b3793f1b 100644 --- a/Packs/Okta/Integrations/OktaEventCollector/OktaEventCollector_test.py +++ b/Packs/Okta/Integrations/OktaEventCollector/OktaEventCollector_test.py @@ -1,9 +1,9 @@ from unittest.mock import MagicMock - +import dateutil.parser._parser import pytest from freezegun import freeze_time from OktaEventCollector import Client, DemistoException, fetch_events, get_events_command, get_last_run, main, remove_duplicates - +import requests_mock import demistomock as demisto @@ -71,11 +71,36 @@ def test_remove_duplicates(events, ids, result): '1d0844b6-3148-11ec-9027-a5b57ec5fbbb'], 'next_link': ''}), ([], '2022-04-17T12:31:36.667', - {'after': '2022-04-17T12:31:36.667000', 'ids': [], 'next_link': ''})]) + {'after': '2022-04-17T12:31:36.667000', 'ids': [], 'next_link': ''}) +]) def test_get_last_run(events, last_run_after, result): assert get_last_run(events, last_run_after, next_link='') == result +def test_get_last_run_with_different_format(): + events = [{'published': '2022-04-17T12:31:36', + 'uuid': '1d0844b6-3148-11ec-9027-a5b57ec5faaa'}, + {'published': '2022-04-17T12:32:36', + 'uuid': '1d0844b6-3148-11ec-9027-a5b57ec5fbbb'}, + {'published': '2022-04-17T12:33:36', + 'uuid': '1d0844b6-3148-11ec-9027-a5b57ec5fccc'}] + last_run_after = '2022-04-17T11:30:00' + expected_result = {'after': '2022-04-17T12:33:36', 'ids': ['1d0844b6-3148-11ec-9027-a5b57ec5fccc'], 'next_link': ''} + assert get_last_run(events, last_run_after, next_link='') == expected_result + + +def test_get_last_run_invalid_date_format(): + events = [{'published': '2022-04-17T12:31:36', + 'uuid': '1d0844b6-3148-11ec-9027-a5b57ec5faaa'}, + {'published': '2022-04-17T12:32:36', + 'uuid': '1d0844b6-3148-11ec-9027-a5b57ec5fbbb'}, + {'published': 'xxxyyyzzz', + 'uuid': '1d0844b6-3148-11ec-9027-a5b57ec5fccc'}] + last_run_after = '2022-04-17T11:30:00' + with pytest.raises(dateutil.parser._parser.ParserError): + get_last_run(events, last_run_after, next_link='') + + def test_get_events_success(dummy_client, mocker): mock_remove_duplicates = MagicMock() mock_remove_duplicates.return_value = [{'id': 1, @@ -143,7 +168,7 @@ def test_fetch_event(dummy_client, mocker): @freeze_time('2022-04-17T12:32:36.667Z') -def test_429_too_many_requests(mocker, requests_mock): +def test_429_too_many_requests(mocker): mock_events = [ { @@ -163,16 +188,6 @@ def test_429_too_many_requests(mocker, requests_mock): 'published': '2022-04-17T14:00:03.000Z' } ] - requests_mock.get( - 'https://testurl.com/api/v1/logs?since=2022-04-17T12%3A32%3A36.667000%2B00%3A00&sortOrder=ASCENDING&limit=5', - json=mock_events) - requests_mock.get('https://testurl.com/api/v1/logs?since=2022-04-17T14%3A00%3A03.000Z&sortOrder=ASCENDING&limit=5', - status_code=429, - reason='Too many requests', - headers={ - 'x-rate-limit-remaining': '0', - 'x-rate-limit-reset': '1698343702', - }) mocker.patch.object(demisto, 'command', return_value='fetch-events') mocker.patch.object(demisto, 'getLastRun', return_value={}) @@ -188,6 +203,72 @@ def test_429_too_many_requests(mocker, requests_mock): }) send_events_to_xsiam_mock = mocker.patch('OktaEventCollector.send_events_to_xsiam', return_value={}) - main() + with requests_mock.Mocker() as m: + m.get( + 'https://testurl.com/api/v1/logs?since=2022-04-17T12%3A32%3A36.667000%2B00%3A00&sortOrder=ASCENDING&limit=5', + json=mock_events) + m.get('https://testurl.com/api/v1/logs?since=2022-04-17T14%3A00%3A03.000Z&sortOrder=ASCENDING&limit=5', + status_code=429, + reason='Too many requests', + headers={ + 'x-rate-limit-remaining': '0', + 'x-rate-limit-reset': '1698343702', + }) + + main() send_events_to_xsiam_mock.assert_called_once_with(mock_events, vendor='okta', product='okta') + + +@freeze_time('2022-04-17T12:32:36.667Z') +@pytest.mark.parametrize("address, command", [ + ('https://testurl.com/api/v1/logs?sortOrder=ASCENDING&since=2022-04-16T12%3A32%3A36.667000&limit=5', 'okta-get-events'), + ('https://testurl.com/api/v1/logs?sortOrder=ASCENDING&since=2022-04-17T11%3A32%3A36.667000&limit=5', 'test-module') +]) +def test_okta_get_events(mocker, address, command): + + mock_events = [ + { + 'uuid': 1, + 'published': '2022-04-17T14:00:00.000Z' + }, + { + 'uuid': 2, + 'published': '2022-04-17T14:00:01.000Z' + }, + { + 'uuid': 3, + 'published': '2022-04-17T14:00:02.000Z' + }, + { + 'uuid': 4, + 'published': '2022-04-17T14:00:03.000Z' + } + ] + mocker.patch.object(demisto, 'command', return_value=command) + mocker.patch.object(demisto, 'getLastRun', return_value={}) + mocker.patch.object(demisto, 'args', return_value={ + 'from_date': '1 day', + 'should_push_events': True, + }) + mocker.patch.object(demisto, 'params', return_value={ + 'url': 'https://testurl.com', + 'api_key': { + 'password': 'TESTAPIKEY' + }, + 'limit': 5, + 'after': '2022-04-17T12:32:36.667Z', + 'proxy': False, + 'verify': False + }) + send_events_to_xsiam_mock = mocker.patch('OktaEventCollector.send_events_to_xsiam', return_value={}) + with requests_mock.Mocker() as m: + m.get( + address, + json=mock_events) + main() + + if command == 'test-module': + send_events_to_xsiam_mock.assert_not_called() + else: + send_events_to_xsiam_mock.assert_called_once_with(mock_events, vendor='okta', product='okta') diff --git a/Packs/Okta/ReleaseNotes/3_3_7.md b/Packs/Okta/ReleaseNotes/3_3_7.md new file mode 100644 index 000000000000..9ec5cfa336f7 --- /dev/null +++ b/Packs/Okta/ReleaseNotes/3_3_7.md @@ -0,0 +1,7 @@ + +#### Integrations + +##### Okta Event Collector + +- Improved implementation of date parsing for events. +- Updated the Docker image to: *demisto/fastapi:0.115.5.117397*. diff --git a/Packs/Okta/pack_metadata.json b/Packs/Okta/pack_metadata.json index 1860f150e7f7..3299a013d6ad 100644 --- a/Packs/Okta/pack_metadata.json +++ b/Packs/Okta/pack_metadata.json @@ -2,7 +2,7 @@ "name": "Okta", "description": "Integration with Okta's cloud-based identity management service.", "support": "xsoar", - "currentVersion": "3.3.6", + "currentVersion": "3.3.7", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "",