Repository: demisto/content
Pull request #39104: Abnormal Security Bug Fixes
Author: login: sumit-badsara
Labels: name: Contribution
description: Thank you! Contributions are always welcome!
name: docs-approved
name: Partner
name: Contribution Form Filled
description: Whether contribution form filled or not.
name: Partner Support Level
description: Indicates that the contribution is for Partner supported pack
name: External PR
name: Partner-Approved
Created: 2025-03-18T18:31:48Z | Merged: 2025-04-03T11:56:50Z

## Status
- [x] Ready

## Related Issues
N/A

## Description
This pull request includes several enhancements and bug fixes for the `AbnormalSecurity` integration. The most significant changes involve adding support for pagination in threat details, improving date handling, and updating the fetch incidents logic to include a polling lag time parameter.

### Enhancements:

* Added support for pagination in `get_details_of_a_threat_request` method and updated the corresponding command to handle pagination parameters. [[1]](diffhunk://#diff-18bb674ac8b5599a6f60be40ee0fbc142e3d81ab3e220daf7a01b0b804eb8f77L100-L105) [[2]](diffhunk://#diff-18bb674ac8b5599a6f60be40ee0fbc142e3d81ab3e220daf7a01b0b804eb8f77R424-R427)
* Introduced a new function `try_str_to_datetime` to handle datetime strings with milliseconds.
* Updated `fetch_incidents` function to include `max_page_number` and `polling_lag` parameters for better control over incident fetching. [[1]](diffhunk://#diff-18bb674ac8b5599a6f60be40ee0fbc142e3d81ab3e220daf7a01b0b804eb8f77R861-R893) [[2]](diffhunk://#diff-18bb674ac8b5599a6f60be40ee0fbc142e3d81ab3e220daf7a01b0b804eb8f77R1014-R1017)

### Date Handling Improvements:

* Added `get_current_datetime` function to get the current datetime in UTC.
* Updated `generate_threat_incidents` and `generate_abuse_campaign_incidents` functions to handle datetime strings with milliseconds and ensure consistency. [[1]](diffhunk://#diff-18bb674ac8b5599a6f60be40ee0fbc142e3d81ab3e220daf7a01b0b804eb8f77L755-R808) [[2]](diffhunk://#diff-18bb674ac8b5599a6f60be40ee0fbc142e3d81ab3e220daf7a01b0b804eb8f77L774-R827)

### Configuration Updates:

* Added new parameters `polling_lag` and `max_page_number` to the integration's configuration file to allow customization of polling lag time and maximum number of pages to fetch.
This pull request includes several changes to the `AbnormalSecurity.py` file to enhance the functionality of the Abnormal Security integration. The main updates include adding pagination support, improving datetime handling, and introducing new parameters for fetching incidents.

### Enhancements to Pagination and Fetching:

* Added methods for paginated fetching of cases, threats, and abuse campaigns to the `Client` class, allowing for more efficient data retrieval.
* Introduced new parameters `page_size` and `page_number` in the `get_details_of_a_threat_request` method to support pagination.
* Updated `fetch_incidents` function to include `max_page_number` and `polling_lag` parameters for better control over data fetching and consistency. [[1]](diffhunk://#diff-18bb674ac8b5599a6f60be40ee0fbc142e3d81ab3e220daf7a01b0b804eb8f77L799-R919) [[2]](diffhunk://#diff-18bb674ac8b5599a6f60be40ee0fbc142e3d81ab3e220daf7a01b0b804eb8f77R928-R980)

### Improvements to DateTime Handling:

* Added `try_str_to_datetime` and `get_current_datetime` helper functions to standardize datetime conversions and ensure consistent timezone handling.
* Modified the `fetch_incidents` function to use the new datetime helper functions for accurate timestamp management.

### Command and Incident Generation Updates:

* Enhanced `generate_threat_incidents`, `generate_abuse_campaign_incidents`, and `generate_account_takeover_cases_incidents` functions to handle paginated responses and filter messages based on remediation timestamps. [[1]](diffhunk://#diff-18bb674ac8b5599a6f60be40ee0fbc142e3d81ab3e220daf7a01b0b804eb8f77L755-R870) [[2]](diffhunk://#diff-18bb674ac8b5599a6f60be40ee0fbc142e3d81ab3e220daf7a01b0b804eb8f77L774-R889) [[3]](diffhunk://#diff-18bb674ac8b5599a6f60be40ee0fbc142e3d81ab3e220daf7a01b0b804eb8f77L785-R905)
* Updated command functions to pass pagination parameters and handle responses accordingly.

### Configuration File Changes:

* Added new configuration parameters `polling_lag` and `max_page_number` to the `AbnormalSecurity.yml` file for better control over incident fetching settings.

These changes collectively improve the efficiency and accuracy of data fetching and processing in the Abnormal Security integration.
### Testing Enhancements:

* Updated tests to include the new pagination and datetime handling logic, and added fixtures for mock responses. [[1]](diffhunk://#diff-55d6d22c39b2bf54207bb169708115e9e28d9ac6302c8d8cf642ea971ba84298R4) [[2]](diffhunk://#diff-55d6d22c39b2bf54207bb169708115e9e28d9ac6302c8d8cf642ea971ba84298R27-L28) [[3]](diffhunk://#diff-55d6d22c39b2bf54207bb169708115e9e28d9ac6302c8d8cf642ea971ba84298L265-R272) [[4]](diffhunk://#diff-55d6d22c39b2bf54207bb169708115e9e28d9ac6302c8d8cf642ea971ba84298L291-R298) [[5]](diffhunk://#diff-55d6d22c39b2bf54207bb169708115e9e28d9ac6302c8d8cf642ea971ba84298L476-R485)

## Must have
- [x] Tests
- [x] Documentation 



Diff:
diff --git a/Packs/AbnormalSecurity/.pack-ignore b/Packs/AbnormalSecurity/.pack-ignore
index 413c7cdfb2ba..4ed08a744b1b 100644
--- a/Packs/AbnormalSecurity/.pack-ignore
+++ b/Packs/AbnormalSecurity/.pack-ignore
@@ -5,5 +5,21 @@ ignore=IM111
 ignore=MR108
 
 [file:AbnormalSecurityEventCollector.yml]
-ignore=MR108
+ignore=MR108,ST111
 
+[known_words]
+gte
+lte
+MM
+YYYY
+datetime
+Covid
+UUID
+RFC
+URLs
+csv
+abusecampaigns
+UTC
+occurences
+unanalyzed
+graymail
\ No newline at end of file
diff --git a/Packs/AbnormalSecurity/CONTRIBUTORS.json b/Packs/AbnormalSecurity/CONTRIBUTORS.json
new file mode 100644
index 000000000000..74085d662187
--- /dev/null
+++ b/Packs/AbnormalSecurity/CONTRIBUTORS.json
@@ -0,0 +1,3 @@
+[
+    "Sumit Badsara"
+]
\ No newline at end of file
diff --git a/Packs/AbnormalSecurity/Classifiers/classifier-Abnormal_Security_Mapper.json b/Packs/AbnormalSecurity/Classifiers/classifier-Abnormal_Security_Mapper.json
index f543b4a34b90..aa45d2231184 100644
--- a/Packs/AbnormalSecurity/Classifiers/classifier-Abnormal_Security_Mapper.json
+++ b/Packs/AbnormalSecurity/Classifiers/classifier-Abnormal_Security_Mapper.json
@@ -78,6 +78,9 @@
                 "Abnormal Security Case Status": {
                     "simple": "case_status"
                 },
+                "Abnormal Security Case GenAI Summary": {
+                    "simple": "genai_summary"
+                },
                 "Abnormal Security Customer Visible Time": {
                     "simple": "customerVisibleTime"
                 },
diff --git a/Packs/AbnormalSecurity/IncidentFields/incidentfields_Abnormal_Security_Case_GenAI_Summary.json b/Packs/AbnormalSecurity/IncidentFields/incidentfields_Abnormal_Security_Case_GenAI_Summary.json
new file mode 100644
index 000000000000..43555f44a8ec
--- /dev/null
+++ b/Packs/AbnormalSecurity/IncidentFields/incidentfields_Abnormal_Security_Case_GenAI_Summary.json
@@ -0,0 +1,31 @@
+{
+    "id": "incident_abnormalsecuritycasegenaisummary",
+    "version": -1,
+    "modified": "2025-03-20T09:53:29.981208869Z",
+    "name": "Abnormal Security Case GenAI Summary",
+    "ownerOnly": false,
+    "cliName": "abnormalsecuritycasegenaisummary",
+    "type": "longText",
+    "closeForm": false,
+    "editForm": true,
+    "required": false,
+    "neverSetAsRequired": false,
+    "isReadOnly": false,
+    "useAsKpi": false,
+    "locked": false,
+    "system": false,
+    "content": true,
+    "group": 0,
+    "hidden": false,
+    "openEnded": false,
+    "associatedTypes": [
+        "AbnormalSecurity"
+    ],
+    "associatedToAll": false,
+    "unmapped": false,
+    "unsearchable": true,
+    "caseInsensitive": true,
+    "sla": 0,
+    "threshold": 72,
+    "fromVersion": "6.0.0"
+}
\ No newline at end of file
diff --git a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
index 561746f4ce8e..6739f368fcff 100644
--- a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
+++ b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
@@ -1,5 +1,5 @@
 import logging
-from datetime import datetime
+from datetime import datetime, timedelta
 from typing import Any
 
 import demistomock as demisto  # noqa: F401
@@ -23,6 +23,22 @@
 }
 
 ISO_8601_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
+TIME_FORMAT_WITHMS = "%Y-%m-%dT%H:%M:%S.%fZ"
+
+
+def try_str_to_datetime(time: str) -> datetime:
+    """
+    Try to convert a string to a datetime object.
+    """
+    try:
+        return datetime.strptime(time, ISO_8601_FORMAT).astimezone(timezone.utc)
+    except Exception as _:
+        pass
+    return datetime.strptime((time[:26] + 'Z') if len(time) > 26 else time, TIME_FORMAT_WITHMS).astimezone(timezone.utc)
+
+
+def get_current_datetime() -> datetime:
+    return datetime.utcnow().astimezone(timezone.utc)
 
 
 class FetchIncidentsError(Exception):
@@ -30,6 +46,10 @@ class FetchIncidentsError(Exception):
 
 
 class Client(BaseClient):
+    CASES = "cases"
+    ABUSE_CAMPAIGNS = "abusecampaigns"
+    THREATS = "threats"
+
     def __init__(self, server_url, verify, proxy, headers, auth):
         super().__init__(base_url=server_url, verify=verify, proxy=proxy, headers=headers, auth=auth, timeout=2400)
 
@@ -132,15 +152,87 @@ def get_a_list_of_threats_request(
 
         response = self._http_request("get", "threats", params=params, headers=headers)
 
-        response = self._remove_keys_from_response(response, ["pageNumber", "nextPageNumber"])
-
         return response
 
-    def get_details_of_a_threat_request(self, threat_id, subtenant=None):
+    def get_page_number_and_max_iterations(self, max_incidents_to_fetch):
+        page_size = min(max_incidents_to_fetch, 100)
+        max_iterations = (max_incidents_to_fetch // page_size) + 1
+        return page_size, max_iterations
+
+    def get_paginated_cases_list(self, filter_='', max_incidents_to_fetch=FETCH_LIMIT):
+        cases_response: dict[str, list[dict]] = {"cases": []}
+        if max_incidents_to_fetch < 1:
+            return cases_response
+
+        page_number, current_iteration = 1, 1
+        page_size, max_iterations = self.get_page_number_and_max_iterations(max_incidents_to_fetch)
+
+        while page_number is not None:
+            response = self.get_a_list_of_abnormal_cases_identified_by_abnormal_security_request(
+                filter_=filter_, page_size=page_size, page_number=page_number
+            )
+            cases_response["cases"].extend(response.get('cases', []))
+            page_number = response.get('nextPageNumber', None)
+            current_iteration += 1
+            if current_iteration > max_iterations:
+                break
+        return cases_response
+
+    def get_paginated_threats_list(self, filter_='', max_incidents_to_fetch=FETCH_LIMIT):
+        threats_response: dict[str, list[dict]] = {"threats": []}
+        if max_incidents_to_fetch < 1:
+            return threats_response
+
+        page_number, current_iteration = 1, 1
+        page_size, max_iterations = self.get_page_number_and_max_iterations(max_incidents_to_fetch)
+
+        while page_number is not None:
+            response = self.get_a_list_of_threats_request(
+                filter_=filter_, page_size=page_size, page_number=page_number
+            )
+            threats_response["threats"].extend(response.get('threats', []))
+            page_number = response.get('nextPageNumber', None)
+            current_iteration += 1
+            if current_iteration > max_iterations:
+                break
+        return threats_response
+
+    def get_paginated_abusecampaigns_list(self, filter_='', max_incidents_to_fetch=FETCH_LIMIT):
+        campaigns_response: dict[str, list[dict]] = {"campaigns": []}
+        if max_incidents_to_fetch < 1:
+            return campaigns_response
+
+        page_number, current_iteration = 1, 1
+        page_size, max_iterations = self.get_page_number_and_max_iterations(max_incidents_to_fetch)
+
+        while page_number is not None:
+            response = self.get_a_list_of_campaigns_submitted_to_abuse_mailbox_request(
+                filter_=filter_, page_size=page_size, page_number=page_number
+            )
+            campaigns_response["campaigns"].extend(response.get('campaigns', []))
+            page_number = response.get('nextPageNumber', None)
+            current_iteration += 1
+            if current_iteration > max_iterations:
+                break
+        return campaigns_response
+
+    def get_details_of_a_threat_request(self, threat_id, subtenant=None, page_size=None, page_number=None):
+        """
+        Get details of a specific threat with pagination support.
+
+        Args:
+            threat_id (str): The ID of the threat to get details for
+            subtenant (str, optional): The subtenant ID
+            page_size (int, optional): The number of items per page
+            page_number (int, optional): The page number (zero-based)
+
+        Returns:
+            dict: The threat details with pagination
+        """
         headers = self._headers
-        params = assign_params(subtenant=subtenant)
+        params = assign_params(subtenant=subtenant, pageSize=page_size, pageNumber=page_number)
 
-        response = self._http_request("get", f"threats/{threat_id}", params=params, headers=headers)
+        response = self._http_request('get', f'threats/{threat_id}', params=params, headers=headers)
 
         return response
 
@@ -419,10 +511,12 @@ def get_a_list_of_threats_command(client, args):
 
 
 def get_details_of_a_threat_command(client, args):
-    threat_id = str(args.get("threat_id", ""))
-    subtenant = args.get("subtenant", None)
+    threat_id = str(args.get('threat_id', ''))
+    subtenant = args.get('subtenant', None)
+    page_size = args.get('page_size', None)
+    page_number = args.get('page_number', None)
 
-    response = client.get_details_of_a_threat_request(threat_id, subtenant)
+    response = client.get_details_of_a_threat_request(threat_id, subtenant, page_size, page_number)
     headers = [
         "subject",
         "fromAddress",
@@ -474,8 +568,16 @@ def get_details_of_an_abnormal_case_command(client, args):
     case_id = str(args.get("case_id", ""))
     subtenant = args.get("subtenant", None)
     response = client.get_details_of_an_abnormal_case_request(case_id, subtenant)
-    headers = ["caseId", "severity", "affectedEmployee", "firstObserved", "threatIds"]
-    markdown = tableToMarkdown(f"Details of Case {response.get('caseId', '')}", response, headers=headers, removeNull=True)
+    headers = [
+        'caseId',
+        'severity',
+        'affectedEmployee',
+        'firstObserved',
+        'threatIds',
+        'genai_summary'
+    ]
+    markdown = tableToMarkdown(
+        f"Details of Case {response.get('caseId', '')}", response, headers=headers, removeNull=True)
     command_results = CommandResults(
         readable_output=markdown,
         outputs_prefix="AbnormalSecurity.AbnormalCaseDetails",
@@ -741,14 +843,33 @@ def get_a_list_of_unanalyzed_abuse_mailbox_campaigns_command(client, args):
     return command_results
 
 
-def generate_threat_incidents(client, threats):
+def generate_threat_incidents(client, threats, max_page_number, start_datetime, end_datetime):
     incidents = []
     for threat in threats:
-        threat_details = client.get_details_of_a_threat_request(threat["threatId"])
+        page_number = 1
+        all_messages, all_filtered_messages = [], []
+        while page_number is not None:
+            threat_details = client.get_details_of_a_threat_request(threat["threatId"], page_number=page_number)
+            for message in threat_details["messages"]:
+                all_messages.append(message)
+                remediation_datetime = try_str_to_datetime(message.get("remediationTimestamp"))
+                if remediation_datetime and start_datetime <= remediation_datetime <= end_datetime:
+                    all_filtered_messages.append(message)
+                if remediation_datetime and remediation_datetime < start_datetime:
+                    break
+            page_number = threat_details.get('nextPageNumber', None)
+            if page_number is not None and page_number > max_page_number:
+                break
+
+        received_time = ""
+        threat_details["messages"] = all_filtered_messages or all_messages
+        if threat_details.get("messages", []):
+            received_time = threat_details["messages"][0].get("receivedTime")
+
         incident = {
             "dbotMirrorId": str(threat["threatId"]),
             "name": "Threat",
-            "occurred": threat_details["messages"][0].get("receivedTime"),
+            "occurred": received_time[:26] if len(received_time) > 26 else received_time,
             "details": "Threat",
             "rawJSON": json.dumps(threat_details) if threat_details else {},
         }
@@ -760,12 +881,13 @@ def generate_abuse_campaign_incidents(client, campaigns):
     incidents = []
     for campaign in campaigns:
         campaign_details = client.get_details_of_an_abuse_mailbox_campaign_request(campaign["campaignId"])
+        first_reported = campaign_details.get("firstReported", "")
         incident = {
-            "dbotMirrorId": str(campaign["campaignId"]),
+            "dbotMirrorId": str(campaign.get("campaignId", "")),
             "name": "Abuse Campaign",
-            "occurred": campaign_details["firstReported"],
-            "details": "Abuse Campaign",
-            "rawJSON": json.dumps(campaign_details) if campaign_details else {},
+            "occurred": first_reported[:26] if len(first_reported) > 26 else first_reported,
+            'details': "Abuse Campaign",
+            "rawJSON": json.dumps(campaign_details) if campaign_details else {}
         }
         incidents.append(incident)
     return incidents
@@ -780,20 +902,23 @@ def generate_account_takeover_cases_incidents(client, cases):
             "name": "Account Takeover Case",
             "occurred": case_details["firstObserved"],
             "details": case["description"],
-            "rawJSON": json.dumps(case_details) if case_details else {},
+            "genaiSummary": case_details["genai_summary"],
+            "rawJSON": json.dumps(case_details) if case_details else {}
         }
         incidents.append(incident)
     return incidents
 
 
 def fetch_incidents(
-    client: Client,
-    last_run: dict[str, Any],
-    first_fetch_time: str,
-    fetch_threats: bool,
-    fetch_abuse_campaigns: bool,
-    fetch_account_takeover_cases: bool,
-    max_incidents_to_fetch: Optional[int] = FETCH_LIMIT,
+        client: Client,
+        last_run: dict[str, Any],
+        first_fetch_time: str,
+        fetch_threats: bool,
+        fetch_abuse_campaigns: bool,
+        fetch_account_takeover_cases: bool,
+        max_page_number: int = 8,
+        max_incidents_to_fetch: int = FETCH_LIMIT,
+        polling_lag: timedelta = timedelta(minutes=0),
 ):
     """
     Fetch incidents from various sources (threats, abuse campaigns, and account takeovers).
@@ -802,45 +927,63 @@ def fetch_incidents(
     - client (Client): Client object to interact with the API.
     - last_run (Dict[str, Any]): Dictionary containing details about the last time incidents were fetched.
     - first_fetch_time (str): ISO formatted string indicating the first time from which to start fetching incidents.
+    - max_page_number (int): Maximum number of pages to fetch for incidents.
     - max_incidents_to_fetch (int, optional): Maximum number of incidents to fetch. Defaults to FETCH_LIMIT.
+    - polling_lag (int, optional): Time in minutes to subtract from polling time window for data consistency. Defaults to 0.
 
     Returns:
     - Tuple[Dict[str, str], List[Dict]]: Tuple containing a dictionary with the `last_fetch` time and a list of fetched incidents.
     """
-
     try:
         last_fetch = last_run.get("last_fetch", first_fetch_time)
-        last_fetch_datetime = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)
-        last_fetch = last_fetch_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
+        last_fetch = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)
 
-        current_datetime = datetime.utcnow().astimezone(timezone.utc)
-        current_iso_format_time = current_datetime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
-        all_incidents = []
+        current_datetime = get_current_datetime()
+        start_time = last_fetch
+        end_time = get_current_datetime()
 
-        if fetch_threats:
-            threats_filter = f"receivedTime gte {last_fetch}"
-            threats_response = client.get_a_list_of_threats_request(filter_=threats_filter, page_size=100)
-            all_incidents += generate_threat_incidents(client, threats_response.get("threats", []))
+        if polling_lag is not None:
+            start_time = start_time - polling_lag
+            end_time = end_time - polling_lag
 
-        if fetch_abuse_campaigns:
-            abuse_campaigns_filter = f"lastReportedTime gte {last_fetch}"
-            abuse_campaigns_response = client.get_a_list_of_campaigns_submitted_to_abuse_mailbox_request(
-                filter_=abuse_campaigns_filter, page_size=100
-            )
-            all_incidents += generate_abuse_campaign_incidents(client, abuse_campaigns_response.get("campaigns", []))
+        start_timestamp = start_time.strftime(ISO_8601_FORMAT)
+        end_timestamp = end_time.strftime(ISO_8601_FORMAT)
 
-        if fetch_account_takeover_cases:
-            account_takeover_cases_filter = f"lastModifiedTime gte {last_fetch}"
-            account_takeover_cases_response = client.get_a_list_of_abnormal_cases_identified_by_abnormal_security_request(
-                filter_=account_takeover_cases_filter, page_size=100
+        all_incidents = []
+        current_pending_incidents_to_fetch = max_incidents_to_fetch
+        threat_incidents, abuse_campaign_incidents, account_takeover_cases_incidents = [], [], []
+
+        if fetch_threats and current_pending_incidents_to_fetch > 0:
+            threats_filter = f"latestTimeRemediated gte {start_timestamp} and latestTimeRemediated lte {end_timestamp}"
+            threats_response = client.get_paginated_threats_list(
+                filter_=threats_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            threat_incidents = generate_threat_incidents(
+                client, threats_response.get('threats', []), max_page_number, start_time, end_time
             )
-            all_incidents += generate_account_takeover_cases_incidents(client, account_takeover_cases_response.get("cases", []))
-
+        current_pending_incidents_to_fetch -= len(threat_incidents)
+
+        if fetch_abuse_campaigns and current_pending_incidents_to_fetch > 0:
+            abuse_campaigns_filter = f"lastReportedTime gte {start_timestamp}"
+            abuse_campaigns_response = client.get_paginated_abusecampaigns_list(
+                filter_=abuse_campaigns_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            abuse_campaign_incidents = generate_abuse_campaign_incidents(client, abuse_campaigns_response.get('campaigns', []))
+        current_pending_incidents_to_fetch -= len(abuse_campaign_incidents)
+
+        if fetch_account_takeover_cases and current_pending_incidents_to_fetch > 0:
+            account_takeover_cases_filter = f"lastModifiedTime gte {start_timestamp}"
+            account_takeover_cases_response = client.get_paginated_cases_list(
+                filter_=account_takeover_cases_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            account_takeover_cases_incidents = generate_account_takeover_cases_incidents(
+                client, account_takeover_cases_response.get('cases', []))
+
+        all_incidents = threat_incidents + abuse_campaign_incidents + account_takeover_cases_incidents
     except Exception as e:
         logging.error(f"Failed fetching incidents: {e}")
         raise FetchIncidentsError(f"Error while fetching incidents: {e}")
 
-    next_run = {"last_fetch": current_iso_format_time}
+    next_run = {
+        "last_fetch": current_datetime.strftime(ISO_8601_FORMAT)
+    }
 
     return next_run, all_incidents[:max_incidents_to_fetch]
 
@@ -915,6 +1058,10 @@ def main():  # pragma: nocover
         elif command == "fetch-incidents" and is_fetch:
             max_incidents_to_fetch = arg_to_number(params.get("max_fetch", FETCH_LIMIT))
             fetch_threats = params.get("fetch_threats", False)
+            # Get the polling lag time parameter
+            polling_lag_minutes = int(params.get('polling_lag', 5))
+            max_page_number = int(params.get('max_page_number', 8))
+            polling_lag_delta = timedelta(minutes=polling_lag_minutes)
             fetch_abuse_campaigns = params.get("fetch_abuse_campaigns", False)
             fetch_account_takeover_cases = params.get("fetch_account_takeover_cases", False)
             first_fetch_datetime = arg_to_datetime(arg=params.get("first_fetch"), arg_name="First fetch time", required=True)
@@ -926,10 +1073,12 @@ def main():  # pragma: nocover
                 client=client,
                 last_run=demisto.getLastRun(),
                 first_fetch_time=first_fetch_time,
-                max_incidents_to_fetch=max_incidents_to_fetch,
+                max_incidents_to_fetch=max_incidents_to_fetch or FETCH_LIMIT,
                 fetch_threats=fetch_threats,
                 fetch_abuse_campaigns=fetch_abuse_campaigns,
                 fetch_account_takeover_cases=fetch_account_takeover_cases,
+                max_page_number=max_page_number,
+                polling_lag=polling_lag_delta,
             )
             demisto.setLastRun(next_run)
             demisto.incidents(incidents)
diff --git a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.yml b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.yml
index 36bc0d26da25..4852773a4041 100644
--- a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.yml
+++ b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.yml
@@ -1,4 +1,7 @@
 category: Data Enrichment & Threat Intelligence
+sectionOrder:
+- Connect
+- Collect
 commonfields:
   id: Abnormal Security
   version: -1
@@ -7,44 +10,54 @@ configuration:
   display: Server URL (e.g. https://api.abnormalplatform.com/v1)
   name: url
   required: true
+  section: Connect
   type: 0
 - display: API Key
   name: api_key
   required: true
+  section: Connect
   type: 4
 - display: Trust any certificate (not secure)
   name: insecure
+  section: Connect
   type: 8
   required: false
 - display: Use system proxy settings
   name: proxy
+  section: Connect
   type: 8
   required: false
 - display: Fetch incidents
   name: isFetch
   required: false
+  section: Collect
   type: 8
 - additionalinfo: Maximum number of incidents per fetch. The default value is 200.
   defaultvalue: '200'
   display: Maximum incidents to fetch.
   name: max_fetch
+  section: Collect
   type: 0
   required: false
 - display: Fetch Threats
   name: fetch_threats
   type: 8
+  section: Collect
   required: false
 - display: Fetch Abuse Campaigns
   name: fetch_abuse_campaigns
   type: 8
+  section: Collect
   required: false
 - display: Fetch Account Takeover Cases
   name: fetch_account_takeover_cases
   type: 8
   required: false
+  section: Collect
 - defaultvalue: 3 days
   display: First fetch time
   additionalinfo: First alert created date to fetch. e.g., "1 min ago","2 weeks ago","3 months ago"
+  section: Collect
   name: first_fetch
   type: 0
   required: false
@@ -52,11 +65,27 @@ configuration:
   name: incidentType
   type: 13
   required: false
+  section: Collect
 - defaultvalue: '1'
   display: Incidents Fetch Interval
   name: incidentFetchInterval
   required: false
   type: 19
+  section: Collect
+- display: Polling Lag Time (in minutes)
+  name: polling_lag
+  defaultvalue: "0"
+  type: 0
+  required: false
+  additionalinfo: Time in minutes to subtract from polling time window for data consistency
+  section: Collect
+- display: Maximum incidents pages to fetch
+  name: max_page_number
+  defaultvalue: "8"
+  type: 0
+  required: false
+  additionalinfo: Maximum number of pages to fetch for incidents
+  section: Collect
 description: Abnormal Security detects the whole spectrum of email attacks, from vendor email compromise and spear-phishing to unwanted email spam and graymail. To stop these advanced attacks, Abnormal leverages the industry’s most advanced behavioral data science to baseline known good behavior and detects anomalies.
 display: Abnormal Security
 name: Abnormal Security
@@ -90,7 +119,7 @@ script:
       - Text
       - Others
       - Attachment with Zipped File
-    - description: Number of threats that on in each page. Each page of data will have at most page_size threats. Has no effect if filter is not specified.
+    - description: Number of threats per page. Each page will contain up to page_size threats. This has no effect if no filter is specified.
       name: page_size
     - description: 1-indexed page number to get a particular page of threats. Has no effect if filter is not specified.
       name: page_number
@@ -121,6 +150,10 @@ script:
       name: mock-data
     - description: Subtenant of the user (if applicable).
       name: subtenant
+    - description: Number of threats per page. Each page will contain up to page_size threats. This has no effect if no filter is specified.
+      name: page_size
+    - description: 1-indexed page number to get a particular page of threats. Has no effect if filter is not specified.
+      name: page_number
     description: Get details of a threat.
     name: abnormal-security-get-threat
     outputs:
@@ -322,6 +355,9 @@ script:
     - contextPath: AbnormalSecurity.AbnormalCaseDetails.firstObserved
       description: First time suspicious behavior was observed.
       type: String
+    - contextPath: AbnormalSecurity.AbnormalCaseDetails.genai_summary
+      description: Gen AI summary of the case.
+      type: String
   - arguments:
     - description: A string representing the email case. Can be retrieved by first running command to list cases.
       name: case_id
@@ -759,7 +795,7 @@ script:
     description: Get the latest threat intel feed.
     name: abnormal-security-get-latest-threat-intel-feed
     deprecated: true
-  dockerimage: demisto/python3:3.11.10.115186
+  dockerimage: demisto/python3:3.12.8.1983910
   script: ""
   subtype: python3
   type: python
diff --git a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity_test.py b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity_test.py
index 608d8a8ea435..5e60a7509e0e 100644
--- a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity_test.py
+++ b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity_test.py
@@ -1,45 +1,45 @@
 import json
-from datetime import datetime
-
+from datetime import datetime, timedelta, UTC
 import demistomock as demisto
 import pytest
+
 from AbnormalSecurity import (
-    ISO_8601_FORMAT,
     Client,
     check_the_status_of_an_action_requested_on_a_case_command,
     check_the_status_of_an_action_requested_on_a_threat_command,
-    download_data_from_threat_log_in_csv_format_command,
-    fetch_incidents,
     get_a_list_of_abnormal_cases_identified_by_abnormal_security_command,
-    get_a_list_of_campaigns_submitted_to_abuse_mailbox_command,
     get_a_list_of_threats_command,
-    get_a_list_of_unanalyzed_abuse_mailbox_campaigns_command,
-    get_a_list_of_vendor_cases_command,
     get_a_list_of_vendors_command,
-    get_details_of_an_abnormal_case_command,
-    get_details_of_an_abuse_mailbox_campaign_command,
-    get_employee_identity_analysis_genome_data_command,
-    get_employee_information_command,
-    get_employee_login_information_for_last_30_days_in_csv_format_command,
-    get_the_activity_of_a_specific_vendor_command,
     get_the_details_of_a_specific_vendor_command,
+    get_the_activity_of_a_specific_vendor_command,
+    get_a_list_of_vendor_cases_command,
     get_the_details_of_a_vendor_case_command,
-    get_the_latest_threat_intel_feed_command,
     manage_a_threat_identified_by_abnormal_security_command,
     manage_an_abnormal_case_command,
+    get_details_of_an_abnormal_case_command,
+    get_details_of_an_abuse_mailbox_campaign_command,
     provides_the_analysis_and_timeline_details_of_a_case_command,
     submit_an_inquiry_to_request_a_report_on_misjudgement_by_abnormal_security_command,
     submit_false_negative_report_command,
     submit_false_positive_report_command,
+    get_a_list_of_campaigns_submitted_to_abuse_mailbox_command,
+    get_the_latest_threat_intel_feed_command,
+    get_employee_identity_analysis_genome_data_command,
+    get_employee_information_command,
+    get_employee_login_information_for_last_30_days_in_csv_format_command,
+    download_data_from_threat_log_in_csv_format_command,
+    generate_threat_incidents,
+    get_a_list_of_unanalyzed_abuse_mailbox_campaigns_command, fetch_incidents, ISO_8601_FORMAT
 )
 from CommonServerPython import DemistoException
 from test_data.fixtures import BASE_URL, apikey
+from test_data.mock_paginated_response import create_mock_paginator_side_effect, create_mock_detail_side_effect
+
 
 headers = {
     "Authorization": f"Bearer {apikey}",
 }
 
-
 class MockResponse:
     def __init__(self, data, status_code):
         self.data = data
@@ -57,11 +57,17 @@ def util_load_response(path):
         return MockResponse(f.read(), 200)
 
 
-def mock_client(mocker, http_request_result=None, throw_error=False):
-    mocker.patch.object(demisto, "getIntegrationContext", return_value={"current_refresh_token": "refresh_token"})
-    client = Client(server_url=BASE_URL, verify=False, proxy=False, auth=None, headers=headers)
-    if http_request_result:
-        mocker.patch.object(client, "_http_request", return_value=http_request_result)
+def mock_client(mocker, response=None, side_effect=None, throw_error=False):
+
+    mocker.patch.object(demisto, 'getIntegrationContext', return_value={'current_refresh_token': 'refresh_token'})
+    client = Client(
+        server_url=BASE_URL,
+        verify=False,
+        proxy=False,
+        auth=None,
+        headers=headers
+    )
+    mocker.patch.object(client, '_http_request', return_value=response, side_effect=side_effect)
 
     if throw_error:
         err_msg = "Error in API call [400] - BAD REQUEST}"
@@ -82,6 +88,13 @@ def mock_get_a_list_of_threats_request(mocker):
     )
 
 
+@pytest.fixture
+def mock_get_details_of_a_threat_request(mocker):
+    threat_details = util_load_json('test_data/test_get_details_of_a_threat_page2.json')
+    threat_details['messages'][0]['remediationTimestamp'] = '2023-09-17T15:43:09Z'
+    mocker.patch("AbnormalSecurity.Client.get_details_of_a_threat_request").return_value = threat_details
+
+
 @pytest.fixture
 def mock_get_a_list_of_campaigns_submitted_to_abuse_mailbox_request(mocker):
     mocker.patch(
@@ -132,7 +145,11 @@ def test_get_a_list_of_abnormal_cases_identified_by_abnormal_security_command(mo
         - Assert the context data is as expected.
         - Assert output prefix data is as expected
     """
-    client = mock_client(mocker, util_load_json("test_data/test_get_list_of_abnormal_cases.json"))
+    # Modify the mock response to have a nextPageNumber
+    abnormal_cases_list = util_load_json('test_data/test_get_list_of_abnormal_cases.json')
+    abnormal_cases_list["nextPageNumber"] = 2
+
+    client = mock_client(mocker, abnormal_cases_list)
     results = get_a_list_of_abnormal_cases_identified_by_abnormal_security_command(client, {})
     assert results.outputs.get("cases")[0].get("caseId") == "1234"
     assert results.outputs.get("pageNumber", 0) > 0
@@ -263,7 +280,7 @@ def test_manage_a_threat_identified_by_abnormal_security_command_failure(mocker)
     Then
         - Assert error is thrown as expected
     """
-    client = mock_client(mocker, None, True)
+    client = mock_client(mocker, None, False, True)
     with pytest.raises(DemistoException):
         manage_a_threat_identified_by_abnormal_security_command(client, {})
 
@@ -289,7 +306,7 @@ def test_manage_an_abnormal_case_command_failure(mocker):
     Then
         - Assert error is thrown as expected
     """
-    client = mock_client(mocker, None, True)
+    client = mock_client(mocker, None, False, True)
     with pytest.raises(DemistoException):
         manage_an_abnormal_case_command(client, {})
 
@@ -385,7 +402,11 @@ def test_get_a_list_of_campaigns_submitted_to_abuse_mailbox_command(mocker):
         - Assert the context data is as expected.
         - Assert output prefix data is as expected
     """
-    client = mock_client(mocker, util_load_json("test_data/test_get_list_of_abuse_campaigns.json"))
+    # Modify the mock response to have a nextPageNumber
+    abnormal_campaigns_list = util_load_json('test_data/test_get_list_of_abuse_campaigns.json')
+    abnormal_campaigns_list["nextPageNumber"] = 2
+
+    client = mock_client(mocker, abnormal_campaigns_list)
     results = get_a_list_of_campaigns_submitted_to_abuse_mailbox_command(client, {})
     assert results.outputs.get("campaigns")[0].get("campaignId") == "fff51768-c446-34e1-97a8-9802c29c3ebd"
     assert results.outputs.get("pageNumber", 0) > 0
@@ -467,9 +488,9 @@ def test_provides_the_analysis_and_timeline_details_of_a_case_command(mocker):
 
 
 def test_fetch_threat_incidents(mocker, mock_get_a_list_of_threats_request):
-    client = mock_client(mocker, util_load_json("test_data/test_get_details_of_a_threat.json"))
+    client = mock_client(mocker, util_load_json('test_data/test_get_details_of_a_threat_page2.json'))
     first_fetch_time = datetime.now().strftime(ISO_8601_FORMAT)
-    next_run, incidents = fetch_incidents(
+    _, incidents = fetch_incidents(
         client=client,
         last_run={"last_fetch": "2023-09-17T14:43:09Z"},
         first_fetch_time=first_fetch_time,
@@ -484,7 +505,7 @@ def test_fetch_threat_incidents(mocker, mock_get_a_list_of_threats_request):
 def test_fetch_cases_incidents(mocker, mock_get_a_list_of_abnormal_cases_identified_by_abnormal_security_request):
     client = mock_client(mocker, util_load_json("test_data/test_get_details_of_an_abnormal_case.json"))
     first_fetch_time = datetime.now().strftime(ISO_8601_FORMAT)
-    next_run, incidents = fetch_incidents(
+    _, incidents = fetch_incidents(
         client=client,
         last_run={"last_fetch": "2023-09-17T14:43:09Z"},
         first_fetch_time=first_fetch_time,
@@ -494,12 +515,13 @@ def test_fetch_cases_incidents(mocker, mock_get_a_list_of_abnormal_cases_identif
         fetch_threats=False,
     )
     assert len(incidents) == 1
+    assert incidents[0].get('genaiSummary') == 'genai_summary'
 
 
 def test_fetch_abuse_campaign_incidents(mocker, mock_get_a_list_of_campaigns_submitted_to_abuse_mailbox_request):
     client = mock_client(mocker, util_load_json("test_data/test_get_details_of_abuse_campaign.json"))
     first_fetch_time = datetime.now().strftime(ISO_8601_FORMAT)
-    next_run, incidents = fetch_incidents(
+    _, incidents = fetch_incidents(
         client=client,
         last_run={"last_fetch": "2023-09-17T14:43:09Z"},
         first_fetch_time=first_fetch_time,
@@ -509,3 +531,666 @@ def test_fetch_abuse_campaign_incidents(mocker, mock_get_a_list_of_campaigns_sub
         fetch_threats=False,
     )
     assert len(incidents) == 1
+
+
+def test_get_details_of_a_threat_request_two_pages(mocker):
+    return_val = util_load_json('test_data/test_get_details_of_a_threat.json')
+    return_val['messages'][0]['remediationTimestamp'] = '2023-09-17T15:43:09Z'
+    page_2 = util_load_json('test_data/test_get_details_of_a_threat_page2.json')
+    page_2['messages'][0]['remediationTimestamp'] = '2023-09-17T16:43:09Z'
+
+    client = mock_client(mocker, side_effect=[return_val, page_2])
+    # Create datetime objects instead of using strings
+    start_datetime = datetime(2023, 9, 17, 14, 43, 9, tzinfo=UTC)
+    end_datetime = datetime(2023, 9, 18, 14, 43, 9, tzinfo=UTC)
+
+    incidents = generate_threat_incidents(client, [{'threatId': 'asdf097sdf907'}], 2, start_datetime, end_datetime)
+    assert len(incidents) == 1
+    assert len(json.loads(incidents[0].get('rawJSON')).get('messages')) == 2
+
+
+def test_get_details_of_a_threat_request_single_page(mocker):
+    return_val = util_load_json('test_data/test_get_details_of_a_threat_page2.json')
+    return_val['messages'][0]['remediationTimestamp'] = '2023-09-17T15:43:09Z'
+    client = mock_client(mocker, response=return_val)
+    # Create datetime objects instead of using strings
+    start_datetime = datetime(2023, 9, 17, 14, 43, 9, tzinfo=UTC)
+    end_datetime = datetime(2023, 9, 18, 14, 43, 9, tzinfo=UTC)
+
+    incidents = generate_threat_incidents(client, [{'threatId': 'asdf097sdf907'}], 1, start_datetime, end_datetime)
+    assert len(incidents) == 1
+
+
+def test_get_details_of_a_threat_request_nanosecond_timestamp(mocker, mock_get_details_of_a_threat_request):
+    client = mock_client(mocker, response=util_load_json("test_data/test_get_list_of_abnormal_threats.json"))
+    last_run = {"last_fetch": "2023-09-17T14:43:09Z"}
+    first_fetch_time = "3 days"
+    max_incidents = 200
+    # Call fetch_incidents with the polling lag
+    _, incidents = fetch_incidents(
+        client=client,
+        last_run=last_run,
+        first_fetch_time=first_fetch_time,
+        max_incidents_to_fetch=max_incidents,
+        fetch_account_takeover_cases=False,
+        fetch_abuse_campaigns=False,
+        fetch_threats=True,
+    )
+    assert len(incidents) == 1
+    assert incidents[0].get('occurred') == "2023-12-03T19:26:36.123456"
+
+
+def test_polling_lag(mocker, mock_get_details_of_a_threat_request):
+    """Test that polling lag is correctly applied when fetching incidents."""
+    # Mock the client and its get_a_list_of_threats_request method
+    return_val = util_load_json("test_data/test_get_list_of_abnormal_threats.json")
+    client = mock_client(mocker, response=return_val)
+
+    # Create a spy on the get_a_list_of_threats_request method to capture its calls
+    get_threats_spy = mocker.spy(client, 'get_a_list_of_threats_request')
+
+    # Define test parameters
+    last_run = {"last_fetch": "2023-09-17T14:43:09Z"}
+    first_fetch_time = "3 days"
+    max_incidents = 200
+
+    # Set up a 5-minute polling lag
+    polling_lag = timedelta(minutes=5)
+
+    # Calculate expected timestamps
+    original_timestamp = datetime.fromisoformat(last_run["last_fetch"][:-1]).replace(tzinfo=UTC)
+    adjusted_start_time = original_timestamp - polling_lag
+    expected_start_time = adjusted_start_time.strftime("%Y-%m-%dT%H:%M:%SZ")
+
+    # Mock the get_current_datetime function to return a fixed time
+    fixed_current_time = datetime(2023, 9, 18, 14, 43, 9, tzinfo=UTC)
+    mocker.patch('AbnormalSecurity.get_current_datetime', return_value=fixed_current_time)
+
+    # Calculate expected end time based on the fixed current time
+    adjusted_end_time = fixed_current_time - polling_lag
+    expected_end_time = adjusted_end_time.strftime("%Y-%m-%dT%H:%M:%SZ")
+
+    expected_filter = f"latestTimeRemediated gte {expected_start_time} and latestTimeRemediated lte {expected_end_time}"
+
+    # Call fetch_incidents with the polling lag
+    _, incidents = fetch_incidents(
+        client=client,
+        last_run=last_run,
+        first_fetch_time=first_fetch_time,
+        max_incidents_to_fetch=max_incidents,
+        fetch_account_takeover_cases=False,
+        fetch_abuse_campaigns=False,
+        fetch_threats=True,
+        polling_lag=polling_lag
+    )
+
+    # Check that the method was called with the expected filter
+    get_threats_spy.assert_called_once()
+    call_args = get_threats_spy.call_args[1]
+
+    # Assert that the filter matches our expected filter
+    assert call_args['filter_'] == expected_filter
+    assert call_args['page_size'] == 100
+
+
+def test_get_details_of_a_threat_request_time_window_filtering(mocker):
+    """Test that messages outside the time window are filtered out."""
+    # Create a mock response with 3 messages
+    mock_response = {
+        "threatId": "test-threat-id",
+        "messages": [
+            {
+                "threatId": "test-threat-id",
+                "receivedTime": "2023-09-17T15:00:00Z",
+                "remediationTimestamp": "2023-09-17T15:30:00Z"  # Inside window
+            },
+            {
+                "threatId": "test-threat-id",
+                "receivedTime": "2023-09-17T16:00:00Z",
+                "remediationTimestamp": "2023-09-17T16:30:00Z"  # Inside window
+            },
+            {
+                "threatId": "test-threat-id",
+                "receivedTime": "2023-09-17T12:00:00Z",
+                "remediationTimestamp": "2023-09-17T12:30:00Z"  # Outside window (before start_time)
+            }
+        ]
+    }
+
+    client = mock_client(mocker, response=mock_response)
+
+    # Define time window that includes only the first two messages
+    start_datetime = datetime(2023, 9, 17, 14, 0, 0, tzinfo=UTC)
+    end_datetime = datetime(2023, 9, 17, 17, 0, 0, tzinfo=UTC)
+
+    incidents = generate_threat_incidents(client, [{'threatId': 'test-threat-id'}], 1, start_datetime, end_datetime)
+
+    # Verify we get one incident
+    assert len(incidents) == 1
+
+    # Verify the incident contains only the messages within the time window
+    incident_data = json.loads(incidents[0]['rawJSON'])
+    assert len(incident_data['messages']) == 2
+
+    # Verify the filtered messages are the ones we expect
+    remediation_times = [msg['remediationTimestamp'] for msg in incident_data['messages']]
+    assert '2023-09-17T15:30:00Z' in remediation_times
+    assert '2023-09-17T16:30:00Z' in remediation_times
+    assert '2023-09-17T12:30:00Z' not in remediation_times
+
+
+def test_get_details_of_a_threat_request_early_exit(mocker):
+    """Test that processing stops early when encountering messages outside the time window."""
+    # Create mock responses for two pages
+    # Page 1 with 2 messages (both inside time window)
+    page_1 = {
+        "threatId": "test-threat-id",
+        "messages": [
+            {
+                "threatId": "test-threat-id",
+                "receivedTime": "2023-09-17T16:00:00Z",
+                "remediationTimestamp": "2023-09-17T16:30:00Z"  # Inside window (latest)
+            },
+            {
+                "threatId": "test-threat-id",
+                "receivedTime": "2023-09-17T15:00:00Z",
+                "remediationTimestamp": "2023-09-17T15:30:00Z"  # Inside window
+            }
+        ],
+        "nextPageNumber": 2  # Indicate there's a second page
+    }
+
+    # Page 2 with 2 messages (both outside time window)
+    page_2 = {
+        "threatId": "test-threat-id",
+        "messages": [
+            {
+                "threatId": "test-threat-id",
+                "receivedTime": "2023-09-17T13:00:00Z",
+                "remediationTimestamp": "2023-09-17T13:30:00Z"  # Outside window
+            },
+            {
+                "threatId": "test-threat-id",
+                "receivedTime": "2023-09-17T12:00:00Z",
+                "remediationTimestamp": "2023-09-17T12:30:00Z"  # Outside window (earliest)
+            }
+        ],
+        "nextPageNumber": None  # No more pages
+    }
+
+    # Create a spy for the get_details_of_a_threat_request method
+    client = mock_client(mocker, side_effect=[page_1, page_2])
+    get_details_spy = mocker.spy(client, 'get_details_of_a_threat_request')
+
+    # Define time window that includes only the first two messages
+    start_datetime = datetime(2023, 9, 17, 14, 0, 0, tzinfo=UTC)
+    end_datetime = datetime(2023, 9, 17, 17, 0, 0, tzinfo=UTC)
+
+    incidents = generate_threat_incidents(client, [{'threatId': 'test-threat-id'}], 3, start_datetime, end_datetime)
+
+    # Verify we get one incident
+    assert len(incidents) == 1
+
+    # Verify the incident contains only the messages within the time window
+    incident_data = json.loads(incidents[0]['rawJSON'])
+    assert len(incident_data['messages']) == 2
+
+    # Verify the filtered messages are the ones we expect (from page 1 only)
+    remediation_times = [msg['remediationTimestamp'] for msg in incident_data['messages']]
+    assert '2023-09-17T16:30:00Z' in remediation_times
+    assert '2023-09-17T15:30:00Z' in remediation_times
+    assert '2023-09-17T13:30:00Z' not in remediation_times
+    assert '2023-09-17T12:30:00Z' not in remediation_times
+
+    # Verify that get_details_of_a_threat_request was called exactly twice
+    # (once for page 1, once for page 2 where we encounter messages outside the time window and exit early)
+    assert get_details_spy.call_count == 2
+
+    # Verify the calls were made with the correct page numbers
+    first_call_args = get_details_spy.call_args_list[0][1]
+    second_call_args = get_details_spy.call_args_list[1][1]
+    assert first_call_args['page_number'] == 1
+    assert second_call_args['page_number'] == 2
+
+
+def test_pagination_methods_in_fetch_incidents(mocker):
+    """
+    Test that the pagination methods are called correctly from fetch_incidents.
+    This test verifies:
+    1. The methods are called with the correct parameters
+    2. The pagination logic is executed as expected
+    3. The returned incidents are correctly processed
+    """
+    # Create mock pagination side effects for threats, cases, and campaigns
+    threat_list_side_effect = create_mock_paginator_side_effect("threat")
+    case_list_side_effect = create_mock_paginator_side_effect("case")
+    campaign_list_side_effect = create_mock_paginator_side_effect("campaign")
+
+    # Create mock detail side effects
+    threat_detail_side_effect = create_mock_detail_side_effect("threat")
+    case_detail_side_effect = create_mock_detail_side_effect("case")
+    campaign_detail_side_effect = create_mock_detail_side_effect("campaign")
+
+    # Create client
+    client = Client(
+        server_url=BASE_URL,
+        verify=False,
+        proxy=False,
+        auth=None,
+        headers=headers
+    )
+
+    # Get threat response samples for the mock
+    threat_page1 = threat_list_side_effect(page_number=1, page_size=2)
+    threat_page2 = threat_list_side_effect(page_number=2, page_size=2)
+
+    # Get case response samples for the mock
+    case_page1 = case_list_side_effect(page_number=1, page_size=2)
+    case_page2 = case_list_side_effect(page_number=2, page_size=2)
+
+    # Get campaign response samples for the mock
+    campaign_page1 = campaign_list_side_effect(page_number=1, page_size=2)
+    campaign_page2 = campaign_list_side_effect(page_number=2, page_size=2)
+
+    # Extract threat IDs for detail responses - for each page we'll get exactly page_size items
+    threat_ids = [threat['threatId'] for threat in threat_page1.get('threats')[:2] + threat_page2.get('threats')[:2]]
+    case_ids = [case['caseId'] for case in case_page1.get('cases')[:2] + case_page2.get('cases')[:2]]
+    campaign_ids = [campaign['campaignId']
+                    for campaign in campaign_page1.get('campaigns')[:2] + campaign_page2.get('campaigns')[:2]]
+
+    # Combine responses for each type
+    threats_combined = {
+        "threats": threat_page1.get('threats') + threat_page2.get('threats')
+    }
+
+    cases_combined = {
+        "cases": case_page1.get('cases') + case_page2.get('cases')
+    }
+
+    campaigns_combined = {
+        "campaigns": campaign_page1.get('campaigns') + campaign_page2.get('campaigns')
+    }
+
+    # Set up test parameters
+    last_run = {"last_fetch": "2023-09-17T14:43:09Z"}
+    first_fetch_time = "3 days"
+    max_incidents = 200
+    polling_lag = timedelta(minutes=5)
+
+    # Mock the three pagination methods
+    get_paginated_threats_spy = mocker.patch.object(
+        client, 'get_paginated_threats_list', return_value=threats_combined
+    )
+
+    get_paginated_cases_spy = mocker.patch.object(
+        client, 'get_paginated_cases_list', return_value=cases_combined
+    )
+
+    get_paginated_campaigns_spy = mocker.patch.object(
+        client, 'get_paginated_abusecampaigns_list', return_value=campaigns_combined
+    )
+
+    # Mock the get_details methods to return appropriate data for incident generation
+    mocker.patch.object(
+        client, 'get_details_of_a_threat_request',
+        side_effect=lambda threat_id, **kwargs: threat_detail_side_effect(threat_id)
+    )
+
+    mocker.patch.object(
+        client, 'get_details_of_an_abnormal_case_request',
+        side_effect=lambda case_id, **kwargs: case_detail_side_effect(case_id)
+    )
+
+    mocker.patch.object(
+        client, 'get_details_of_an_abuse_mailbox_campaign_request',
+        side_effect=lambda campaign_id, **kwargs: campaign_detail_side_effect(campaign_id)
+    )
+
+    # Mock the get_current_datetime function to return a fixed time
+    mocker.patch('AbnormalSecurity.get_current_datetime', return_value=datetime(2023, 9, 18, 14, 43, 9, tzinfo=UTC))
+
+    # Call fetch_incidents with all three fetch options enabled
+    next_run, incidents = fetch_incidents(
+        client=client,
+        last_run=last_run,
+        first_fetch_time=first_fetch_time,
+        max_incidents_to_fetch=max_incidents,
+        fetch_account_takeover_cases=True,
+        fetch_abuse_campaigns=True,
+        fetch_threats=True,
+        polling_lag=polling_lag
+    )
+
+    # Verify the pagination methods were called with the correct filters
+
+    # 1. Verify threats pagination
+    get_paginated_threats_spy.assert_called_once()
+    threats_call_kwargs = get_paginated_threats_spy.call_args.kwargs
+
+    # Verify the filter contains latestTimeRemediated with adjusted time due to polling lag
+    assert "latestTimeRemediated gte" in threats_call_kwargs['filter_']
+    assert "latestTimeRemediated lte" in threats_call_kwargs['filter_']
+    assert threats_call_kwargs['max_incidents_to_fetch'] == max_incidents
+
+    # 2. Verify abuse campaigns pagination (this is called next in the code)
+    get_paginated_campaigns_spy.assert_called_once()
+    campaigns_call_kwargs = get_paginated_campaigns_spy.call_args.kwargs
+
+    # Verify the filter contains lastReportedTime
+    assert "lastReportedTime gte" in campaigns_call_kwargs['filter_']
+    assert campaigns_call_kwargs['max_incidents_to_fetch'] == max_incidents - len(threat_ids)
+
+    # 3. Verify cases pagination (this is called last in the code)
+    get_paginated_cases_spy.assert_called_once()
+    cases_call_kwargs = get_paginated_cases_spy.call_args.kwargs
+    # Verify the filter contains lastModifiedTime
+    assert "lastModifiedTime gte" in cases_call_kwargs['filter_']
+    assert cases_call_kwargs['max_incidents_to_fetch'] == max_incidents - len(threat_ids) - len(campaign_ids)
+
+    # Verify we got the expected number of incidents
+    expected_incident_count = len(threat_ids) + len(case_ids) + len(campaign_ids)
+    assert len(incidents) == expected_incident_count
+
+    # Verify the types of incidents
+    threat_incidents = [i for i in incidents if i.get('name') == 'Threat']
+    case_incidents = [i for i in incidents if i.get('name') == 'Account Takeover Case']
+    campaign_incidents = [i for i in incidents if i.get('name') == 'Abuse Campaign']
+
+    assert len(threat_incidents) == len(threat_ids)
+    assert len(case_incidents) == len(case_ids)
+    assert len(campaign_incidents) == len(campaign_ids)
+
+    # Verify next_run contains updated last_fetch timestamp
+    assert next_run.get('last_fetch', None) is not None
+    assert next_run.get('last_fetch') > last_run.get('last_fetch')
+
+
+def test_get_paginated_threats_list(mocker):
+    """
+    Test the get_paginated_threats_list method to verify:
+    1. It correctly handles pagination
+    2. It respects the max_incidents_to_fetch parameter
+    """
+    # Create client
+    client = Client(
+        server_url=BASE_URL,
+        verify=False,
+        proxy=False,
+        auth=None,
+        headers=headers
+    )
+
+    # Create a side effect function for threats
+    get_threats_side_effect = create_mock_paginator_side_effect("threat")
+
+    # Mock the underlying get_a_list_of_threats_request method
+    get_threats_mock = mocker.patch.object(
+        client, 'get_a_list_of_threats_request',
+        side_effect=get_threats_side_effect
+    )
+
+    # Test case 1: Get all threats with high limit (max_incidents_to_fetch > existing items)
+    # This should set page_size to the limit (10) but return only as many items as exist
+    result = client.get_paginated_threats_list(filter_="test filter", max_incidents_to_fetch=10)
+
+    # Verify the result contains threats (the exact count depends on the mock function)
+    assert len(result['threats']) > 0
+
+    # Verify the first call was made with correct parameters
+    assert get_threats_mock.call_count >= 1
+    first_call_kwargs = get_threats_mock.call_args_list[0][1]
+    assert first_call_kwargs['filter_'] == "test filter"
+    assert first_call_kwargs['page_size'] == 10
+    assert first_call_kwargs['page_number'] == 1
+
+    # Reset the mock for the next test
+    get_threats_mock.reset_mock()
+
+    # Test case 2: Limited page size (max_incidents_to_fetch = 2)
+    # With many threats available and max_incidents_to_fetch=2, we expect page_size=2
+    # This should result in multiple page calls since there are more threats than fit on one page
+    result = client.get_paginated_threats_list(filter_="test filter", max_incidents_to_fetch=2)
+
+    # Verify we got threats
+    assert len(result['threats']) > 0
+
+    # Verify each page was requested with the correct parameters
+    assert get_threats_mock.call_count >= 1
+
+    # Check first call parameters
+    first_call_kwargs = get_threats_mock.call_args_list[0][1]
+    assert first_call_kwargs['filter_'] == "test filter"
+    assert first_call_kwargs['page_size'] == 2
+    assert first_call_kwargs['page_number'] == 1
+
+    # If there was a second call, check its parameters
+    if get_threats_mock.call_count > 1:
+        second_call_kwargs = get_threats_mock.call_args_list[1][1]
+        assert second_call_kwargs['page_size'] == 2
+        assert second_call_kwargs['page_number'] == 2
+
+    # Reset the mock for the next test
+    get_threats_mock.reset_mock()
+
+    # Test case 3: One threat per page (max_incidents_to_fetch = 1)
+    # With many threats available and max_incidents_to_fetch=1, we expect page_size=1
+    # This should result in multiple page calls, one per threat
+    result = client.get_paginated_threats_list(filter_="test filter", max_incidents_to_fetch=1)
+
+    # Verify we got threats
+    assert len(result['threats']) > 0
+
+    # Verify multiple pages were requested
+    assert get_threats_mock.call_count >= 1
+
+    # Check that all calls have the correct page_size
+    for i in range(get_threats_mock.call_count):
+        call_kwargs = get_threats_mock.call_args_list[i][1]
+        assert call_kwargs['page_size'] == 1
+        assert call_kwargs['page_number'] == i + 1
+
+    # Reset the mock for the next test
+    get_threats_mock.reset_mock()
+
+    # Test case 4: No threats to fetch (max_incidents_to_fetch = 0)
+    result = client.get_paginated_threats_list(filter_="test filter", max_incidents_to_fetch=0)
+
+    # Verify that no threats were fetched
+    assert len(result['threats']) == 0
+
+    # Verify that the underlying method was not called
+    assert get_threats_mock.call_count == 0
+
+
+def test_get_paginated_cases_list(mocker):
+    """
+    Test the get_paginated_cases_list method to verify:
+    1. It correctly handles pagination
+    2. It respects the max_incidents_to_fetch parameter
+    """
+    # Create client
+    client = Client(
+        server_url=BASE_URL,
+        verify=False,
+        proxy=False,
+        auth=None,
+        headers=headers
+    )
+
+    # Create a side effect function for cases
+    get_cases_side_effect = create_mock_paginator_side_effect("case")
+
+    # Mock the underlying get_a_list_of_abnormal_cases_identified_by_abnormal_security_request method
+    get_cases_mock = mocker.patch.object(
+        client, 'get_a_list_of_abnormal_cases_identified_by_abnormal_security_request',
+        side_effect=get_cases_side_effect
+    )
+
+    # Test case 1: Get all cases with high limit (max_incidents_to_fetch > existing items)
+    # This should set page_size to the limit (10) but return only as many items as exist
+    result = client.get_paginated_cases_list(filter_="test filter", max_incidents_to_fetch=10)
+
+    # Verify the result contains cases (the exact count depends on the mock function)
+    assert len(result['cases']) > 0
+
+    # Verify the first call was made with correct parameters
+    assert get_cases_mock.call_count >= 1
+    first_call_kwargs = get_cases_mock.call_args_list[0][1]
+    assert first_call_kwargs['filter_'] == "test filter"
+    assert first_call_kwargs['page_size'] == 10
+    assert first_call_kwargs['page_number'] == 1
+
+    # Reset the mock for the next test
+    get_cases_mock.reset_mock()
+
+    # Test case 2: Limited page size (max_incidents_to_fetch = 2)
+    # With many cases available and max_incidents_to_fetch=2, we expect page_size=2
+    # This should result in multiple page calls since there are more cases than fit on one page
+    result = client.get_paginated_cases_list(filter_="test filter", max_incidents_to_fetch=2)
+
+    # Verify we got cases
+    assert len(result['cases']) > 0
+
+    # Verify each page was requested with the correct parameters
+    assert get_cases_mock.call_count >= 1
+
+    # Check first call parameters
+    first_call_kwargs = get_cases_mock.call_args_list[0][1]
+    assert first_call_kwargs['filter_'] == "test filter"
+    assert first_call_kwargs['page_size'] == 2
+    assert first_call_kwargs['page_number'] == 1
+
+    # If there was a second call, check its parameters
+    if get_cases_mock.call_count > 1:
+        second_call_kwargs = get_cases_mock.call_args_list[1][1]
+        assert second_call_kwargs['page_size'] == 2
+        assert second_call_kwargs['page_number'] == 2
+
+    # Reset the mock for the next test
+    get_cases_mock.reset_mock()
+
+    # Test case 3: One case per page (max_incidents_to_fetch = 1)
+    # With many cases available and max_incidents_to_fetch=1, we expect page_size=1
+    # This should result in multiple page calls, one per case
+    result = client.get_paginated_cases_list(filter_="test filter", max_incidents_to_fetch=1)
+
+    # Verify we got cases
+    assert len(result['cases']) > 0
+
+    # Verify multiple pages were requested
+    assert get_cases_mock.call_count >= 1
+
+    # Check that all calls have the correct page_size
+    for i in range(get_cases_mock.call_count):
+        call_kwargs = get_cases_mock.call_args_list[i][1]
+        assert call_kwargs['page_size'] == 1
+        assert call_kwargs['page_number'] == i + 1
+
+    # Reset the mock for the next test
+    get_cases_mock.reset_mock()
+
+    # Test case 4: No cases to fetch (max_incidents_to_fetch = 0)
+    result = client.get_paginated_cases_list(filter_="test filter", max_incidents_to_fetch=0)
+
+    # Verify that no cases were fetched
+    assert len(result['cases']) == 0
+
+    # Verify that the underlying method was not called
+    assert get_cases_mock.call_count == 0
+
+
+def test_get_paginated_abusecampaigns_list(mocker):
+    """
+    Test the get_paginated_abusecampaigns_list method to verify:
+    1. It correctly handles pagination
+    2. It respects the max_incidents_to_fetch parameter
+    """
+    # Create client
+    client = Client(
+        server_url=BASE_URL,
+        verify=False,
+        proxy=False,
+        auth=None,
+        headers=headers
+    )
+
+    # Create a side effect function for campaigns
+    get_campaigns_side_effect = create_mock_paginator_side_effect("campaign")
+
+    # Mock the underlying get_a_list_of_campaigns_submitted_to_abuse_mailbox_request method
+    get_campaigns_mock = mocker.patch.object(
+        client, 'get_a_list_of_campaigns_submitted_to_abuse_mailbox_request',
+        side_effect=get_campaigns_side_effect
+    )
+
+    # Test case 1: Get all campaigns with high limit (max_incidents_to_fetch > existing items)
+    # This should set page_size to the limit (10) but return only as many items as exist
+    result = client.get_paginated_abusecampaigns_list(filter_="test filter", max_incidents_to_fetch=10)
+
+    # Verify the result contains campaigns (the exact count depends on the mock function)
+    assert len(result['campaigns']) > 0
+
+    # Verify the first call was made with correct parameters
+    assert get_campaigns_mock.call_count >= 1
+    first_call_kwargs = get_campaigns_mock.call_args_list[0][1]
+    assert first_call_kwargs['filter_'] == "test filter"
+    assert first_call_kwargs['page_size'] == 10
+    assert first_call_kwargs['page_number'] == 1
+
+    # Reset the mock for the next test
+    get_campaigns_mock.reset_mock()
+
+    # Test case 2: Limited page size (max_incidents_to_fetch = 2)
+    # With many campaigns available and max_incidents_to_fetch=2, we expect page_size=2
+    # This should result in multiple page calls since there are more campaigns than fit on one page
+    result = client.get_paginated_abusecampaigns_list(filter_="test filter", max_incidents_to_fetch=2)
+
+    # Verify we got campaigns
+    assert len(result['campaigns']) > 0
+
+    # Verify each page was requested with the correct parameters
+    assert get_campaigns_mock.call_count >= 1
+
+    # Check first call parameters
+    first_call_kwargs = get_campaigns_mock.call_args_list[0][1]
+    assert first_call_kwargs['filter_'] == "test filter"
+    assert first_call_kwargs['page_size'] == 2
+    assert first_call_kwargs['page_number'] == 1
+
+    # If there was a second call, check its parameters
+    if get_campaigns_mock.call_count > 1:
+        second_call_kwargs = get_campaigns_mock.call_args_list[1][1]
+        assert second_call_kwargs['page_size'] == 2
+        assert second_call_kwargs['page_number'] == 2
+
+    # Reset the mock for the next test
+    get_campaigns_mock.reset_mock()
+
+    # Test case 3: One campaign per page (max_incidents_to_fetch = 1)
+    # With many campaigns available and max_incidents_to_fetch=1, we expect page_size=1
+    # This should result in multiple page calls, one per campaign
+    result = client.get_paginated_abusecampaigns_list(filter_="test filter", max_incidents_to_fetch=1)
+
+    # Verify we got campaigns
+    assert len(result['campaigns']) > 0
+
+    # Verify multiple pages were requested
+    assert get_campaigns_mock.call_count >= 1
+
+    # Check that all calls have the correct page_size
+    for i in range(get_campaigns_mock.call_count):
+        call_kwargs = get_campaigns_mock.call_args_list[i][1]
+        assert call_kwargs['page_size'] == 1
+        assert call_kwargs['page_number'] == i + 1
+
+    # Reset the mock for the next test
+    get_campaigns_mock.reset_mock()
+
+    # Test case 4: No campaigns to fetch (max_incidents_to_fetch = 0)
+    result = client.get_paginated_abusecampaigns_list(filter_="test filter", max_incidents_to_fetch=0)
+
+    # Verify that no campaigns were fetched
+    assert len(result['campaigns']) == 0
+
+    # Verify that the underlying method was not called
+    assert get_campaigns_mock.call_count == 0
diff --git a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/README.md b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/README.md
index c186ad08970f..b3caf9c2368f 100644
--- a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/README.md
+++ b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/README.md
@@ -4,12 +4,24 @@ This integration was integrated and tested with version 1.3.0 of Abnormal Securi
 ## Configure Abnormal Security in Cortex
 
 
-| **Parameter**                                         | **Required** |
-| ----------------------------------------------------- | ------------ |
-| Server URL (e.g. https://api.abnormalplatform.com/v1) | True         |
-| API Key                                               | True         |
-| Trust any certificate (not secure)                    | False        |
-| Use system proxy settings                             | False        |
+
+| **Parameter** | **Description** | **Required** |
+| --- | --- | --- |
+| Server URL (e.g. https://api.abnormalplatform.com/v1) |  | True |
+| API Key |  | True |
+| Trust any certificate (not secure) |  | False |
+| Use system proxy settings |  | False |
+| Fetch incidents |  | False |
+| Maximum incidents to fetch. | Maximum number of incidents per fetch. The default value is 200. | False |
+| Fetch Threats |  | False |
+| Fetch Abuse Campaigns |  | False |
+| Fetch Account Takeover Cases |  | False |
+| First fetch time | First alert created date to fetch. e.g., "1 min ago","2 weeks ago","3 months ago" | False |
+| Incident type |  | False |
+| Incidents Fetch Interval |  | False |
+| Polling Lag Time (in minutes) | Time in minutes to subtract from polling time window for data consistency | False |
+| Maximum incidents pages to fetch | Maximum number of pages to fetch for incidents | False |
+
 
 
 ## Commands
@@ -306,7 +318,7 @@ Get a list of threats
 | **Argument Name** | **Description**                                                                                                                                                                                                                                                                                                                          | **Required** |
 | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
 | filter            | Value must be of the format `filter={FILTER KEY} gte YYYY-MM-DDTHH:MM:SSZ lte YYYY-MM-DDTHH:MM:SSZ`. A `{FILTER KEY}` must be specified, and currently the only key that is supported for `/threats` is `receivedTime`. At least 1 of `gte`/`lte` must be specified, with a datetime string following the `YYYY-MM-DDTHH:MM:SSZ format`. | Optional     |
-| page_size         | Number of threats that on in each page. Each page of data will have at most page_size threats. Has no effect if filter is not specified.                                                                                                                                                                                                 | Optional     |
+| page_size         | Number of threats per page. Each page will contain up to page_size threats. This has no effect if no filter is specified.                                                                                                                                                                                                 | Optional     |
 | page_number       | 1-indexed page number to get a particular page of threats. Has no effect if filter is not specified.                                                                                                                                                                                                                                     | Optional     |
 | mock-data         | Returns test data if set to `True`.                                                                                                                                                                                                                                                                                                      | Optional     |
 | source            | Filters threats based on the source of detection.                                                                                                                                                                                                                                                                                        | Optional     |
@@ -364,11 +376,13 @@ Get details of a threat
 
 #### Input
 
-| **Argument Name** | **Description**                                                                                                               | **Required** |
-| ----------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------ |
-| threat_id         | A UUID representing a threat campaign. Full list of threat IDs can be obtained by first running the command to list a threat. | Required     |
-| mock-data         | Returns test data if set to `True`.                                                                                           | Optional     |
-| subtenant         | Subtenant of the user (if applicable).                                                                                        | Optional     |
+| **Argument Name** | **Description**                                                                                                                           | **Required** |
+| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
+| threat_id         | A UUID representing a threat campaign. Full list of threat IDs can be obtained by first running the command to list a threat.             | Required     |
+| mock-data         | Returns test data if set to `True`.                                                                                                       | Optional     |
+| subtenant         | Subtenant of the user (if applicable).                                                                                                    | Optional     |
+| page_size         | Number of threats per page. Each page will contain up to page_size threats. This has no effect if no filter is specified.                 | Optional     | 
+| page_number       | 1-indexed page number to get a particular page of threats. Has no effect if filter is not specified.                                      | Optional     | 
 
 #### Context Output
 
@@ -491,6 +505,7 @@ Get details of an Abnormal case
 | AbnormalSecurity.AbnormalCaseDetails.severity         | String   | Description of the severity level for this case. |
 | AbnormalSecurity.AbnormalCaseDetails.affectedEmployee | String   | Which employee this case pertains to.            |
 | AbnormalSecurity.AbnormalCaseDetails.firstObserved    | String   | First time suspicious behavior was observed.     |
+| AbnormalSecurity.AbnormalCaseDetails.genai_summary    | String   | Gen AI summary for this case                     |
 
 #### Command Example
 
@@ -509,7 +524,8 @@ Get details of an Abnormal case
             "firstObserved": "2020-06-09T17:42:59Z",
             "remediation_status": "Not remediated",
             "severity": "Potential Account Takeover",
-            "threatIds": ["184712ab-6d8b-47b3-89d3-a314efef79e2"]
+            "threatIds": ["184712ab-6d8b-47b3-89d3-a314efef79e2"],
+            "genai_summary": "Observed 2 lateral phishing emails sent internally from the user's account"
         }
     }
 }
diff --git a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/command_examples.txt b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/command_examples.txt
index bc1661a77278..13ed3b4cfdb9 100644
--- a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/command_examples.txt
+++ b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/command_examples.txt
@@ -1,7 +1,7 @@
 !abnormal-security-check-case-action-status case_id=12345 action_id=abcdefgh-1234-5678-ijkl-mnop9qrstuvwx
 !abnormal-security-check-threat-action-status threat_id=xwvutsrq-9pon-mlkj-i876-54321hgfedcba action_id=abcdefgh-1234-5678-ijkl-mnop9qrstuvwx
 !abnormal-security-list-abnormal-cases filter="lastModifiedTime gte 2020-12-01T01:01:01Z"
-!abnormal-security-list-threats filter="receivedTime gte 2020-12-01T01:01:01Z"
+!abnormal-security-list-threats filter="latestTimeRemediated gte 2020-12-01T01:01:01Z"
 !abnormal-security-download-threat-log-csv filter="receivedTime gte 2020-12-01T01:01:01Z"
 !abnormal-security-get-threat threat_id=xwvutsrq-9pon-mlkj-i876-54321hgfedcba
 !abnormal-security-get-abnormal-case case_id=12805
diff --git a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/mock_paginated_response.py b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/mock_paginated_response.py
new file mode 100644
index 000000000000..b1d2e7a4cb47
--- /dev/null
+++ b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/mock_paginated_response.py
@@ -0,0 +1,212 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+Utility module for generating paginated mock responses for testing.
+This module provides functions to create realistic paginated responses
+with configurable properties for testing API pagination.
+
+Templates for items (threats, cases, campaigns) are loaded from template_items.json.
+"""
+
+import copy
+import uuid
+import json
+import os
+
+
+def util_load_json(path):
+    """Load a JSON file from the given path."""
+    with open(path, encoding='utf-8') as f:
+        return json.loads(f.read())
+
+
+def create_paginated_response(template_item, item_key, count=10, items_per_page=None, max_pages=None):
+    """
+    Create a paginated mock response with multiple pages based on a template item.
+
+    Args:
+        template_item (dict): Template for a single item (e.g., a single threat, case, or campaign)
+        item_key (str): The key under which items are stored (e.g., 'threats', 'cases', 'campaigns')
+        count (int): Total number of items to generate
+        items_per_page (int, optional): Number of items per page. If None, all items in a single page.
+        max_pages (int, optional): Maximum number of pages to generate. If None, generate enough pages for all items.
+
+    Returns:
+        dict: A dictionary with all generated items and pagination information
+    """
+    # Generate the items
+    items = []
+    for i in range(count):
+        item = copy.deepcopy(template_item)
+
+        # Add unique ID if it exists in template
+        for id_field in ['threatId', 'caseId', 'campaignId']:
+            if id_field in item:
+                item[id_field] = f"{id_field.replace('Id', '')}-{i+1}"
+
+        # Add any other unique identifiers if needed
+        if 'abxMessageId' in item:
+            item['abxMessageId'] = str(uuid.uuid4())
+
+        items.append(item)
+
+    # Create the paginated structure
+    if items_per_page is None:
+        # Return all items in a single page
+        return {
+            item_key: items,
+            "pageNumber": 1,
+            "nextPageNumber": None
+        }
+
+    # Calculate how many pages we need
+    total_pages = (count + items_per_page - 1) // items_per_page
+    if max_pages is not None:
+        total_pages = min(total_pages, max_pages)
+
+    # Create the pages
+    pages = {}
+    for page_num in range(1, total_pages + 1):
+        start_idx = (page_num - 1) * items_per_page
+        end_idx = min(start_idx + items_per_page, count)
+
+        next_page = page_num + 1 if page_num < total_pages else None
+
+        pages[f"page{page_num}"] = {
+            item_key: items[start_idx:end_idx],
+            "pageNumber": page_num,
+            "nextPageNumber": next_page
+        }
+
+    return pages
+
+
+def create_mock_paginator_side_effect(item_type):
+    """
+    Create a function that can be used as a side effect for mock API calls with pagination.
+
+    Args:
+        item_type (str): The type of item to use as a template ('threat', 'case', or 'campaign')
+                         This is used to load the appropriate template and derive the item_key.
+
+    Returns:
+        function: A function to use as a side effect for mocked API calls
+    """
+    # Map item types to their plural forms for API responses
+    item_key_map = {
+        'threat': 'threats',
+        'case': 'cases',
+        'campaign': 'campaigns'
+    }
+
+    # Derive the item_key from the item_type
+    item_key = item_key_map.get(item_type)
+    if not item_key:
+        raise ValueError(f"Unknown item type: {item_type}. Must be one of: {', '.join(item_key_map.keys())}")
+
+    # Load template items from JSON
+    current_dir = os.path.dirname(os.path.abspath(__file__))
+    template_path = os.path.join(current_dir, 'template_items.json')
+    template_items = util_load_json(template_path)
+
+    # Get the list template for the current item type
+    template_item = template_items.get(item_type)
+    if not template_item:
+        raise ValueError(f"Template for {item_type} not found in template_items.json")
+
+    # Create a large pool of items to draw from
+    all_items = []
+    for i in range(100):  # Create 100 items as a pool
+        item = copy.deepcopy(template_item)
+
+        all_items.append(item)
+
+    def side_effect(**kwargs):
+        """Side effect function for mocking paginated API responses"""
+        page_number = kwargs.get('page_number', 1)
+        page_size = kwargs.get('page_size', 10)
+
+        # Calculate start and end indices
+        start_idx = (page_number - 1) * page_size
+        end_idx = min(start_idx + page_size, len(all_items))
+
+        # If start_idx is beyond our data, return empty page
+        if start_idx >= len(all_items):
+            return {
+                item_key: [],
+                "pageNumber": page_number,
+                "nextPageNumber": None
+            }
+
+        # Get the requested slice of items
+        page_items = all_items[start_idx:end_idx]
+
+        # Determine if there's a next page
+        next_page = page_number + 1 if end_idx < len(all_items) else None
+
+        # Build the response
+        response = {
+            item_key: page_items,
+            "pageNumber": page_number,
+            "nextPageNumber": next_page
+        }
+
+        return response
+
+    return side_effect
+
+
+def create_mock_detail_side_effect(item_type):
+    """
+    Create a function that can be used as a side effect for mock API detail calls.
+
+    Args:
+        item_type (str): The type of item to use as a template ('threat', 'case', or 'campaign')
+                         This is used to load the appropriate detail template.
+
+    Returns:
+        function: A function to use as a side effect for mocked API detail calls
+    """
+    # Load template items from JSON
+    current_dir = os.path.dirname(os.path.abspath(__file__))
+    template_path = os.path.join(current_dir, 'template_items.json')
+    template_items = util_load_json(template_path)
+
+    # Load the detail template
+    detail_template_key = f"{item_type}_detail"
+    detail_template = template_items.get(detail_template_key)
+    if not detail_template:
+        raise ValueError(f"Detail template for {detail_template_key} not found in template_items.json")
+
+    # Create a pool of detail items
+    detail_items = {}
+    for i in range(100):
+        item_id = f"{item_type}-{i+1}"
+        detail_item = copy.deepcopy(detail_template)
+
+        # Set the correct ID
+        for id_field in ['threatId', 'caseId', 'campaignId']:
+            if id_field in detail_item:
+                detail_item[id_field] = item_id
+
+        detail_items[item_id] = detail_item
+
+    def side_effect(item_id, **kwargs):
+        """Side effect function for mocking detail API responses"""
+        # If we have a detail for this ID, return it
+        if item_id in detail_items:
+            return detail_items[item_id]
+
+        # If not, create a new one based on the template
+        detail_item = copy.deepcopy(detail_template)
+        for id_field in ['threatId', 'caseId', 'campaignId']:
+            if id_field in detail_item:
+                detail_item[id_field] = item_id
+
+        # Add the item to our cache
+        detail_items[item_id] = detail_item
+
+        return detail_item
+
+    return side_effect
diff --git a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/template_items.json b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/template_items.json
new file mode 100644
index 000000000000..66f3b9dd23d4
--- /dev/null
+++ b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/template_items.json
@@ -0,0 +1,97 @@
+{
+  "threat": {
+    "threatId": "template-threat",
+    "remediationTimestamp": "2023-09-17T15:00:00Z",
+    "description": "Template threat description",
+    "subject": "Suspicious Email - Possible Phishing Attempt"
+  },
+  "threat_detail": {
+    "threatId": "template-threat-detail",
+    "messages": [
+      {
+        "abxMessageId": "abx-123456",
+        "abxPortalUrl": "https://portal.abnormal.com/threats/abx-123456",
+        "attachmentCount": 2,
+        "attachmentNames": ["invoice.pdf", "document.docx"],
+        "attackStrategy": "Credential Theft",
+        "attackType": "Phishing",
+        "attackVector": "Email",
+        "attackedParty": "Corporate",
+        "autoRemediated": false,
+        "fromAddress": "attacker@malicious.com",
+        "fromName": "John Smith",
+        "impersonatedParty": "Microsoft Office 365",
+        "internetMessageId": "<message-id@example.com>",
+        "isRead": false,
+        "postRemediated": false,
+        "receivedTime": "2023-09-17T14:55:00Z",
+        "recipientAddress": "victim@example.com",
+        "remediationStatus": "Remediated",
+        "remediationTimestamp": "2023-09-17T15:00:00Z",
+        "sentTime": "2023-09-17T14:50:00Z",
+        "subject": "Suspicious Email - Possible Phishing Attempt",
+        "toAddresses": ["victim@example.com", "employee@example.com"],
+        "ccEmails": ["cc@example.com"],
+        "replyToEmails": ["reply@malicious.com"],
+        "returnPath": "<bounce@malicious.com>",
+        "senderDomain": "malicious.com",
+        "senderIpAddress": "192.168.1.100",
+        "summaryInsights": [
+          "Contains suspicious sender",
+          "Contains suspicious link",
+          "Impersonates known brand",
+          "Urgent request for action"
+        ],
+        "urlCount": 2,
+        "urls": [
+          "https://malicious-site.com/login",
+          "https://tracking.malicious-site.com/pixel.gif"
+        ],
+        "body": "Dear User, Your account has been compromised. Please login to secure your account: <a href='https://malicious-site.com/login'>Secure Account</a>"
+      }
+    ],
+    "pageNumber": 1,
+    "nextPageNumber": null
+  },
+  "case": {
+    "caseId": "template-case",
+    "description": "Template account takeover case description",
+    "lastModifiedTime": "2023-09-17T15:00:00Z"
+  },
+  "case_detail": {
+    "caseId": "template-case-detail",
+    "severity": "High",
+    "affectedEmployee": "user@example.com",
+    "firstObserved": "2023-09-17T14:45:00Z",
+    "threatIds": ["threat-123", "threat-456"],
+    "genai_summary": "Account takeover attempt detected for user@example.com. Multiple login attempts observed from unusual locations. Two related phishing threats identified targeting the user's credentials.",
+    "status": "Open",
+    "caseType": "Account Takeover",
+    "assignedTo": "security-analyst@example.com",
+    "lastModifiedTime": "2023-09-17T15:00:00Z",
+    "createdBy": "security-system@example.com",
+    "createdTime": "2023-09-17T14:46:00Z",
+    "resolution": "",
+    "resolutionTime": null
+  },
+  "campaign": {
+    "campaignId": "template-campaign",
+    "lastReportedTime": "2023-09-17T15:00:00Z"
+  },
+  "campaign_detail": {
+    "campaignId": "template-campaign-detail",
+    "firstReported": "2023-09-17T14:50:00Z",
+    "lastReported": "2023-09-17T15:00:00Z",
+    "messageId": "abx-789012",
+    "subject": "Phishing Campaign - Fake Invoice",
+    "fromName": "Amazon Billing",
+    "fromAddress": "billing@malicious-domain.com",
+    "recipientName": "Finance Department",
+    "recipientAddress": "finance@example.com",
+    "judgementStatus": "Malicious",
+    "overallStatus": "Remediated",
+    "attackType": "Attack Type: Phishing",
+    "reporter": "security@example.com",
+    "threatType": "Credential Theft"
+  }
+} 
\ No newline at end of file
diff --git a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_details_of_a_threat.json b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_details_of_a_threat.json
index 6de41a8137d6..a3d741e9b6c7 100644
--- a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_details_of_a_threat.json
+++ b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_details_of_a_threat.json
@@ -17,7 +17,7 @@
             "internetMessageId": "Some internet message id",
             "isRead": false,
             "postRemediated": false,
-            "receivedTime": "2023-12-03T19:26:36Z",
+            "receivedTime": "2023-12-03T19:26:36.123456789Z",
             "recipientAddress": "some recipient address",
             "remediationStatus": "some remediation status",
             "remediationTimestamp": "2023-12-09T23:06:16.066762Z",
@@ -45,5 +45,7 @@
                 "https://someurl.com"
             ]
         }
-    ]
+    ],
+    "pageNumber": 1,
+    "nextPageNumber": 2
 }
diff --git a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_details_of_a_threat_page2.json b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_details_of_a_threat_page2.json
new file mode 100644
index 000000000000..e678e570b9c6
--- /dev/null
+++ b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_details_of_a_threat_page2.json
@@ -0,0 +1,50 @@
+{
+    "threatId": "asdf097sdf907",
+    "messages": [
+        {
+            "abxMessageId": -123456790,
+            "abxPortalUrl": "https://someurl.com",
+            "attachmentCount": 0,
+            "attachmentNames": [],
+            "attackStrategy": "Some attack strategy",
+            "attackType": "Some attack type",
+            "attackVector": "Some attack vector",
+            "attackedParty": "Some attacked party",
+            "autoRemediated": false,
+            "fromAddress": "some@test.com",
+            "fromName": "some name",
+            "impersonatedParty": "Some impersonated party",
+            "internetMessageId": "Some internet message id",
+            "isRead": false,
+            "postRemediated": false,
+            "receivedTime": "2023-12-03T19:26:36.123456789Z",
+            "recipientAddress": "some recipient address",
+            "remediationStatus": "some remediation status",
+            "remediationTimestamp": "2023-12-09T23:06:16.066762Z",
+            "sentTime": "2023-12-03T19:26:36Z",
+            "subject": "some subject",
+            "threatId": "asdf097sdf907",
+            "toAddresses": ["some@test.com"],
+            "ccEmails": [],
+            "replyToEmails": [],
+            "returnPath": null,
+            "senderDomain": "some.com",
+            "senderIpAddress": null,
+            "summaryInsights": [
+                "Insight #1",
+                "Insight #2",
+                "Insight #3",
+                "Insight #4"
+            ],
+            "urlCount": 10,
+            "urls": [
+                "https://someurl.com",
+                "https://someurl.com",
+                "https://someurl.com",
+                "https://someurl.com",
+                "https://someurl.com"
+            ]
+        }
+    ],
+    "pageNumber": 2
+}
diff --git a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_details_of_an_abnormal_case.json b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_details_of_an_abnormal_case.json
index 72f41068da5c..8be94d5b0e08 100644
--- a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_details_of_an_abnormal_case.json
+++ b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_details_of_an_abnormal_case.json
@@ -5,5 +5,6 @@
     "firstObserved": "2020-06-09T17:42:59Z",
     "threatIds": [
         "184712ab-6d8b-47b3-89d3-a314efef79e2"
-    ]
+    ],
+    "genai_summary": "genai_summary"
 }
\ No newline at end of file
diff --git a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_list_of_abnormal_cases.json b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_list_of_abnormal_cases.json
index 513d56fb0562..29a762ec9394 100644
--- a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_list_of_abnormal_cases.json
+++ b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_list_of_abnormal_cases.json
@@ -6,5 +6,5 @@
         }
     ],
     "pageNumber": 1,
-    "nextPageNumber": 2
+    "nextPageNumber": null
 }
\ No newline at end of file
diff --git a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_list_of_abnormal_threats.json b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_list_of_abnormal_threats.json
index ef96e191281e..51e2907f8077 100644
--- a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_list_of_abnormal_threats.json
+++ b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_list_of_abnormal_threats.json
@@ -5,5 +5,5 @@
         }
     ],
     "pageNumber": 1,
-    "nextPageNumber": 2
+    "nextPageNumber": null
 }
diff --git a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_list_of_abuse_campaigns.json b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_list_of_abuse_campaigns.json
index 1b35df3cd4ef..8d063af4bafb 100644
--- a/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_list_of_abuse_campaigns.json
+++ b/Packs/AbnormalSecurity/Integrations/AbnormalSecurity/test_data/test_get_list_of_abuse_campaigns.json
@@ -5,5 +5,5 @@
         }
     ],
     "pageNumber": 1,
-    "nextPageNumber": 2
+    "nextPageNumber": null
 }
diff --git a/Packs/AbnormalSecurity/ReleaseNotes/2_3_0.md b/Packs/AbnormalSecurity/ReleaseNotes/2_3_0.md
new file mode 100644
index 000000000000..8043cc30b55a
--- /dev/null
+++ b/Packs/AbnormalSecurity/ReleaseNotes/2_3_0.md
@@ -0,0 +1,29 @@
+
+#### Incident Fields
+
+##### New: Abnormal Security Case GenAI Summary
+
+- New: Added the *Abnormal Security Case GenAI Summary* incident field.
+
+
+#### Integrations
+
+##### Abnormal Security
+- Updated the Docker image to: *demisto/python3:3.12.8.1983910*.
+
+- Added support for the following parameters:
+    - **Maximum incidents pages to fetch** that limits the number of incidents fetched in a single fetch_incidents call.
+    - **Polling Lag Time (in minutes)** that adds a custom lag while polling incidents.
+- Added support for the following arguments in **abnormal-security-get-threat** command, to enable pagination:
+    - *page_number*
+    - *page_size*
+- Fixed pagination issue in fetch-incidents to handle max_fetch below 100 properly.
+- Enhanced pagination support in fetch-incidents to reliably fetch more than 100 incidents across multiple pages, enabling comprehensive data retrieval for large-scale incident analysis.
+
+  
+#### Mappers
+
+##### Abnormal Security - Incoming Mapper
+
+- Updated the Abnormal Security - Incoming Mapper to include genai_summary field in case details.
+
diff --git a/Packs/AbnormalSecurity/pack_metadata.json b/Packs/AbnormalSecurity/pack_metadata.json
index a4c6e2cf21d8..782a6ec42da5 100644
--- a/Packs/AbnormalSecurity/pack_metadata.json
+++ b/Packs/AbnormalSecurity/pack_metadata.json
@@ -2,7 +2,7 @@
     "name": "Abnormal Security",
     "description": "Abnormal Security detects and protects against the whole spectrum of email attacks",
     "support": "partner",
-    "currentVersion": "2.2.12",
+    "currentVersion": "2.3.0",
     "author": "Abnormal Security",
     "url": "",
     "email": "support@abnormalsecurity.com",


Reviews:
user: login: inbalapt1
body: Hi @sumit-badsara , 
Thanks for your contribution, we really appreciate it!

I've left some comments—please review and address them.

Additionally, please add a short description in the release notes about the impact of your changes on clients (e.g., which commands will be affected by the pagination support you've added), along with the arguments you've added and the corresponding commands. Make sure to update them in the YAML and README files as well.

Thanks in advance!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:41:37Z
user: login: sumit-badsara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T19:46:46Z
user: login: sumit-badsara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T19:49:21Z
user: login: sumit-badsara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T19:52:45Z
user: login: sumit-badsara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T21:09:06Z
user: login: sumit-badsara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T21:09:10Z
user: login: sumit-badsara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T21:09:20Z
user: login: sumit-badsara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T21:09:25Z
user: login: sumit-badsara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T21:09:29Z
user: login: inbalapt1
body: Hi @sumit-badsara , 
Thanks for updating the code!
I've left some comments, please review and address them.
Also, please check and fix the pre-commit failures.
Please remove the script_runner file.

In addition, for adding you to the slack community- please send me your full name and email. 
Thanks!
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T09:42:13Z
user: login: sumit-badsara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T10:26:36Z
user: login: sumit-badsara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T10:26:40Z
user: login: sumit-badsara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T10:26:45Z
user: login: inbalapt1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T09:58:46Z
user: login: inbalapt1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T10:20:54Z
user: login: inbalapt1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T09:59:50Z
user: login: sumit-badsara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T10:08:31Z
user: login: inbalapt1
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T11:38:03Z

Review comments:
diff_hunk: @@ -57,6 +57,18 @@ configuration:
   name: incidentFetchInterval
   required: false
   type: 19
+- display: Polling Lag Time (in minutes)
+  name: polling_lag
+  defaultvalue: "0"
+  type: 0
+  required: false
+  additionalinfo: Time in minutes to subtract from polling time window for data consistency
+- display: Maximum number of pages to fetch for incidents
+  name: max_page_number
+  defaultvalue: "8"
+  type: 0
+  required: false
+  additionalinfo: Maximum number of pages to fetch for incidents
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.yml
user: login: inbalapt1
body: 
```suggestion
- display: Maximum incidents pages to fetch
  name: max_page_number
  defaultvalue: "8"
  type: 0
  required: false
  additionalinfo: Maximum number of pages to fetch for incidents
```
created_at: 2025-03-20T09:48:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -394,8 +485,10 @@ def get_a_list_of_threats_command(client, args):
 def get_details_of_a_threat_command(client, args):
     threat_id = str(args.get('threat_id', ''))
     subtenant = args.get('subtenant', None)
+    page_size = args.get('page_size', None)
+    page_number = args.get('page_number', None)
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
user: login: inbalapt1
body: Please append these args to this command in the yml file.
created_at: 2025-03-20T11:11:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -805,45 +930,63 @@ def fetch_incidents(
     - client (Client): Client object to interact with the API.
     - last_run (Dict[str, Any]): Dictionary containing details about the last time incidents were fetched.
     - first_fetch_time (str): ISO formatted string indicating the first time from which to start fetching incidents.
+    - max_page_number (int): Maximum number of pages to fetch for incidents.
     - max_incidents_to_fetch (int, optional): Maximum number of incidents to fetch. Defaults to FETCH_LIMIT.
+    - polling_lag (int, optional): Time in minutes to subtract from polling time window for data consistency. Defaults to 0.
 
     Returns:
     - Tuple[Dict[str, str], List[Dict]]: Tuple containing a dictionary with the `last_fetch` time and a list of fetched incidents.
     """
-
     try:
         last_fetch = last_run.get("last_fetch", first_fetch_time)
-        last_fetch_datetime = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)
-        last_fetch = last_fetch_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
+        last_fetch = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)
+
+        current_datetime = get_current_datetime()
+
+        start_time = last_fetch
+        end_time = datetime.now(timezone.utc)
+
+        if polling_lag is not None:
+            start_time = start_time - polling_lag
+            end_time = end_time - polling_lag
+
+        start_timestamp = start_time.strftime(ISO_8601_FORMAT)
+        end_timestamp = end_time.strftime(ISO_8601_FORMAT)
 
-        current_datetime = datetime.utcnow().astimezone(timezone.utc)
-        current_iso_format_time = current_datetime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
         all_incidents = []
+        current_pending_incidents_to_fetch = max_incidents_to_fetch
+        threat_incidents, abuse_campaign_incidents, account_takeover_cases_incidents = [], [], []
 
         if fetch_threats:
-            threats_filter = f"receivedTime gte {last_fetch}"
-            threats_response = client.get_a_list_of_threats_request(filter_=threats_filter, page_size=100)
-            all_incidents += generate_threat_incidents(client, threats_response.get('threats', []))
+            threats_filter = f"latestTimeRemediated gte {start_timestamp} and latestTimeRemediated lte {end_timestamp}"
+            threats_response = client.get_paginated_threats_list(
+                filter_=threats_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            threat_incidents = generate_threat_incidents(
+                client, threats_response.get('threats', []), max_page_number, start_time, end_time
+            )
+        current_pending_incidents_to_fetch -= len(threat_incidents)
 
         if fetch_abuse_campaigns:
-            abuse_campaigns_filter = f"lastReportedTime gte {last_fetch}"
-            abuse_campaigns_response = client.get_a_list_of_campaigns_submitted_to_abuse_mailbox_request(
-                filter_=abuse_campaigns_filter, page_size=100)
-            all_incidents += generate_abuse_campaign_incidents(client, abuse_campaigns_response.get('campaigns', []))
+            abuse_campaigns_filter = f"lastReportedTime gte {start_timestamp}"
+            abuse_campaigns_response = client.get_paginated_abusecampaigns_list(
+                filter_=abuse_campaigns_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            abuse_campaign_incidents = generate_abuse_campaign_incidents(client, abuse_campaigns_response.get('campaigns', []))
+        current_pending_incidents_to_fetch -= len(abuse_campaign_incidents)
 
         if fetch_account_takeover_cases:
-            account_takeover_cases_filter = f"lastModifiedTime gte {last_fetch}"
-            account_takeover_cases_response = client.get_a_list_of_abnormal_cases_identified_by_abnormal_security_request(
-                filter_=account_takeover_cases_filter, page_size=100)
-            all_incidents += generate_account_takeover_cases_incidents(
+            account_takeover_cases_filter = f"lastModifiedTime gte {start_timestamp}"
+            account_takeover_cases_response = client.get_paginated_cases_list(
+                filter_=account_takeover_cases_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            account_takeover_cases_incidents = generate_account_takeover_cases_incidents(
                 client, account_takeover_cases_response.get('cases', []))
 
+        all_incidents = threat_incidents + abuse_campaign_incidents + account_takeover_cases_incidents
     except Exception as e:
         logging.error(f"Failed fetching incidents: {e}")
         raise FetchIncidentsError(f"Error while fetching incidents: {e}")
 
     next_run = {
-        "last_fetch": current_iso_format_time
+        "last_fetch": current_datetime.strftime(ISO_8601_FORMAT)
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
user: login: inbalapt1
body: Can you explain why did you change the last_fetch to be ISO_8601_FORMAT and not TIME_FORMAT_WITHMS as before?
created_at: 2025-03-20T13:01:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -805,45 +930,63 @@ def fetch_incidents(
     - client (Client): Client object to interact with the API.
     - last_run (Dict[str, Any]): Dictionary containing details about the last time incidents were fetched.
     - first_fetch_time (str): ISO formatted string indicating the first time from which to start fetching incidents.
+    - max_page_number (int): Maximum number of pages to fetch for incidents.
     - max_incidents_to_fetch (int, optional): Maximum number of incidents to fetch. Defaults to FETCH_LIMIT.
+    - polling_lag (int, optional): Time in minutes to subtract from polling time window for data consistency. Defaults to 0.
 
     Returns:
     - Tuple[Dict[str, str], List[Dict]]: Tuple containing a dictionary with the `last_fetch` time and a list of fetched incidents.
     """
-
     try:
         last_fetch = last_run.get("last_fetch", first_fetch_time)
-        last_fetch_datetime = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)
-        last_fetch = last_fetch_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
+        last_fetch = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)
+
+        current_datetime = get_current_datetime()
+
+        start_time = last_fetch
+        end_time = datetime.now(timezone.utc)
+
+        if polling_lag is not None:
+            start_time = start_time - polling_lag
+            end_time = end_time - polling_lag
+
+        start_timestamp = start_time.strftime(ISO_8601_FORMAT)
+        end_timestamp = end_time.strftime(ISO_8601_FORMAT)
 
-        current_datetime = datetime.utcnow().astimezone(timezone.utc)
-        current_iso_format_time = current_datetime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
         all_incidents = []
+        current_pending_incidents_to_fetch = max_incidents_to_fetch
+        threat_incidents, abuse_campaign_incidents, account_takeover_cases_incidents = [], [], []
 
         if fetch_threats:
-            threats_filter = f"receivedTime gte {last_fetch}"
-            threats_response = client.get_a_list_of_threats_request(filter_=threats_filter, page_size=100)
-            all_incidents += generate_threat_incidents(client, threats_response.get('threats', []))
+            threats_filter = f"latestTimeRemediated gte {start_timestamp} and latestTimeRemediated lte {end_timestamp}"
+            threats_response = client.get_paginated_threats_list(
+                filter_=threats_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            threat_incidents = generate_threat_incidents(
+                client, threats_response.get('threats', []), max_page_number, start_time, end_time
+            )
+        current_pending_incidents_to_fetch -= len(threat_incidents)
 
         if fetch_abuse_campaigns:
-            abuse_campaigns_filter = f"lastReportedTime gte {last_fetch}"
-            abuse_campaigns_response = client.get_a_list_of_campaigns_submitted_to_abuse_mailbox_request(
-                filter_=abuse_campaigns_filter, page_size=100)
-            all_incidents += generate_abuse_campaign_incidents(client, abuse_campaigns_response.get('campaigns', []))
+            abuse_campaigns_filter = f"lastReportedTime gte {start_timestamp}"
+            abuse_campaigns_response = client.get_paginated_abusecampaigns_list(
+                filter_=abuse_campaigns_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            abuse_campaign_incidents = generate_abuse_campaign_incidents(client, abuse_campaigns_response.get('campaigns', []))
+        current_pending_incidents_to_fetch -= len(abuse_campaign_incidents)
 
         if fetch_account_takeover_cases:
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
user: login: inbalapt1
body: 
```suggestion
        if fetch_account_takeover_cases and current_pending_incidents_to_fetch > 0:
```
created_at: 2025-03-20T15:46:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -805,45 +930,63 @@ def fetch_incidents(
     - client (Client): Client object to interact with the API.
     - last_run (Dict[str, Any]): Dictionary containing details about the last time incidents were fetched.
     - first_fetch_time (str): ISO formatted string indicating the first time from which to start fetching incidents.
+    - max_page_number (int): Maximum number of pages to fetch for incidents.
     - max_incidents_to_fetch (int, optional): Maximum number of incidents to fetch. Defaults to FETCH_LIMIT.
+    - polling_lag (int, optional): Time in minutes to subtract from polling time window for data consistency. Defaults to 0.
 
     Returns:
     - Tuple[Dict[str, str], List[Dict]]: Tuple containing a dictionary with the `last_fetch` time and a list of fetched incidents.
     """
-
     try:
         last_fetch = last_run.get("last_fetch", first_fetch_time)
-        last_fetch_datetime = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)
-        last_fetch = last_fetch_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
+        last_fetch = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)
+
+        current_datetime = get_current_datetime()
+
+        start_time = last_fetch
+        end_time = datetime.now(timezone.utc)
+
+        if polling_lag is not None:
+            start_time = start_time - polling_lag
+            end_time = end_time - polling_lag
+
+        start_timestamp = start_time.strftime(ISO_8601_FORMAT)
+        end_timestamp = end_time.strftime(ISO_8601_FORMAT)
 
-        current_datetime = datetime.utcnow().astimezone(timezone.utc)
-        current_iso_format_time = current_datetime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
         all_incidents = []
+        current_pending_incidents_to_fetch = max_incidents_to_fetch
+        threat_incidents, abuse_campaign_incidents, account_takeover_cases_incidents = [], [], []
 
         if fetch_threats:
-            threats_filter = f"receivedTime gte {last_fetch}"
-            threats_response = client.get_a_list_of_threats_request(filter_=threats_filter, page_size=100)
-            all_incidents += generate_threat_incidents(client, threats_response.get('threats', []))
+            threats_filter = f"latestTimeRemediated gte {start_timestamp} and latestTimeRemediated lte {end_timestamp}"
+            threats_response = client.get_paginated_threats_list(
+                filter_=threats_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            threat_incidents = generate_threat_incidents(
+                client, threats_response.get('threats', []), max_page_number, start_time, end_time
+            )
+        current_pending_incidents_to_fetch -= len(threat_incidents)
 
         if fetch_abuse_campaigns:
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
user: login: inbalapt1
body: 
```suggestion
        if fetch_abuse_campaigns and current_pending_incidents_to_fetch > 0:
```
created_at: 2025-03-20T15:47:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -752,14 +845,34 @@ def get_a_list_of_unanalyzed_abuse_mailbox_campaigns_command(client, args):
     return command_results
 
 
-def generate_threat_incidents(client, threats):
+def generate_threat_incidents(client, threats, max_page_number, start_datetime, end_datetime):
     incidents = []
     for threat in threats:
-        threat_details = client.get_details_of_a_threat_request(threat["threatId"])
+        page_number = 1
+        all_filtered_messages = []
+        while page_number is not None:
+            threat_details = client.get_details_of_a_threat_request(threat["threatId"], page_number=page_number)
+            for message in threat_details["messages"]:
+                remediation_datetime = try_str_to_datetime(message.get("remediationTimestamp"))
+                if remediation_datetime and start_datetime <= remediation_datetime <= end_datetime:
+                    all_filtered_messages.append(message)
+                if remediation_datetime and remediation_datetime < start_datetime:
+                    break
+            page_number = threat_details.get('nextPageNumber', None)
+            if page_number is not None and page_number > max_page_number:
+                break
+
+        threat_details["messages"] = all_filtered_messages
+        if all_filtered_messages:
+            received_time = all_filtered_messages[0].get("receivedTime")
+        elif threat_details.get("messages", []):
+            received_time = threat_details.get("messages", [])[0].get("receivedTime")
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
user: login: inbalapt1
body: What is the difference between these two condition if you assign threat_details["messages"] to be all_filtered_messages?
created_at: 2025-03-20T16:35:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -805,45 +930,63 @@ def fetch_incidents(
     - client (Client): Client object to interact with the API.
     - last_run (Dict[str, Any]): Dictionary containing details about the last time incidents were fetched.
     - first_fetch_time (str): ISO formatted string indicating the first time from which to start fetching incidents.
+    - max_page_number (int): Maximum number of pages to fetch for incidents.
     - max_incidents_to_fetch (int, optional): Maximum number of incidents to fetch. Defaults to FETCH_LIMIT.
+    - polling_lag (int, optional): Time in minutes to subtract from polling time window for data consistency. Defaults to 0.
 
     Returns:
     - Tuple[Dict[str, str], List[Dict]]: Tuple containing a dictionary with the `last_fetch` time and a list of fetched incidents.
     """
-
     try:
         last_fetch = last_run.get("last_fetch", first_fetch_time)
-        last_fetch_datetime = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)
-        last_fetch = last_fetch_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
+        last_fetch = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)
+
+        current_datetime = get_current_datetime()
+
+        start_time = last_fetch
+        end_time = datetime.now(timezone.utc)
+
+        if polling_lag is not None:
+            start_time = start_time - polling_lag
+            end_time = end_time - polling_lag
+
+        start_timestamp = start_time.strftime(ISO_8601_FORMAT)
+        end_timestamp = end_time.strftime(ISO_8601_FORMAT)
 
-        current_datetime = datetime.utcnow().astimezone(timezone.utc)
-        current_iso_format_time = current_datetime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
         all_incidents = []
+        current_pending_incidents_to_fetch = max_incidents_to_fetch
+        threat_incidents, abuse_campaign_incidents, account_takeover_cases_incidents = [], [], []
 
         if fetch_threats:
-            threats_filter = f"receivedTime gte {last_fetch}"
-            threats_response = client.get_a_list_of_threats_request(filter_=threats_filter, page_size=100)
-            all_incidents += generate_threat_incidents(client, threats_response.get('threats', []))
+            threats_filter = f"latestTimeRemediated gte {start_timestamp} and latestTimeRemediated lte {end_timestamp}"
+            threats_response = client.get_paginated_threats_list(
+                filter_=threats_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            threat_incidents = generate_threat_incidents(
+                client, threats_response.get('threats', []), max_page_number, start_time, end_time
+            )
+        current_pending_incidents_to_fetch -= len(threat_incidents)
 
         if fetch_abuse_campaigns:
-            abuse_campaigns_filter = f"lastReportedTime gte {last_fetch}"
-            abuse_campaigns_response = client.get_a_list_of_campaigns_submitted_to_abuse_mailbox_request(
-                filter_=abuse_campaigns_filter, page_size=100)
-            all_incidents += generate_abuse_campaign_incidents(client, abuse_campaigns_response.get('campaigns', []))
+            abuse_campaigns_filter = f"lastReportedTime gte {start_timestamp}"
+            abuse_campaigns_response = client.get_paginated_abusecampaigns_list(
+                filter_=abuse_campaigns_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            abuse_campaign_incidents = generate_abuse_campaign_incidents(client, abuse_campaigns_response.get('campaigns', []))
+        current_pending_incidents_to_fetch -= len(abuse_campaign_incidents)
 
         if fetch_account_takeover_cases:
-            account_takeover_cases_filter = f"lastModifiedTime gte {last_fetch}"
-            account_takeover_cases_response = client.get_a_list_of_abnormal_cases_identified_by_abnormal_security_request(
-                filter_=account_takeover_cases_filter, page_size=100)
-            all_incidents += generate_account_takeover_cases_incidents(
+            account_takeover_cases_filter = f"lastModifiedTime gte {start_timestamp}"
+            account_takeover_cases_response = client.get_paginated_cases_list(
+                filter_=account_takeover_cases_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            account_takeover_cases_incidents = generate_account_takeover_cases_incidents(
                 client, account_takeover_cases_response.get('cases', []))
 
+        all_incidents = threat_incidents + abuse_campaign_incidents + account_takeover_cases_incidents
     except Exception as e:
         logging.error(f"Failed fetching incidents: {e}")
         raise FetchIncidentsError(f"Error while fetching incidents: {e}")
 
     next_run = {
-        "last_fetch": current_iso_format_time
+        "last_fetch": current_datetime.strftime(ISO_8601_FORMAT)
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
user: login: sumit-badsara
body: If this is not an issue with demisto, there is no specific requirement of millisecond precision with last_fetch.
I wanted to keep the timestamp consistent across the script. 
TIME_FORMAT_WITHMS is only used for parsing the API response data - There could be inconsistency with responses hence required.
Otherwise for the known cases, I have used ISO_8601_FORMAT everywhere else.
created_at: 2025-03-21T19:46:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -805,45 +930,63 @@ def fetch_incidents(
     - client (Client): Client object to interact with the API.
     - last_run (Dict[str, Any]): Dictionary containing details about the last time incidents were fetched.
     - first_fetch_time (str): ISO formatted string indicating the first time from which to start fetching incidents.
+    - max_page_number (int): Maximum number of pages to fetch for incidents.
     - max_incidents_to_fetch (int, optional): Maximum number of incidents to fetch. Defaults to FETCH_LIMIT.
+    - polling_lag (int, optional): Time in minutes to subtract from polling time window for data consistency. Defaults to 0.
 
     Returns:
     - Tuple[Dict[str, str], List[Dict]]: Tuple containing a dictionary with the `last_fetch` time and a list of fetched incidents.
     """
-
     try:
         last_fetch = last_run.get("last_fetch", first_fetch_time)
-        last_fetch_datetime = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)
-        last_fetch = last_fetch_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
+        last_fetch = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)
+
+        current_datetime = get_current_datetime()
+
+        start_time = last_fetch
+        end_time = datetime.now(timezone.utc)
+
+        if polling_lag is not None:
+            start_time = start_time - polling_lag
+            end_time = end_time - polling_lag
+
+        start_timestamp = start_time.strftime(ISO_8601_FORMAT)
+        end_timestamp = end_time.strftime(ISO_8601_FORMAT)
 
-        current_datetime = datetime.utcnow().astimezone(timezone.utc)
-        current_iso_format_time = current_datetime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
         all_incidents = []
+        current_pending_incidents_to_fetch = max_incidents_to_fetch
+        threat_incidents, abuse_campaign_incidents, account_takeover_cases_incidents = [], [], []
 
         if fetch_threats:
-            threats_filter = f"receivedTime gte {last_fetch}"
-            threats_response = client.get_a_list_of_threats_request(filter_=threats_filter, page_size=100)
-            all_incidents += generate_threat_incidents(client, threats_response.get('threats', []))
+            threats_filter = f"latestTimeRemediated gte {start_timestamp} and latestTimeRemediated lte {end_timestamp}"
+            threats_response = client.get_paginated_threats_list(
+                filter_=threats_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            threat_incidents = generate_threat_incidents(
+                client, threats_response.get('threats', []), max_page_number, start_time, end_time
+            )
+        current_pending_incidents_to_fetch -= len(threat_incidents)
 
         if fetch_abuse_campaigns:
-            abuse_campaigns_filter = f"lastReportedTime gte {last_fetch}"
-            abuse_campaigns_response = client.get_a_list_of_campaigns_submitted_to_abuse_mailbox_request(
-                filter_=abuse_campaigns_filter, page_size=100)
-            all_incidents += generate_abuse_campaign_incidents(client, abuse_campaigns_response.get('campaigns', []))
+            abuse_campaigns_filter = f"lastReportedTime gte {start_timestamp}"
+            abuse_campaigns_response = client.get_paginated_abusecampaigns_list(
+                filter_=abuse_campaigns_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            abuse_campaign_incidents = generate_abuse_campaign_incidents(client, abuse_campaigns_response.get('campaigns', []))
+        current_pending_incidents_to_fetch -= len(abuse_campaign_incidents)
 
         if fetch_account_takeover_cases:
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
user: login: sumit-badsara
body: Have added this already as part of method being called as base condition.
Adding it here would make sense for readability, but i just wanted to make sure if we use that function elsewere, it doesn't crash the method.

But let's add it here as well might help with dry run and readability. The other check can act as a backup.
created_at: 2025-03-21T19:49:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -752,14 +845,34 @@ def get_a_list_of_unanalyzed_abuse_mailbox_campaigns_command(client, args):
     return command_results
 
 
-def generate_threat_incidents(client, threats):
+def generate_threat_incidents(client, threats, max_page_number, start_datetime, end_datetime):
     incidents = []
     for threat in threats:
-        threat_details = client.get_details_of_a_threat_request(threat["threatId"])
+        page_number = 1
+        all_filtered_messages = []
+        while page_number is not None:
+            threat_details = client.get_details_of_a_threat_request(threat["threatId"], page_number=page_number)
+            for message in threat_details["messages"]:
+                remediation_datetime = try_str_to_datetime(message.get("remediationTimestamp"))
+                if remediation_datetime and start_datetime <= remediation_datetime <= end_datetime:
+                    all_filtered_messages.append(message)
+                if remediation_datetime and remediation_datetime < start_datetime:
+                    break
+            page_number = threat_details.get('nextPageNumber', None)
+            if page_number is not None and page_number > max_page_number:
+                break
+
+        threat_details["messages"] = all_filtered_messages
+        if all_filtered_messages:
+            received_time = all_filtered_messages[0].get("receivedTime")
+        elif threat_details.get("messages", []):
+            received_time = threat_details.get("messages", [])[0].get("receivedTime")
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
user: login: sumit-badsara
body: Oh thanks, I do see the mistake here, let me fix this. The second if condition is as good as nothing in this case ^
created_at: 2025-03-21T19:52:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,6 +57,18 @@ configuration:
   name: incidentFetchInterval
   required: false
   type: 19
+- display: Polling Lag Time (in minutes)
+  name: polling_lag
+  defaultvalue: "0"
+  type: 0
+  required: false
+  additionalinfo: Time in minutes to subtract from polling time window for data consistency
+- display: Maximum number of pages to fetch for incidents
+  name: max_page_number
+  defaultvalue: "8"
+  type: 0
+  required: false
+  additionalinfo: Maximum number of pages to fetch for incidents
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.yml
user: login: sumit-badsara
body: done
created_at: 2025-03-21T21:09:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -394,8 +485,10 @@ def get_a_list_of_threats_command(client, args):
 def get_details_of_a_threat_command(client, args):
     threat_id = str(args.get('threat_id', ''))
     subtenant = args.get('subtenant', None)
+    page_size = args.get('page_size', None)
+    page_number = args.get('page_number', None)
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
user: login: sumit-badsara
body: done
created_at: 2025-03-21T21:09:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -805,45 +930,63 @@ def fetch_incidents(
     - client (Client): Client object to interact with the API.
     - last_run (Dict[str, Any]): Dictionary containing details about the last time incidents were fetched.
     - first_fetch_time (str): ISO formatted string indicating the first time from which to start fetching incidents.
+    - max_page_number (int): Maximum number of pages to fetch for incidents.
     - max_incidents_to_fetch (int, optional): Maximum number of incidents to fetch. Defaults to FETCH_LIMIT.
+    - polling_lag (int, optional): Time in minutes to subtract from polling time window for data consistency. Defaults to 0.
 
     Returns:
     - Tuple[Dict[str, str], List[Dict]]: Tuple containing a dictionary with the `last_fetch` time and a list of fetched incidents.
     """
-
     try:
         last_fetch = last_run.get("last_fetch", first_fetch_time)
-        last_fetch_datetime = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)
-        last_fetch = last_fetch_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
+        last_fetch = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)
+
+        current_datetime = get_current_datetime()
+
+        start_time = last_fetch
+        end_time = datetime.now(timezone.utc)
+
+        if polling_lag is not None:
+            start_time = start_time - polling_lag
+            end_time = end_time - polling_lag
+
+        start_timestamp = start_time.strftime(ISO_8601_FORMAT)
+        end_timestamp = end_time.strftime(ISO_8601_FORMAT)
 
-        current_datetime = datetime.utcnow().astimezone(timezone.utc)
-        current_iso_format_time = current_datetime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
         all_incidents = []
+        current_pending_incidents_to_fetch = max_incidents_to_fetch
+        threat_incidents, abuse_campaign_incidents, account_takeover_cases_incidents = [], [], []
 
         if fetch_threats:
-            threats_filter = f"receivedTime gte {last_fetch}"
-            threats_response = client.get_a_list_of_threats_request(filter_=threats_filter, page_size=100)
-            all_incidents += generate_threat_incidents(client, threats_response.get('threats', []))
+            threats_filter = f"latestTimeRemediated gte {start_timestamp} and latestTimeRemediated lte {end_timestamp}"
+            threats_response = client.get_paginated_threats_list(
+                filter_=threats_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            threat_incidents = generate_threat_incidents(
+                client, threats_response.get('threats', []), max_page_number, start_time, end_time
+            )
+        current_pending_incidents_to_fetch -= len(threat_incidents)
 
         if fetch_abuse_campaigns:
-            abuse_campaigns_filter = f"lastReportedTime gte {last_fetch}"
-            abuse_campaigns_response = client.get_a_list_of_campaigns_submitted_to_abuse_mailbox_request(
-                filter_=abuse_campaigns_filter, page_size=100)
-            all_incidents += generate_abuse_campaign_incidents(client, abuse_campaigns_response.get('campaigns', []))
+            abuse_campaigns_filter = f"lastReportedTime gte {start_timestamp}"
+            abuse_campaigns_response = client.get_paginated_abusecampaigns_list(
+                filter_=abuse_campaigns_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            abuse_campaign_incidents = generate_abuse_campaign_incidents(client, abuse_campaigns_response.get('campaigns', []))
+        current_pending_incidents_to_fetch -= len(abuse_campaign_incidents)
 
         if fetch_account_takeover_cases:
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
user: login: sumit-badsara
body: done
created_at: 2025-03-21T21:09:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -805,45 +930,63 @@ def fetch_incidents(
     - client (Client): Client object to interact with the API.
     - last_run (Dict[str, Any]): Dictionary containing details about the last time incidents were fetched.
     - first_fetch_time (str): ISO formatted string indicating the first time from which to start fetching incidents.
+    - max_page_number (int): Maximum number of pages to fetch for incidents.
     - max_incidents_to_fetch (int, optional): Maximum number of incidents to fetch. Defaults to FETCH_LIMIT.
+    - polling_lag (int, optional): Time in minutes to subtract from polling time window for data consistency. Defaults to 0.
 
     Returns:
     - Tuple[Dict[str, str], List[Dict]]: Tuple containing a dictionary with the `last_fetch` time and a list of fetched incidents.
     """
-
     try:
         last_fetch = last_run.get("last_fetch", first_fetch_time)
-        last_fetch_datetime = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)
-        last_fetch = last_fetch_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
+        last_fetch = datetime.fromisoformat(last_fetch[:-1]).astimezone(timezone.utc)
+
+        current_datetime = get_current_datetime()
+
+        start_time = last_fetch
+        end_time = datetime.now(timezone.utc)
+
+        if polling_lag is not None:
+            start_time = start_time - polling_lag
+            end_time = end_time - polling_lag
+
+        start_timestamp = start_time.strftime(ISO_8601_FORMAT)
+        end_timestamp = end_time.strftime(ISO_8601_FORMAT)
 
-        current_datetime = datetime.utcnow().astimezone(timezone.utc)
-        current_iso_format_time = current_datetime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
         all_incidents = []
+        current_pending_incidents_to_fetch = max_incidents_to_fetch
+        threat_incidents, abuse_campaign_incidents, account_takeover_cases_incidents = [], [], []
 
         if fetch_threats:
-            threats_filter = f"receivedTime gte {last_fetch}"
-            threats_response = client.get_a_list_of_threats_request(filter_=threats_filter, page_size=100)
-            all_incidents += generate_threat_incidents(client, threats_response.get('threats', []))
+            threats_filter = f"latestTimeRemediated gte {start_timestamp} and latestTimeRemediated lte {end_timestamp}"
+            threats_response = client.get_paginated_threats_list(
+                filter_=threats_filter, max_incidents_to_fetch=current_pending_incidents_to_fetch)
+            threat_incidents = generate_threat_incidents(
+                client, threats_response.get('threats', []), max_page_number, start_time, end_time
+            )
+        current_pending_incidents_to_fetch -= len(threat_incidents)
 
         if fetch_abuse_campaigns:
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
user: login: sumit-badsara
body: done
created_at: 2025-03-21T21:09:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -752,14 +845,34 @@ def get_a_list_of_unanalyzed_abuse_mailbox_campaigns_command(client, args):
     return command_results
 
 
-def generate_threat_incidents(client, threats):
+def generate_threat_incidents(client, threats, max_page_number, start_datetime, end_datetime):
     incidents = []
     for threat in threats:
-        threat_details = client.get_details_of_a_threat_request(threat["threatId"])
+        page_number = 1
+        all_filtered_messages = []
+        while page_number is not None:
+            threat_details = client.get_details_of_a_threat_request(threat["threatId"], page_number=page_number)
+            for message in threat_details["messages"]:
+                remediation_datetime = try_str_to_datetime(message.get("remediationTimestamp"))
+                if remediation_datetime and start_datetime <= remediation_datetime <= end_datetime:
+                    all_filtered_messages.append(message)
+                if remediation_datetime and remediation_datetime < start_datetime:
+                    break
+            page_number = threat_details.get('nextPageNumber', None)
+            if page_number is not None and page_number > max_page_number:
+                break
+
+        threat_details["messages"] = all_filtered_messages
+        if all_filtered_messages:
+            received_time = all_filtered_messages[0].get("receivedTime")
+        elif threat_details.get("messages", []):
+            received_time = threat_details.get("messages", [])[0].get("receivedTime")
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
user: login: sumit-badsara
body: done
created_at: 2025-03-21T21:09:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -752,14 +846,33 @@ def get_a_list_of_unanalyzed_abuse_mailbox_campaigns_command(client, args):
     return command_results
 
 
-def generate_threat_incidents(client, threats):
+def generate_threat_incidents(client, threats, max_page_number, start_datetime, end_datetime):
     incidents = []
     for threat in threats:
-        threat_details = client.get_details_of_a_threat_request(threat["threatId"])
+        page_number = 1
+        all_messages, all_filtered_messages = [], []
+        while page_number is not None:
+            threat_details = client.get_details_of_a_threat_request(threat["threatId"], page_number=page_number)
+            for message in threat_details["messages"]:
+                all_messages.append(message)
+                remediation_datetime = try_str_to_datetime(message.get("remediationTimestamp"))
+                if remediation_datetime and start_datetime <= remediation_datetime <= end_datetime:
+                    all_filtered_messages.append(message)
+                if remediation_datetime and remediation_datetime < start_datetime:
+                    break
+            page_number = threat_details.get('nextPageNumber', None)
+            if page_number is not None and page_number > max_page_number:
+                break
+
+        received_time = ""
+        threat_details["messages"] = all_messages or all_filtered_messages
+        if threat_details["messages"]:
+            received_time = threat_details["messages"][0].get("receivedTime")
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
user: login: inbalapt1
body: 
```suggestion
        if threat_details.get("messages"):
            received_time = threat_details.get("messages")[0].get("receivedTime")
```
created_at: 2025-03-23T08:56:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,6 +136,10 @@ script:
       name: mock-data
     - description: Subtenant of the user (if applicable).
       name: subtenant
+    - description: Number of threats that on in each page. Each page of data will have at most page_size threats. Has no effect if filter is not specified.
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.yml
user: login: inbalapt1
body: 
```suggestion
    - description: Number of threats per page. Each page will contain up to page_size threats. This has no effect if no filter is specified.
```
created_at: 2025-03-23T09:01:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -771,9 +884,14 @@ def generate_abuse_campaign_incidents(client, campaigns):
     incidents = []
     for campaign in campaigns:
         campaign_details = client.get_details_of_an_abuse_mailbox_campaign_request(campaign["campaignId"])
-        incident = {"dbotMirrorId": str(campaign["campaignId"]), "name": "Abuse Campaign",
-                    "occurred": campaign_details["firstReported"], 'details': "Abuse Campaign",
-                    "rawJSON": json.dumps(campaign_details) if campaign_details else {}}
+        first_reported = campaign_details["firstReported"]
+        incident = {
+            "dbotMirrorId": str(campaign["campaignId"]),
+            "name": "Abuse Campaign",
+            "occurred": first_reported[:26] if len(first_reported) > 26 else first_reported,
+            'details': "Abuse Campaign",
+            "rawJSON": json.dumps(campaign_details) if campaign_details else {}
+        }
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
user: login: inbalapt1
body: 
```suggestion
        first_reported = campaign_details.get("firstReported", "")
        incident = {
            "dbotMirrorId": str(campaign.get("campaignId")),
            "name": "Abuse Campaign",
            "occurred": first_reported[:26] if len(first_reported) > 26 else first_reported,
            'details': "Abuse Campaign",
            "rawJSON": json.dumps(campaign_details) if campaign_details else {}
        }
```
created_at: 2025-03-23T09:38:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -752,14 +846,33 @@ def get_a_list_of_unanalyzed_abuse_mailbox_campaigns_command(client, args):
     return command_results
 
 
-def generate_threat_incidents(client, threats):
+def generate_threat_incidents(client, threats, max_page_number, start_datetime, end_datetime):
     incidents = []
     for threat in threats:
-        threat_details = client.get_details_of_a_threat_request(threat["threatId"])
+        page_number = 1
+        all_messages, all_filtered_messages = [], []
+        while page_number is not None:
+            threat_details = client.get_details_of_a_threat_request(threat["threatId"], page_number=page_number)
+            for message in threat_details["messages"]:
+                all_messages.append(message)
+                remediation_datetime = try_str_to_datetime(message.get("remediationTimestamp"))
+                if remediation_datetime and start_datetime <= remediation_datetime <= end_datetime:
+                    all_filtered_messages.append(message)
+                if remediation_datetime and remediation_datetime < start_datetime:
+                    break
+            page_number = threat_details.get('nextPageNumber', None)
+            if page_number is not None and page_number > max_page_number:
+                break
+
+        received_time = ""
+        threat_details["messages"] = all_messages or all_filtered_messages
+        if threat_details["messages"]:
+            received_time = threat_details["messages"][0].get("receivedTime")
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
user: login: sumit-badsara
body: done
created_at: 2025-03-24T10:26:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,6 +136,10 @@ script:
       name: mock-data
     - description: Subtenant of the user (if applicable).
       name: subtenant
+    - description: Number of threats that on in each page. Each page of data will have at most page_size threats. Has no effect if filter is not specified.
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.yml
user: login: sumit-badsara
body: done
created_at: 2025-03-24T10:26:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -771,9 +884,14 @@ def generate_abuse_campaign_incidents(client, campaigns):
     incidents = []
     for campaign in campaigns:
         campaign_details = client.get_details_of_an_abuse_mailbox_campaign_request(campaign["campaignId"])
-        incident = {"dbotMirrorId": str(campaign["campaignId"]), "name": "Abuse Campaign",
-                    "occurred": campaign_details["firstReported"], 'details': "Abuse Campaign",
-                    "rawJSON": json.dumps(campaign_details) if campaign_details else {}}
+        first_reported = campaign_details["firstReported"]
+        incident = {
+            "dbotMirrorId": str(campaign["campaignId"]),
+            "name": "Abuse Campaign",
+            "occurred": first_reported[:26] if len(first_reported) > 26 else first_reported,
+            'details': "Abuse Campaign",
+            "rawJSON": json.dumps(campaign_details) if campaign_details else {}
+        }
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.py
user: login: sumit-badsara
body: done
created_at: 2025-03-24T10:26:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,41 +10,50 @@ configuration:
   display: Server URL (e.g. https://api.abnormalplatform.com/v1)
   name: url
   required: true
+  section: Connect
   type: 0
 - display: API Key
   name: api_key
   required: true
+  section: Connect
   type: 4
 - display: Trust any certificate (not secure)
   name: insecure
+  section: Connect
   type: 8
   required: false
 - display: Use system proxy settings
   name: proxy
+  section: Connect
   type: 8
   required: false
 - display: Fetch incidents
   name: isFetch
   required: false
+  section: Collect
   type: 8
 - additionalinfo: Maximum number of incidents per fetch. The default value is 200.
   defaultvalue: '200'
   display: Maximum incidents to fetch.
   name: max_fetch
+  section: Collect
   type: 0
   required: false
 - display: Fetch Threats
   name: fetch_threats
   type: 8
+  section: Collect
   required: false
 - display: Fetch Abuse Campaigns
   name: fetch_abuse_campaigns
   type: 8
+  section: Collect
   required: false
 - display: Fetch Account Takeover Cases
   name: fetch_account_takeover_cases
   type: 8
   required: false
+  section: Collect
 - defaultvalue: 3 days
   display: First fetch time
   additionalinfo: First alert created date to fetch. e.g., "1 min ago","2 weeks ago","3 months ago"
path: Packs/AbnormalSecurity/Integrations/AbnormalSecurity/AbnormalSecurity.yml
user: login: inbalapt1
body: 
```suggestion
- defaultvalue: 3 days
  display: First fetch time
  additionalinfo: First alert created date to fetch. e.g., "1 min ago","2 weeks ago","3 months ago"
  section: Collect
```
created_at: 2025-03-25T09:58:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,34 @@
+
+#### Incident Fields
+
+##### New: Abnormal Security Case GenAI Summary
+
+- New: Added a new incident field- Abnormal Security Case GenAI Summary.
+
+
+#### Integrations
+
+##### Abnormal Security
+- Updated the Docker image to: *demisto/python3:3.12.8.1983910*.
+
+- Added support for **Maximum incidents pages to fetch** parameter that maximum number of pages to fetch for incidents.
+- Added support for **Polling Lag Time (in minutes)** parameter that time in minutes to subtract from polling time window for data consistency.
+- Added support for *page_number* argument in the **abnormal-security-get-threat** command.
+- Added support for *page_size* argument in the **abnormal-security-get-threat** command.
path: Packs/AbnormalSecurity/ReleaseNotes/2_3_0.md
user: login: inbalapt1
body: 
```suggestion
- Added support for the following parameters:
    - **Maximum incidents pages to fetch**
    - **Polling Lag Time (in minutes)**
- Added support for the following arguments in **abnormal-security-get-threat** command:
    - *page_number*
    - *page_size*
```
created_at: 2025-03-25T10:18:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,36 @@
+
+#### Incident Fields
+
+##### New: Abnormal Security Case GenAI Summary
+
+- New: Added a new incident field- Abnormal Security Case GenAI Summary.
+
+
+#### Integrations
+
+##### Abnormal Security
+- Updated the Docker image to: *demisto/python3:3.12.8.1983910*.
+
+- Newly added functionalities
+  - Added support for the following parameters:
+    - **Maximum incidents pages to fetch**
+      - Allows user to limit the maximum incidents to fetch in single call to fetch_incidents
+    - **Polling Lag Time (in minutes)**
+      - Allows user to add a custom lag while polling incidents
+  - Added support for the following arguments in **abnormal-security-get-threat** command:
+    - *page_number*
+    - *page_size*
+- Bug Fixes
+  - Fixed timestamp parsing to handle high-precision timestamps (nanoseconds) in incident data, ensuring proper handling of remediation and event timestamps.
+- Performance Optimization(s)
+  - Fixed pagination issue in fetch-incidents command to properly handle fetching of incidents when max_fetch is less than 100, optimizing API calls for better performance
+  - Enhanced pagination support in fetch-incidents to reliably fetch more than 100 incidents across multiple pages, enabling comprehensive data retrieval for large-scale incident analysis.
+  - Fixed pagination issue in get-threats command to properly handle fetching of multiple messages part of the threat by paginating and fetching all pages, while filtering the messages by user provided time window.
+
+  
+#### Mappers
+
+##### Abnormal Security - Incoming Mapper
+
+- Updated the Abnormal Security - Incoming Mapper mapper to include genai_summary field in case details.
+
path: Packs/AbnormalSecurity/ReleaseNotes/2_3_0.md
user: login: inbalapt1
body: 
```suggestion

#### Incident Fields

##### New: Abnormal Security Case GenAI Summary

- New: Added the *Abnormal Security Case GenAI Summary* incident field.


#### Integrations

##### Abnormal Security
- Updated the Docker image to: *demisto/python3:3.12.8.1983910*.

- Added support for the following parameters:
    - **Maximum incidents pages to fetch** that limits the number of incidents fetched in a single fetch_incidents call.
    - **Polling Lag Time (in minutes)** that adds a custom lag while polling incidents.
- Added support for the following arguments in **abnormal-security-get-threat** command, to enable pagination:
    - *page_number*
    - *page_size*
- Fixed pagination issue in fetch-incidents to handle max_fetch below 100 properly.
- Enhanced pagination support in fetch-incidents to reliably fetch more than 100 incidents across multiple pages, enabling comprehensive data retrieval for large-scale incident analysis.

  
#### Mappers

##### Abnormal Security - Incoming Mapper

- Updated the Abnormal Security - Incoming Mapper to include genai_summary field in case details.

```
created_at: 2025-04-03T09:59:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,36 @@
+
+#### Incident Fields
+
+##### New: Abnormal Security Case GenAI Summary
+
+- New: Added a new incident field- Abnormal Security Case GenAI Summary.
+
+
+#### Integrations
+
+##### Abnormal Security
+- Updated the Docker image to: *demisto/python3:3.12.8.1983910*.
+
+- Newly added functionalities
+  - Added support for the following parameters:
+    - **Maximum incidents pages to fetch**
+      - Allows user to limit the maximum incidents to fetch in single call to fetch_incidents
+    - **Polling Lag Time (in minutes)**
+      - Allows user to add a custom lag while polling incidents
+  - Added support for the following arguments in **abnormal-security-get-threat** command:
+    - *page_number*
+    - *page_size*
+- Bug Fixes
+  - Fixed timestamp parsing to handle high-precision timestamps (nanoseconds) in incident data, ensuring proper handling of remediation and event timestamps.
+- Performance Optimization(s)
+  - Fixed pagination issue in fetch-incidents command to properly handle fetching of incidents when max_fetch is less than 100, optimizing API calls for better performance
+  - Enhanced pagination support in fetch-incidents to reliably fetch more than 100 incidents across multiple pages, enabling comprehensive data retrieval for large-scale incident analysis.
+  - Fixed pagination issue in get-threats command to properly handle fetching of multiple messages part of the threat by paginating and fetching all pages, while filtering the messages by user provided time window.
+
+  
+#### Mappers
+
+##### Abnormal Security - Incoming Mapper
+
+- Updated the Abnormal Security - Incoming Mapper mapper to include genai_summary field in case details.
+
path: Packs/AbnormalSecurity/ReleaseNotes/2_3_0.md
user: login: sumit-badsara
body: done
created_at: 2025-04-03T10:08:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: content-bot
created_at: 2025-03-18T18:34:22Z
body: Thank you for your contribution. Your generosity and caring are unrivaled! Make sure to register your contribution by filling the [Contribution Registration](https://forms.gle/XDfxU4E61ZwEESSMA) form, so our content wizard @inbalapt1 will know the proposed changes are ready to be reviewed.
For your convenience, here is a [link](https://xsoar.pan.dev/docs/contributing/sla) to the contributions SLAs document.
author_association: CONTRIBUTOR
user: login: content-bot
created_at: 2025-03-18T18:34:23Z
body: Hi @sumit-badsara, thanks for contributing to the XSOAR marketplace. To receive credit for your generous contribution please follow this [link](https://xsoar.pan.dev/docs/packs/packs-format#contributorsjson).
author_association: CONTRIBUTOR
user: login: sumit-badsara
created_at: 2025-03-21T21:34:04Z
body: @inbalapt1 Have done the proposed changes, Also can I get invite in slack community, I am unable to do so with the form available online. 
author_association: CONTRIBUTOR
user: login: sumit-badsara
created_at: 2025-03-24T10:04:23Z
body: @inbalapt1 
Full Name : Sumit Badsara
Email : sumitbadsara.dev@gmail.com
author_association: CONTRIBUTOR
user: login: inbalapt1
created_at: 2025-03-24T12:56:39Z
body: Hi @sumit-badsara ,
You should have received an invitation to DFIR in your email. Please accept it!
author_association: CONTRIBUTOR
user: login: sumit-badsara
created_at: 2025-03-24T18:32:45Z
body: Thanks @inbalapt1, joined the slack community. 
Also have fixed the pre-commit pipeline + non-pack modification issue. 
author_association: CONTRIBUTOR
user: login: inbalapt1
created_at: 2025-03-25T10:23:34Z
body: Hi @sumit-badsara  ,
Great work!
We're ready for a demo. Please check [this page](https://xsoar.pan.dev/docs/contributing/demo-prep), and let me know when you're available for one over DFIR.
Alternatively, feel free to send me (to this email - [iapt@paloaltonetworks.com](mailto:iapt@paloaltonetworks.com) or over DFIR - inbal apt) or add to the PR a recording of the demo.
author_association: CONTRIBUTOR
user: login: content-bot
created_at: 2025-04-03T10:03:08Z
body: For the Reviewer: Trigger build request has been accepted for this contribution PR.
author_association: CONTRIBUTOR
user: login: content-bot
created_at: 2025-04-03T10:03:13Z
body: For the Reviewer: Successfully created a pipeline in GitLab with url: https://gitlab.xdr.pan.local/xdr/cortex-content/content/-/pipelines/3046589
author_association: CONTRIBUTOR
user: login: content-bot
created_at: 2025-04-03T11:02:38Z
body: Validate summary
The following errors were thrown as a part of this pr: RM102, RN106.
The following errors can be ignored: RM102.
The following errors cannot be ignored: RN106.
The following errors don't run as part of the nightly flow and therefore can be force merged: RN106.

nVerdict: PR can be force merged from validate perspective? ❌
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-03T11:58:32Z
body: Thank you for your contribution. Your external PR has been merged and the changes are now included in an internal PR for further review. The internal PR will be merged to the master branch within 3 business days.
author_association: NONE