diff --git a/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/README.md b/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/README.md
new file mode 100644
index 00000000000..1f8735d4554
--- /dev/null
+++ b/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/README.md
@@ -0,0 +1,48 @@
+This is the SailPoint IdentityNow event collector integration for Cortex XSIAM.
+This integration was integrated and tested with version 3 of SailPoint API.
+
+## Configure SailPoint IdentityNow Event Collector on Cortex XSOAR
+
+1. Navigate to **Settings** > **Integrations** > **Servers & Services**.
+2. Search for SailPoint IdentityNow Event Collector.
+3. Click **Add instance** to create and configure a new integration instance.
+
+ | **Parameter** | **Required** |
+ | --- | --- |
+ | IdentityNow Server URL (e.g., https://{tenant}.api.identitynow.com)
In order to get the tenant name, follow this [link](https://developer.sailpoint.com/docs/api/getting-started/#find-your-tenant-name).| True |
+ | Client ID
In order to generate the Client ID and Client Secret, follow this [link](https://developer.sailpoint.com/docs/api/authentication/#generate-a-personal-access-token). | True |
+ | Client Secret | True |
+ | Max number of events per fetch | False |
+ | Trust any certificate (not secure) | False |
+ | Use system proxy settings | False |
+
+4. Click **Test** to validate the URLs, token, and connection.
+
+Note: After generating client credentials, it is required to allow the following scopes: sp, search, read.
+
+## Commands
+
+You can execute these commands from the Cortex XSIAM CLI, as part of an automation, or in a playbook.
+After you successfully execute a command, a DBot message appears in the War Room with the command details.
+
+### identitynow-get-events
+
+***
+Gets events from SailPoint IdentityNow. This command is used for developing/debugging and is to be used with caution, as it can create events, leading to event duplication and exceeding API request limitations.
+
+#### Base Command
+
+`identitynow-get-events`
+
+#### Input
+
+| **Argument Name** | **Description** | **Required** |
+| --- | --- | --- |
+| should_push_events | If true, the command will create events, otherwise it will only display them. Possible values are: true, false. Default is false. | Optional |
+| limit | Maximum number of results to return. Default is 50. | Optional |
+| from_date | Date from which to get events in the format of %Y-%m-%dT%H:%M:%S. | Optional |
+| from_id | An ID of the event to retrieve events from.| Optional |
+
+#### Context Output
+
+There is no context output for this command.
\ No newline at end of file
diff --git a/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/SailPointIdentityNowEventCollector.py b/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/SailPointIdentityNowEventCollector.py
new file mode 100644
index 00000000000..a31a39633cb
--- /dev/null
+++ b/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/SailPointIdentityNowEventCollector.py
@@ -0,0 +1,359 @@
+from datetime import datetime
+import demistomock as demisto
+from CommonServerPython import *
+import urllib3
+from dateutil import parser
+
+# Disable insecure warnings
+urllib3.disable_warnings()
+
+''' CONSTANTS '''
+
+DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
+VENDOR = 'sailpoint'
+PRODUCT = 'identitynow'
+CURRENT_TIME_STR = datetime.now().strftime(DATE_FORMAT)
+
+''' CLIENT CLASS '''
+
+
+class Client(BaseClient):
+ """Client class to interact with the service API
+ """
+
+ def __init__(self, client_id: str, client_secret: str, base_url: str, proxy: bool, verify: bool, token: str | None = None):
+ super().__init__(base_url=base_url, proxy=proxy, verify=verify)
+ self.client_id = client_id
+ self.client_secret = client_secret
+ self.token = token
+
+ try:
+ self.token = self.get_token()
+ self.headers = {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'Authorization': f'Bearer {self.token}'
+ }
+ except Exception as e:
+ raise Exception(f'Failed to get token. Error: {str(e)}')
+
+ def generate_token(self) -> str:
+ """
+ Generates an OAuth 2.0 token using client credentials.
+ Returns:
+ str: token
+ """
+ resp = self._http_request(
+ method='POST',
+ url_suffix="oauth/token",
+ data={
+ 'grant_type': 'client_credentials',
+ },
+ auth=(self.client_id, self.client_secret)
+ )
+
+ token = resp.get('access_token')
+ now_timestamp = arg_to_datetime('now').timestamp() # type:ignore
+ expiration_time = now_timestamp + resp.get('expires_in')
+ demisto.debug(f'Generated token that expires at: {expiration_time}.')
+ integration_context = get_integration_context()
+ integration_context.update({'token': token})
+ # Subtract 60 seconds from the expiration time to make sure the token is still valid
+ integration_context.update({'expires': expiration_time - 60})
+ set_integration_context(integration_context)
+
+ return token
+
+ def get_token(self) -> str:
+ """
+ Obtains token from integration context if available and still valid.
+ After expiration, new token are generated and stored in the integration context.
+ Returns:
+ str: token that will be added to authorization header.
+ """
+ integration_context = get_integration_context()
+ token = integration_context.get('token', '')
+ valid_until = integration_context.get('expires')
+
+ now_timestamp = arg_to_datetime('now').timestamp() # type:ignore
+ # if there is a key and valid_until, and the current time is smaller than the valid until
+ # return the current token
+ if token and valid_until and now_timestamp < valid_until:
+ demisto.debug(f'Using existing token that expires at: {valid_until}.')
+ return token
+
+ # else generate a token and update the integration context accordingly
+ token = self.generate_token()
+ demisto.debug('Generated a new token.')
+
+ return token
+
+ def search_events(self, from_date: str, limit: int, prev_id: str | None = None) -> List[Dict]:
+ """
+ Searches for events in SailPoint IdentityNow
+ Args:
+ from_date: The date from which to fetch events
+ limit: Maximum number of events to fetch
+ prev_id: The id of the last event fetched
+ Returns:
+ List of events
+ """
+ query: Dict = {
+ "indices": ["events"],
+ "queryType": "SAILPOINT",
+ "queryVersion": "5.2",
+ "sort": ["+created"] if not prev_id else ["+id"],
+ }
+ if prev_id:
+ query["query"] = {"query": "type:* "}
+ query["searchAfter"] = [prev_id]
+ else:
+ query["query"] = {"query": f"type:* AND created: [{from_date} TO now]"}
+ query["timeZone"] = "GMT"
+
+ url_suffix = f'/v3/search?limit={limit}'
+ demisto.debug(f'Searching for events with query: {query}.')
+ return self._http_request(method='POST', headers=self.headers, url_suffix=url_suffix, data=json.dumps(query))
+
+
+def test_module(client: Client) -> str:
+ """
+ Tests API connectivity and authentication
+ Args:
+ client: Client object with the API client
+ Returns:
+ 'ok' if test passed, anything else will fail the test
+ """
+
+ try:
+ fetch_events(
+ client=client,
+ limit=1,
+ last_run={},
+ )
+
+ except Exception as e:
+ if 'Forbidden' in str(e):
+ return 'Authorization Error: make sure API Key is correctly set'
+ else:
+ raise e
+
+ return 'ok'
+
+
+def get_events(client: Client, from_date: str, from_id: str | None, limit: int = 50) -> tuple[List[Dict], CommandResults]:
+ """
+ Gets events from the SailPoint IdentityNow API
+ Args:
+ client: Client object with the API client
+ limit: Maximum number of events to fetch
+ from_date: The date from which to get events
+ from_id: The ID of an event from which to start to get events from
+ Returns:
+ List of events and CommandResults object
+ """
+ events = client.search_events(
+ prev_id=from_id,
+ from_date=from_date,
+ limit=limit
+ )
+ demisto.debug(f'Got {len(events)} events.')
+ hr = tableToMarkdown(name='Test Events', t=events)
+ return events, CommandResults(readable_output=hr)
+
+
+def fetch_events(client: Client,
+ limit: int, last_run: dict) -> tuple[Dict, List[Dict]]:
+ """
+ Fetches events from the SailPoint IdentityNow API
+ Args:
+ client: Client object with the API client
+ last_run: Dict containing the last run data
+ limit: Maximum number of events to fetch per call
+ Returns:
+ Tuple with the next run data and the list of events fetched
+ """
+ # currently the API fails fetching events by id, so we are fetching by date only.
+ # Once the issue is resolved, we just need to uncomment the commented lines,
+ # and remove the dedup function and last_fetched_ids from everywhere..
+ demisto.debug(f'Starting fetch_events with last_run: {last_run}.')
+ # last_fetched_id = last_run.get('prev_id')
+ last_fetched_creation_date = last_run.get('prev_date', CURRENT_TIME_STR)
+ last_fetched_ids: list = last_run.get('last_fetched_ids', [])
+
+ all_events = []
+ remaining_events_to_fetch = limit
+ # since we allow the user to set the limit to 50,000, but the API only allows 10000 events per call
+ # we need to make multiple calls to the API to fetch all the events
+ while remaining_events_to_fetch > 0:
+ current_batch_to_fetch = min(remaining_events_to_fetch, 10000)
+ demisto.debug(f'trying to fetch {current_batch_to_fetch} events.')
+
+ events = client.search_events(
+ # prev_id=last_fetched_id
+ from_date=last_fetched_creation_date,
+ limit=current_batch_to_fetch
+ )
+ demisto.debug(f'Successfully fetched {len(events)} events in this cycle.')
+ if not events:
+ demisto.debug('No events fetched. Exiting the loop.')
+ events = dedup_events(events, last_fetched_ids)
+ if events:
+ last_fetched_event = events[-1]
+ last_fetched_id = last_fetched_event['id']
+ last_fetched_creation_date = last_fetched_event['created']
+ demisto.debug(
+ f'information of the last event in this cycle: id: {last_fetched_id}, created: {last_fetched_creation_date}.')
+ remaining_events_to_fetch -= len(events)
+ demisto.debug(f'{remaining_events_to_fetch} events are left to fetch in the next calls.')
+ last_fetched_ids = get_last_fetched_ids(events)
+ all_events.extend(events)
+ else:
+ # to avoid infinite loop, if no events are fetched, or all events are duplicates, exit the loop
+ break
+ # next_run = {'prev_id': last_fetched_id, 'prev_date': last_fetched_creation_date}
+ next_run = {'prev_date': last_fetched_creation_date, 'last_fetched_ids': last_fetched_ids}
+ demisto.debug(f'Done fetching. Sum of all events: {len(all_events)}, the next run is {next_run}.')
+ return next_run, all_events
+
+
+''' HELPER FUNCTIONS '''
+
+
+def dedup_events(events: List[Dict], last_fetched_ids: list) -> List[Dict]:
+ """
+ Dedupes the events fetched based on the last fetched ids and creation date.
+ This process is based on the assumption that the events are sorted by creation date.
+
+ Args:
+ events: List of events.
+ last_fetched_ids: List of the last fetched ids.
+ Returns:
+ List of deduped events.
+ """
+ if not last_fetched_ids:
+ demisto.debug("No last fetched ids. Skipping deduping.")
+ return events
+
+ demisto.debug(f"Starting deduping. Number of events before deduping: {len(events)}, last fetched ids: {last_fetched_ids}")
+
+ last_fetched_ids_set = set(last_fetched_ids)
+ deduped_events = [event for event in events if event['id'] not in last_fetched_ids_set]
+
+ demisto.debug(f"Done deduping. Number of events after deduping: {len(deduped_events)}")
+ return deduped_events
+
+
+def get_last_fetched_ids(events: List[Dict]) -> List[str]:
+ """
+ Gets the ids of the last fetched events
+ Args:
+ events: List of events, assumed to be sorted ASC by creation date
+ Returns:
+ List of the last fetched ids
+ """
+ last_creation_date = events[-1]['created']
+ return [event['id'] for event in events if event['created'] == last_creation_date]
+
+
+def add_time_and_status_to_events(events: List[Dict]) -> None:
+ """
+ Adds _time and _ENTRY_STATUS fields to events
+ Args:
+ events: List of events
+ Returns:
+ None
+ """
+ for event in events:
+ created = event['created']
+ created = parser.parse(created)
+
+ modified = event.get('modified')
+ if modified:
+ modified = parser.parse(modified)
+
+ is_modified = created and modified and modified > created
+ event['_time'] = modified.strftime(DATE_FORMAT) if is_modified else created.strftime(DATE_FORMAT)
+ event["_ENTRY_STATUS"] = "modified" if is_modified else "new"
+
+
+''' MAIN FUNCTION '''
+
+
+def main() -> None: # pragma: no cover
+ """
+ main function, parses params and runs command functions
+ """
+
+ params = demisto.params()
+ args = demisto.args()
+ command = demisto.command()
+ client_id = params.get('credentials', {}).get('identifier')
+ client_secret = params.get('credentials', {}).get('password')
+ base_url = params['url']
+ verify_certificate = not params.get('insecure', False)
+ proxy = params.get('proxy', False)
+ fetch_limit = arg_to_number(params.get('limit')) or 50000
+
+ demisto.debug(f'Command being called is {command}')
+ try:
+ client = Client(
+ client_id=client_id,
+ client_secret=client_secret,
+ base_url=base_url,
+ verify=verify_certificate,
+ proxy=proxy)
+
+ if command == 'test-module':
+ result = test_module(client)
+ return_results(result)
+
+ elif command == 'identitynow-get-events':
+ limit = arg_to_number(args.get('limit', 50)) or 50
+ should_push_events = argToBoolean(args.get('should_push_events', False))
+ time_to_start = arg_to_datetime(args.get('from_date'))
+ formatted_time_to_start = time_to_start.strftime(DATE_FORMAT) if time_to_start else CURRENT_TIME_STR
+ id_to_start = args.get('from_id')
+ if not (id_to_start or time_to_start) or (id_to_start and time_to_start):
+ raise DemistoException("Please provide either from_id or from_date.")
+ events, results = get_events(client, from_date=formatted_time_to_start,
+ from_id=id_to_start, limit=limit)
+ return_results(results)
+ if should_push_events:
+ add_time_and_status_to_events(events)
+ send_events_to_xsiam(
+ events,
+ vendor=VENDOR,
+ product=PRODUCT
+ )
+
+ elif command == 'fetch-events':
+
+ last_run = demisto.getLastRun()
+ next_run, events = fetch_events(
+ client=client,
+ limit=fetch_limit,
+ last_run=last_run,
+ )
+
+ add_time_and_status_to_events(events)
+ demisto.debug(f'Sending {len(events)} events to Xsiam.')
+ send_events_to_xsiam(
+ events,
+ vendor=VENDOR,
+ product=PRODUCT
+ )
+ demisto.setLastRun(next_run)
+ demisto.debug(f'Next run is set to: {next_run}.')
+ else:
+ raise NotImplementedError(f'Command {command} is not implemented')
+
+ # Log exceptions and return errors
+ except Exception as e:
+ return_error(f'Failed to execute {command} command.\nError:\n{str(e)}')
+
+
+''' ENTRY POINT '''
+
+if __name__ in ('__main__', '__builtin__', 'builtins'):
+ main()
diff --git a/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/SailPointIdentityNowEventCollector.yml b/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/SailPointIdentityNowEventCollector.yml
new file mode 100644
index 00000000000..a05afc89efd
--- /dev/null
+++ b/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/SailPointIdentityNowEventCollector.yml
@@ -0,0 +1,74 @@
+category: Analytics & SIEM
+sectionOrder:
+- Connect
+- Collect
+commonfields:
+ id: SailPointIdentityNowEventCollector
+ version: -1
+configuration:
+- display: IdentityNow Server URL (e.g., https://{tenant}.api.identitynow.com)
+ name: url
+ required: true
+ type: 0
+ section: Connect
+- display: Client ID
+ name: credentials
+ type: 9
+ required: true
+ displaypassword: Client Secret
+- defaultvalue: 50000
+ section: Collect
+ display: Max number of events per fetch
+ name: limit
+ required: false
+ type: 0
+- display: Trust any certificate (not secure)
+ name: insecure
+ required: false
+ type: 8
+ section: Connect
+- display: Use system proxy settings
+ name: proxy
+ required: false
+ type: 8
+ section: Connect
+description: This is the SailPoint IdentityNow event collector integration for Cortex XSIAM.
+display: SailPoint IdentityNow Event Collector
+name: SailPointIdentityNowEventCollector
+supportlevelheader: xsoar
+script:
+ commands:
+ - arguments:
+ - auto: PREDEFINED
+ defaultValue: 'false'
+ description: If true, the command will create events, otherwise it will only display them.
+ name: should_push_events
+ predefined:
+ - 'true'
+ - 'false'
+ required: false
+ - description: Maximum number of results to return.
+ name: limit
+ required: false
+ defaultValue: 50
+ - description: An ID of the event to retrieve events from.
+ name: from_id
+ required: false
+ - default: false
+ description: Date from which to get events.
+ name: from_date
+ deprecated: false
+ description: Gets events from SailPoint IdentityNow. This command is used for developing/debugging and is to be used with caution, as it can create events, leading to event duplication and exceeding API request limitations.
+
+ name: identitynow-get-events
+ dockerimage: demisto/python3:3.11.9.106403
+ isfetchevents: true
+ runonce: false
+ script: '-'
+ subtype: python3
+ type: python
+marketplaces:
+- marketplacev2
+fromversion: 8.4.0
+tests:
+- No tests (auto formatted)
diff --git a/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/SailPointIdentityNowEventCollector_description.md b/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/SailPointIdentityNowEventCollector_description.md
new file mode 100644
index 00000000000..d1529197c88
--- /dev/null
+++ b/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/SailPointIdentityNowEventCollector_description.md
@@ -0,0 +1,11 @@
+## SailPoint IdentityNow Event Collector
+
+**Identity Now Server URL**:
+In order to get the tenant name, follow this [link](https://developer.sailpoint.com/docs/api/getting-started/#find-your-tenant-name).
+
+
+**Client ID and Client Secret**:
+In order to generate the Client ID and Client Secret, follow this [link](https://developer.sailpoint.com/docs/api/authentication/#generate-a-personal-access-token).
+
+
+Note: After generating client credentials, it is required to allow the following scopes: sp, search, read.
\ No newline at end of file
diff --git a/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/SailPointIdentityNowEventCollector_image.png b/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/SailPointIdentityNowEventCollector_image.png
new file mode 100644
index 00000000000..f953f12dded
Binary files /dev/null and b/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/SailPointIdentityNowEventCollector_image.png differ
diff --git a/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/SailPointIdentityNowEventCollector_test.py b/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/SailPointIdentityNowEventCollector_test.py
new file mode 100644
index 00000000000..dc7c91833a1
--- /dev/null
+++ b/Packs/SailPointIdentityNow/Integrations/SailPointIdentityNowEventCollector/SailPointIdentityNowEventCollector_test.py
@@ -0,0 +1,228 @@
+import pytest
+import demistomock as demisto
+from SailPointIdentityNowEventCollector import fetch_events, add_time_and_status_to_events, Client, \
+ dedup_events, get_last_fetched_ids
+
+EVENTS_WITH_THE_SAME_DATE = [
+ {'created': '2022-01-01T00:00:00Z', 'id': '1'},
+ {'created': '2022-01-01T00:00:00Z', 'id': '2'},
+ {'created': '2022-01-01T00:00:00Z', 'id': '3'},
+ {'created': '2022-01-01T00:00:00Z', 'id': '4'},
+]
+
+EVENTS_WITH_DIFFERENT_DATE = [
+ {'created': '2022-01-01T00:00:00Z', 'id': '1'},
+ {'created': '2022-01-01T00:00:00Z', 'id': '2'},
+ {'created': '2022-01-02T00:00:00Z', 'id': '3'},
+ {'created': '2022-01-02T00:00:00Z', 'id': '4'},
+]
+
+
+@pytest.mark.parametrize('expiration_time, expected', [
+ (9999999999, 'valid_token'),
+ (0, 'new_token')])
+def test_get_token(mocker, expiration_time, expected):
+ """
+ Given:
+ - A SailPointIdentityNow client
+ - A context with a token and expiration time
+ case 1: expiration time is in the future
+ case 2: expiration time is in the past
+ When:
+ - calling get_token
+ Then:
+ - Ensure the token is returned correctly
+ case 1: the token from the context
+ case 2: a new token
+ """
+ mocker.patch.object(Client, '_http_request').return_value = {"access_token": "dummy token",
+ "expires_in": 1}
+ client = Client(base_url="https://example.com", client_id="test_id", client_secret="test_secret", verify=False, proxy=False)
+ mocker.patch('SailPointIdentityNowEventCollector.get_integration_context',
+ return_value={'token': 'valid_token', 'expires': expiration_time})
+ mocker.patch.object(Client, 'generate_token', return_value='new_token')
+ token = client.get_token()
+ assert token == expected
+
+
+def test_fetch_events__end_to_end_with_affective_dedup(mocker):
+ """
+ Given:
+ - A SailPointIdentityNow client with max of 3 events ro return per call
+ When:
+ - calling fetch_events with a max_events_per_fetch of 5
+ Then:
+ - Ensure the pagination is working correctly, and 3 sets of events are fetched to reach
+ the max_events_per_fetch or the end of the events
+ - Ensure the next_run object is returned correctly
+ - ensure the events are deduped correctly.
+ """
+ mocker.patch.object(demisto, 'debug')
+ client = mocker.patch('SailPointIdentityNowEventCollector.Client')
+ last_run = {'prev_id': '0', 'prev_date': '2022-01-01T00:00:00'}
+ max_events_per_fetch = 5
+
+ mocker.patch.object(client, 'search_events', side_effect=[[
+ {'id': str(i), 'created': f'2022-01-01T00:0{i}:00'} for i in range(1, 4)
+ ], [
+ {'id': str(i), 'created': f'2022-01-01T00:0{i}:00'} for i in range(3, 5)
+ ],
+ []])
+
+ next_run, events = fetch_events(client, max_events_per_fetch, last_run)
+
+ assert next_run == {'prev_date': '2022-01-01T00:04:00', 'last_fetched_ids': ['4']}
+ assert len(events) == 4
+
+
+def test_fetch_events__no_events(mocker):
+ """
+ Given:
+ - A SailPointIdentityNow client with max of 3 events per call
+ When:
+ - calling fetch_events with a max_events_per_fetch of 5 and no events to fetch
+ Then:
+ - Ensure the next_run object is returned correctly and we did not enter an infinite loop
+ - Ensure the debug logs are correct
+
+ """
+ mock_debug = mocker.patch.object(demisto, 'debug')
+ client = mocker.patch('SailPointIdentityNowEventCollector.Client')
+ last_run = {'prev_date': '2022-01-01T00:00:00', 'last_fetched_ids': ['0']}
+ max_events_per_fetch = 5
+
+ mocker.patch.object(client, 'search_events', return_value=[])
+ next_run, _ = fetch_events(client, max_events_per_fetch, last_run)
+
+ assert next_run == last_run
+ assert mock_debug.call_args_list[3][0][0] == 'No events fetched. Exiting the loop.'
+
+
+def test_fetch_events__all_events_are_dedup(mocker):
+ """
+ Given:
+ - A SailPointIdentityNow client with max of 3 events per call
+ When:
+ - calling fetch_events with a max_events_per_fetch of 5 and all events are duplicates
+ Then:
+ - Ensure the next_run object is returned correctly
+ - Ensure the we are not stuck in an infinite loop
+ - Ensure the debug messages are correct
+ """
+ mock_debug = mocker.patch.object(demisto, 'debug')
+ client = mocker.patch('SailPointIdentityNowEventCollector.Client')
+ last_run = {'prev_date': '2022-01-01T00:00:00', 'last_fetched_ids': [0]}
+ max_events_per_fetch = 5
+ mocker.patch('SailPointIdentityNowEventCollector.dedup_events', return_value=[])
+
+ mocker.patch.object(client, 'search_events', return_value=[
+ {'id': str(i), 'created': f'2022-01-01T00:0{i}:00'} for i in range(1, 4)
+ ])
+ next_run, _ = fetch_events(client, max_events_per_fetch, last_run)
+ assert next_run == last_run
+ assert 'Successfully fetched 3 events in this cycle.' in mock_debug.call_args_list[2][0][0]
+ assert "Done fetching. Sum of all events: 0, the next run is" in mock_debug.call_args_list[3][0][0]
+
+
+def test_add_time_and_status_to_events(mocker):
+ """
+ Given:
+ - A list of events
+ case 1: created and modified are both present and modified > created
+ case 2: created and modified are both present and modified < created
+ case 3: created is present and modified is not
+ When:
+ - calling add_time_and_status_to_events
+ Then:
+ - Ensure the _ENTRY_STATUS field is added correctly based on the created and modified fields
+ - Ensure the _time field is added correctly
+ case 1: _ENTRY_STATUS = modified, _time = modified time
+ case 2: _ENTRY_STATUS = new, _time = created time
+ case 3: _ENTRY_STATUS = new, _time = created time
+ """
+ mocker.patch.object(demisto, 'debug')
+
+ events = [
+ {'created': '2022-01-01T00:00:00', 'modified': '2022-01-01T00:01:00'},
+ {'created': '2022-01-01T00:02:00', 'modified': '2022-01-01T00:01:00'},
+ {'created': '2022-01-01T00:03:00'},
+ ]
+
+ add_time_and_status_to_events(events)
+
+ assert events[0] == {'created': '2022-01-01T00:00:00', 'modified': '2022-01-01T00:01:00',
+ '_ENTRY_STATUS': 'modified', '_time': '2022-01-01T00:01:00Z'}
+ assert events[1] == {'created': '2022-01-01T00:02:00', 'modified': '2022-01-01T00:01:00',
+ '_ENTRY_STATUS': 'new', '_time': '2022-01-01T00:02:00Z'}
+ assert events[2] == {'created': '2022-01-01T00:03:00', '_time': '2022-01-01T00:03:00Z', '_ENTRY_STATUS': 'new'}
+
+
+@pytest.mark.parametrize('prev_id, expected', [
+ ("123",
+ '{"indices": ["events"], "queryType": "SAILPOINT", "queryVersion": "5.2", "sort": ["+id"], "query": {"query": "type:* "}, "searchAfter": ["123"]}' # noqa: E501
+ ),
+ (None,
+ '{"indices": ["events"], "queryType": "SAILPOINT", "queryVersion": "5.2", "sort": ["+created"], "query": {"query": "type:* AND created: [2022-01-01T00:00:00 TO now]"}, "timeZone": "GMT"}' # noqa: E501
+ )
+])
+def test_search_events(mocker, prev_id, expected):
+ """
+ Given:
+ - A SailPointIdentityNow client
+ When:
+ - calling search_events
+ case 1: with a prev_id
+ case 2: without a prev_id
+ Then:
+ - Ensure the correct request is sent to the API
+ """
+ mocker_request = mocker.patch.object(Client, '_http_request')
+ mocker.patch.object(Client, 'get_token').return_value = {}
+ client = Client(base_url="https://example.com", client_id="test_id", client_secret="test_secret",
+ verify=False, proxy=False, token='dummy_token')
+ client.search_events(from_date='2022-01-01T00:00:00', limit=1, prev_id=prev_id)
+ assert mocker_request.call_args.kwargs["data"] == expected
+
+
+def test_get_last_fetched_ids(mocker):
+ """
+ Given:
+ - A list of events with different creation dates
+ When:
+ - calling get_last_fetched_ids
+ Then:
+ - Ensure the function returns the ids of the events that have the same creation date as the last event
+ """
+ mocker.patch.object(demisto, 'debug')
+
+ assert get_last_fetched_ids(EVENTS_WITH_DIFFERENT_DATE) == ['3', '4']
+
+
+@pytest.mark.parametrize('events, last_fetched_ids, expected, debug_msgs', [
+ (EVENTS_WITH_DIFFERENT_DATE, ['1', '2'], [{'created': '2022-01-02T00:00:00Z', 'id': '3'},
+ {'created': '2022-01-02T00:00:00Z', 'id': '4'}],
+ ["Starting deduping. Number of events before deduping: 4, last fetched ids: ['1', '2']",
+ 'Done deduping. Number of events after deduping: 2']),
+ (EVENTS_WITH_THE_SAME_DATE, ['1', '2', '3', '4'], [], []),
+ (EVENTS_WITH_THE_SAME_DATE, ['6', '5'], EVENTS_WITH_THE_SAME_DATE, [])
+])
+def test_dedup_events(mocker, events, last_fetched_ids, expected, debug_msgs):
+ """
+ Given:
+ - A list of events with duplicate and unique entries
+ - A list of last fetched ids
+ case 1 - some of the new events were fetched in the last fetch.
+ case 2 - all of the new events were fetched in the last fetch.
+ case 3 - none of the new events were fetched in the last fetch.
+ When:
+ - calling dedup_events
+ Then:
+ - Ensure the duplicate events are removed
+ - Ensure the log message contains the info of the dropped events.
+ """
+ debug_msg = mocker.patch.object(demisto, 'debug')
+ deduped_events = dedup_events(events, last_fetched_ids=last_fetched_ids)
+
+ assert deduped_events == expected
+ for i, msg in enumerate(debug_msgs):
+ assert msg in debug_msg.call_args_list[i][0][0]
diff --git a/Packs/SailPointIdentityNow/ModelingRules/SailPointIdentityNow/SailPointIdentityNow.xif b/Packs/SailPointIdentityNow/ModelingRules/SailPointIdentityNow/SailPointIdentityNow.xif
new file mode 100644
index 00000000000..2660a043a1f
--- /dev/null
+++ b/Packs/SailPointIdentityNow/ModelingRules/SailPointIdentityNow/SailPointIdentityNow.xif
@@ -0,0 +1,29 @@
+[MODEL: dataset= "sailpoint_identitynow_raw"]
+alter
+ src_name = actor -> name,
+ dst_name = `target` -> name,
+ objects = objects-> []
+| alter
+ src_email = arrayindex(regextract(src_name, "(?:\w+)\@\S+"),0),
+ dst_email = arrayindex(regextract(dst_name, "(?:\w+)\@\S+"),0),
+ objects = arraymap(objects , trim("@element", "\""))
+| alter
+ xdm.event.original_event_type = action,
+ xdm.event.id = id,
+ xdm.alert.name = name,
+ xdm.event.tags = objects,
+ xdm.event.operation_sub_type = operation,
+ xdm.source.user.ou = org,
+ xdm.source.cloud.region = pod,
+ xdm.event.outcome = if(status in ("PASSED", "SUCCESS","APPROVED","PROCESSED","UPDATED","CREATED","DELETED","ENABLED","DISABLED","FORWARDED","ESCALATED","DETECTED", "ACCESSED", "CANCELLED"), XDM_CONST.OUTCOME_SUCCESS, status in ("FAILED","BLOCKED", "REJECTED"), XDM_CONST.OUTCOME_FAILED, status in ("STARTED", "REMEDIATE"), XDM_CONST.OUTCOME_UNKNOWN,status),
+ xdm.event.is_completed = if(status in ("PASSED", "SUCCESS","APPROVED","PROCESSED","UPDATED","CREATED","DELETED","ENABLED","DISABLED","FORWARDED","ESCALATED","DETECTED", "ACCESSED", "CANCELLED", "FAILED", "BLOCKED", "REJECTED"), True, status in ("STARTED","REMEDIATE"), False),
+ xdm.event.type = type,
+ xdm.target.resource.name = if(stack = "", null, stack),
+ xdm.target.resource.type = "Stack",
+ xdm.target.ipv4 = if(incidr(ipAddress, "0.0.0.0/0"), ipAddress),
+ xdm.target.ipv6 = arrayindex(regextract(ipAddress, "(?:[a-fA-F\d]{0,4}\:){7}[\wa-fA-F]{0,4}"), 0),
+ xdm.source.user.username = if(src_email != null, arrayindex(regextract(src_email, "(\w+)\@"),0), src_name),
+ xdm.source.user.upn = src_email,
+ xdm.target.user.username = if(dst_email != null, arrayindex(regextract(dst_email, "(\w+)\@"),0), dst_name),
+ xdm.target.user.upn = dst_email,
+ xdm.event.description = attributes;
\ No newline at end of file
diff --git a/Packs/SailPointIdentityNow/ModelingRules/SailPointIdentityNow/SailPointIdentityNow.yml b/Packs/SailPointIdentityNow/ModelingRules/SailPointIdentityNow/SailPointIdentityNow.yml
new file mode 100644
index 00000000000..4fa062fcd9f
--- /dev/null
+++ b/Packs/SailPointIdentityNow/ModelingRules/SailPointIdentityNow/SailPointIdentityNow.yml
@@ -0,0 +1,6 @@
+fromversion: 8.6.0 # Will be updated with XSIAM version updates
+id: SailPoint_IdentityNow_ModelingRule
+name: SailPoint IdentityNow Modeling Rule
+rules: ''
+schema: ''
+tags: ''
\ No newline at end of file
diff --git a/Packs/SailPointIdentityNow/ModelingRules/SailPointIdentityNow/SailPointIdentityNow_schema.json b/Packs/SailPointIdentityNow/ModelingRules/SailPointIdentityNow/SailPointIdentityNow_schema.json
new file mode 100644
index 00000000000..923fc05ad46
--- /dev/null
+++ b/Packs/SailPointIdentityNow/ModelingRules/SailPointIdentityNow/SailPointIdentityNow_schema.json
@@ -0,0 +1,64 @@
+{
+ "sailpoint_identitynow_raw": {
+ "action": {
+ "type": "string",
+ "is_array": false
+ },
+ "created": {
+ "type": "datetime",
+ "is_array": false
+ },
+ "id": {
+ "type": "string",
+ "is_array": false
+ },
+ "name": {
+ "type": "string",
+ "is_array": false
+ },
+ "objects": {
+ "type": "string",
+ "is_array": false
+ },
+ "operation": {
+ "type": "string",
+ "is_array": false
+ },
+ "org": {
+ "type": "string",
+ "is_array": false
+ },
+ "pod": {
+ "type": "string",
+ "is_array": false
+ },
+ "stack": {
+ "type": "string",
+ "is_array": false
+ },
+ "ipaddress": {
+ "type": "string",
+ "is_array": false
+ },
+ "actor": {
+ "type": "string",
+ "is_array": false
+ },
+ "target": {
+ "type": "string",
+ "is_array": false
+ },
+ "status": {
+ "type": "string",
+ "is_array": false
+ },
+ "attributes": {
+ "type": "string",
+ "is_array": false
+ },
+ "type": {
+ "type": "string",
+ "is_array": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packs/SailPointIdentityNow/ReleaseNotes/1_0_8.md b/Packs/SailPointIdentityNow/ReleaseNotes/1_0_8.md
new file mode 100644
index 00000000000..cd1611c5a05
--- /dev/null
+++ b/Packs/SailPointIdentityNow/ReleaseNotes/1_0_8.md
@@ -0,0 +1,14 @@
+
+#### Modeling Rules
+
+##### New: SailPoint IdentityNow Modeling Rule
+
+<~XSIAM> (Available from Cortex XSIAM 2.2).~XSIAM>
+
+
+#### Integrations
+
+##### New: SailPoint IdentityNow Event Collector
+
+New: This is the SailPoint IdentityNow event collector integration for Cortex XSIAM.<~XSIAM> (Available from Cortex XSIAM 2.2).~XSIAM>
+
diff --git a/Packs/SailPointIdentityNow/pack_metadata.json b/Packs/SailPointIdentityNow/pack_metadata.json
index 355e6c19b3d..72ed2465e8c 100644
--- a/Packs/SailPointIdentityNow/pack_metadata.json
+++ b/Packs/SailPointIdentityNow/pack_metadata.json
@@ -2,7 +2,7 @@
"name": "SailPoint IdentityNow",
"description": "SailPoint IdentityNow content pack enables XSOAR customers to utilize the deep, enriched contextual data in the SailPoint IdentityNow platform to better drive identity-aware security practices.",
"support": "partner",
- "currentVersion": "1.0.7",
+ "currentVersion": "1.0.8",
"author": "SailPoint",
"url": "https://support.sailpoint.com/hc/en-us/requests/new",
"email": "support.idplusa@sailpoint.com",