SumoLogicSEC

Freeing the analyst with autonomous decisions.

Analytics & SIEM · Sumo Logic Cloud SIEM

Details

IDSumoLogicSEC
ProviderFrancisco Partners
CategoryAnalytics & SIEM
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Freeing the analyst with autonomous decisions.
This integration was integrated and tested with version 6.1.0 of SumoLogicSEC.

Prerequisites

Only use this integration if your Cloud SIEM portal url ends with .sumologic.com - this can be verified via the url in your browser when logged into Cloud SIEM.

You’ll need an access key in order to complete the instance setup. Instructions on how to generate access keys can be found here.

Configure SumoLogicSEC in Cortex

Parameter Description Required
Sumo Logic API Endpoint https://api.<deployment>.sumologic.com/api True
Sumo Logic Instance Endpoint https://<yoursubdomain>.<deployment>.sumologic.com False
Fetch incidents   False
Incident type   False
Access ID   True
Access Key   True
Incidents Fetch Interval   False
Fetch Limit Fetch limit must not be greater than 200 False
Override default fetch query Default fetch query is status:in(“new”, “inprogress”) False
First fetch time   False
Pull associated Sumo Logic Signals as Incidents Pull Sumo Logic signals associated with detected Sumo Logic Insights as XSOAR incidents False
Incident Mirroring Direction Choose the direction to mirror the incident: Incoming (from Sumo Logic SIEM to Cortex XSOAR), Outgoing (from Cortex XSOAR to Sumo Logic SIEM), or Incoming and Outgoing (from/to Cortex XSOAR and Sumo Logic SIEM). Cortex XSOAR only parameter. False
Close Mirrored XSOAR Incident When selected, closing the Sumo Logic SIEM Insight is mirrored in Cortex XSOAR. Cortex XSOAR only parameter. False
Close Mirrored Sumo Logic Insight When selected, closing the Cortex XSOAR incident is mirrored in Sumo Logic SIEM. False
Override Record Summary Fields Record Summary Fields included when fetching Insights False

API documentation and query examples

For commands with query parameter input the available fields and operators are documented in API docs. These docs are useful when executing queries using the following commands:

  • sumologic-sec-insight-search
  • sumologic-sec-signal-search
  • sumologic-sec-entity-search

To access the API documentation, select the link for your deployment from here. Add sec to the end of the url to access Cloud SIEM API docs - e.g. https://api.us2.sumologic.com/docs/sec/.

Example: Insight search query ‘q’ parameter:

The search query string in our custom DSL that is used to filter the results.

Operators:

  • exampleField:"bar": The value of the field is equal to “bar”.
  • exampleField:in("bar", "baz", "qux"): The value of the field > is equal to either “bar”, “baz”, or “qux”.
  • exampleTextField:contains("foo bar"): The value of the field > contains the phrase “foo bar”.
  • exampleNumField:>5: The value of the field is greater than 5. There are similar <, <=, and >= operators.
  • exampleNumField:5..10: The value of the field is between 5 and 10 (inclusive).
  • exampleDateField:>2019-02-01T05:00:00+00:00: The value of the date field is after 5 a.m. UTC time on February 2, 2019.
  • exampleDateField:2019-02-01T05:00:00+00:00..2019-02-01T08:00:00+00:00: The value of the date field is between 5 a.m. and 8 a.m. UTC time on February 2, 2019.

Fields:

  • id
  • readableId
  • status
  • name
  • insightId
  • description
  • created
  • timestamp
  • closed
  • assignee
  • entity.ip
  • entity.hostname
  • entity.username
  • entity.type
  • enrichment
  • tag
  • severity
  • resolution
  • ruleId
  • records

Migrating from JASK content pack

The table below shows differences between this integration and the legacy JASK integration:

JASK (legacy) Sumo Logic Cloud SIEM Notes
jask-get-insight-details sumologic-sec-insight-get-details  
jask-get-insight-comments sumologic-sec-insight-get-comments  
jask-get-signal-details sumologic-sec-signal-get-details  
jask-get-entity-details sumologic-sec-entity-get-details  
jask-get-related-entities   Deprecated
jask-get-whitelisted-entities   Deprecated - use command sumologic-sec-entity-search with filter whitelisted:"true"
jask-search-insights sumologic-sec-insight-search  
jask-search-entities sumologic-sec-entity-search  
jask-search-signals sumologic-sec-signal-search  

New commands introduced in Sumo Logic Cloud SIEM pack

  • sumologic-sec-insight-set-status
  • sumologic-sec-match-list-get
  • sumologic-sec-match-list-update
  • sumologic-sec-threat-intel-search-indicators
  • sumologic-sec-threat-intel-get-sources
  • sumologic-sec-threat-intel-update-source

Commands

You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

sumologic-sec-insight-get-details


Get Insight details for a specific Insight ID.

Base Command

sumologic-sec-insight-get-details

Input

Argument Name Description Required
insight_id The insight to retrieve details for. Required
record_summary_fields Record Summary Fields to include in the output (override default fields). Optional

Context Output

Path Type Description
SumoLogicSec.Insight.Assignee string User or team assigned to the Insight
SumoLogicSec.Insight.Closed Date Closed date
SumoLogicSec.Insight.ClosedBy String Closed by user
SumoLogicSec.Insight.Created Date Created date
SumoLogicSec.Insight.Description String Description of the Insight
SumoLogicSec.Insight.Entity String Entity name associated with the Insight
SumoLogicSec.Insight.Id String The ID of the Insight
SumoLogicSec.Insight.LastUpdated Date The time the Insight was last updated
SumoLogicSec.Insight.LastUpdatedBy string The last user to update the Insight
SumoLogicSec.Insight.Name String The name of the Insight
SumoLogicSec.Insight.ReadableId String The ID of the Insight in readable form
SumoLogicSec.InsightList.RecordSummaryFields Array Record Summary Fields associated with the Insight
SumoLogicSec.Insight.Resolution String Resolution for closed Insight
SumoLogicSec.Insight.Severity String The severity of the Insight
SumoLogicSec.Insight.Signals.contentType String Type of content that triggered the Signal
SumoLogicSec.Insight.Signals.description String Description of the Signal
SumoLogicSec.Insight.Signals.id String The ID of the Signal
SumoLogicSec.Insight.Signals.name String The name of the Signal
SumoLogicSec.Insight.Signals.recordCount Number Number of records associated with the Signal
SumoLogicSec.Insight.Signals.ruleId String Rule ID associated with the Signal
SumoLogicSec.Insight.Signals.severity Number The severity of the Signal
SumoLogicSec.Insight.Signals.stage String The stage of the Signal
SumoLogicSec.Insight.Signals.timestamp Date Signal timestamp
SumoLogicSec.Insight.Source String The source of the Insight
SumoLogicSec.Insight.Status String The status of the Insight
SumoLogicSec.Insight.TimeToDetection Number Insight time to detection
SumoLogicSec.Insight.TimeToRemediation Number Insight time to remediation
SumoLogicSec.Insight.TimeToResponse Number Insight time to response
SumoLogicSec.Insight.Timestamp Date Insight timestamp

Command Example

!sumologic-sec-insight-get-details insight-id=INSIGHT-116

Human Readable Output

Insight Details:

Id Readable Id Name Action Status Assignee Description Last Updated Last Updated By Severity Closed Closed By Timestamp Entity Resolution
c6c97d84-983d-303e-a03b-86f53d657fc8 INSIGHT-116 Lateral Movement with Discovery and Credential Access   Closed   Initial Access, Lateral Movement, Discovery, Initial Access, Credential Access 2021-05-10T23:48:10.016204   HIGH 2021-05-10T23:48:09.961023 obfuscated@email.com 2021-02-18T22:04:08.330000 1.2.3.4 No Action

sumologic-sec-insight-get-comments


Get comments for a specific Insight ID. (Users can post and update comments on the Sumo Logic Cloud SIEM portal for any Insight ID.)

Base Command

sumologic-sec-insight-add-comment

Input

Argument Name Description Required
insight_id The insight ID for which to add a comment. Required
comment The comment to be added. Required

Context Output

Path Type Description
SumoLogicSec.InsightComments.Id String ID of comment
SumoLogicSec.InsightComments.Body String Comment contents
SumoLogicSec.InsightComments.Author String User that created the comment
SumoLogicSec.InsightComments.Timestamp Date Comment created timestamp
SumoLogicSec.InsightComments.InsightId String The ID of the Insight

Command Example

!sumologic-sec-insight-add-comment insight-id=INSIGHT-116 comment="This is an example comment"

Human Readable Output

Insight Comment:

Id Insight Id Author Body Last Updated Timestamp
2 INSIGHT-116 obfuscated@email.com This is an example comment   2021-04-23T00:38:43.977543

sumologic-sec-insight-get-comments


Get comments for a specific Insight ID. (Users can post and update comments on the Sumo Logic Cloud SIEM portal for any Insight ID.)

Base Command

sumologic-sec-insight-get-comments

Input

Argument Name Description Required
insight_id The insight ID for which to retrieve comments. Required

Context Output

Path Type Description
SumoLogicSec.InsightComments.Id String ID of comment
SumoLogicSec.InsightComments.Body String Comment contents
SumoLogicSec.InsightComments.Author String User that created the comment
SumoLogicSec.InsightComments.Timestamp Date Comment created timestamp
SumoLogicSec.InsightComments.InsightId String The ID of the Insight

Command Example

!sumologic-sec-insight-get-comments insight-id=INSIGHT-116

Human Readable Output

Insight Comments:

Id Insight Id Author Body Last Updated Timestamp
2 INSIGHT-116 obfuscated@email.com This is an example comment   2021-04-23T00:38:43.977543

sumologic-sec-signal-get-details


Get Signal details for a specific Signal ID. Signal details command references signals in Sumo Logic Cloud SIEM which are created when records exhibit suspicious properties and mate with patterns or other detection logic.

Base Command

sumologic-sec-signal-get-details

Input

Argument Name Description Required
signal_id The signal to retrieve details for. Required

Context Output

Path Type Description
SumoLogicSec.Signal.ContentType String Type of content that triggered the Signal
SumoLogicSec.Signal.Description String Description of the Signal
SumoLogicSec.Signal.Entity String Entity name associated with the Signal
SumoLogicSec.Signal.Id String The ID of the Signal
SumoLogicSec.Signal.Name String The name of the Signal
SumoLogicSec.Signal.RecordCount Number Number of records associated with the Signal
SumoLogicSec.Signal.RuleId String Rule ID associated with the Signal
SumoLogicSec.Signal.Severity Number The severity of the Signal
SumoLogicSec.Signal.Stage String The stage of the Signal
SumoLogicSec.Signal.Suppressed Boolean Whether or not the Signal was suppressed
SumoLogicSec.Signal.Timestamp Date Signal timestamp

Command Example

!sumologic-sec-signal-get-details signal-id=e0e7096b-2f91-5b72-b1a2-db48ce882dfc

Human Readable Output

Signal Details:

Id Name Rule Id Description Severity Content Type Timestamp Entity
e0e7096b-2f91-5b72-b1a2-db48ce882dfc Potential malicious JVM download LEGACY-S00062 A document was downloaded and opened followed by a file download using a Java user-agent. 4 RULE 2021-02-18T22:04:08.230000 1.2.3.4

sumologic-sec-entity-get-details


Get entity details for a specific entity ID

Base Command

sumologic-sec-entity-get-details

Input

Argument Name Description Required
entity-id The entity to retrieve details for. Required

Context Output

Path Type Description
SumoLogicSec.Entity.ActivityScore Number Entity Activity Score
SumoLogicSec.Entity.FirstSeen Date When the Entity was first seen
SumoLogicSec.Entity.Hostname String Entity hostname
SumoLogicSec.Entity.Id String Entity ID
SumoLogicSec.Entity.IsWhitelisted Boolean Whether or not the Entity is on allow list
SumoLogicSec.Entity.LastSeen Date When the Entity was last seen
SumoLogicSec.Entity.Name String The Entity name
SumoLogicSec.Entity.OperatingSystem String Entity Operating System (observed or from inventory)
SumoLogicSec.Entity.InventoryData Boolean Whether or not this Entity was ingested from inventory e.g. Active Directory

Command Example

!sumologic-sec-entity-get-details entity-id=_hostname-win10--admin.obfuscated

Human Readable Output

Entity Details

Id Name First Seen Last Seen Activity Score Is Whitelisted Operating System Inventory Data
_hostname-win10–admin.obfuscated win10-admin.obfuscated   2021-04-21T14:43:38.526000 9 false Windows 10 Enterprise true

sumologic-sec-insight-search


Search insights using available filters

Base Command

sumologic-sec-insight-search

Input

Argument Name Description Required
query Use a query string to search, see API documentation for more details. Optional
created When the insight was created. Defaults to ‘All time’ if no time arguments are specified. Possible values are: All time, Last week, Last 48 hours, Last 24 hours. Optional
status Comma separated list of values from the options: new,inprogress,closed. Optional
asignee User assigned to Insights. Optional
offset The number of items to skip before starting to collect the result set. Default is 0. Optional
limit The maximum number of items to return. Default is 10. Optional
record_summary_fields Record Summary Fields to include in the output (override default fields). Optional

Context Output

Path Type Description
SumoLogicSec.InsightList.Assignee String User or team assigned to the Insight
SumoLogicSec.InsightList.Closed Date Closed date
SumoLogicSec.InsightList.ClosedBy String Closed by user
SumoLogicSec.InsightList.Created Date Created date
SumoLogicSec.InsightList.Description String Description of the Insight
SumoLogicSec.InsightList.Entity String Entity name associated with the Insight
SumoLogicSec.InsightList.Id String The ID of the Insight
SumoLogicSec.InsightList.LastUpdated Date The time the Insight was last updated
SumoLogicSec.InsightList.LastUpdatedBy String The last user to update the Insight
SumoLogicSec.InsightList.Name String The name of the Insight
SumoLogicSec.InsightList.ReadableId String The ID of the Insight in readable form
SumoLogicSec.InsightList.RecordSummaryFields Array Record Summary Fields associated with the Insight
SumoLogicSec.InsightList.Resolution String Resolution for closed Insight
SumoLogicSec.InsightList.Severity String The severity of the Insight
SumoLogicSec.InsightList.Signals.contentType String Type of content that triggered the Signal
SumoLogicSec.InsightList.Signals.description String Description of the Signal
SumoLogicSec.InsightList.Signals.id String The ID of the Signal
SumoLogicSec.InsightList.Signals.name String The name of the Signal
SumoLogicSec.InsightList.Signals.recordCount Number Number of records associated with the Signal
SumoLogicSec.InsightList.Signals.ruleId String Rule ID associated with the Signal
SumoLogicSec.InsightList.Signals.severity Number The severity of the Signal
SumoLogicSec.InsightList.Signals.stage String The stage of the Signal
SumoLogicSec.InsightList.Signals.timestamp Date Signal timestamp
SumoLogicSec.InsightList.Source String The source of the Insight
SumoLogicSec.InsightList.Status String The status of the Insight
SumoLogicSec.InsightList.TimeToDetection Number Insight time to detection
SumoLogicSec.InsightList.TimeToRemediation Number Insight time to remediation
SumoLogicSec.InsightList.TimeToResponse Number Insight time to response
SumoLogicSec.InsightList.Timestamp Date Insight timestamp

Command Example

!sumologic-sec-insight-search query="timestamp:>\"2021-02-01T05:00:00+00:00\" status:\"closed\" AND severity:>\"MEDIUM\"" limit=3

Human Readable Output

Insights:

Id Readable Id Name Action Status Assignee Description Last Updated Last Updated By Severity Closed Closed By Timestamp Entity Resolution
00853cdd-763e-3e31-a2e4-f74277922f9f INSIGHT-220 Command and Control with Defense Evasion and Execution   Closed   Initial Access, Command and Control, Defense Evasion, Execution 2021-03-23T20:06:51.565599   HIGH 2021-03-23T20:06:51.511505 obfuscated@email.com 2021-02-22T16:27:51 testcomputer.somedomain.net No Action
eefdff8d-7447-3b47-83e0-66a0b210d618 INSIGHT-219 Discovery with Credential Access and Execution   Closed   Initial Access, Credential Access, Initial Access, Execution, Discovery, Credential Access 2021-03-23T21:21:55.029798   HIGH 2021-03-23T21:21:54.914061 obfuscated@email.com 2021-02-22T16:24:07.959000 1.2.3.4 No Action
8a77d12e-5905-3401-ae7c-2e17b1fd3060 INSIGHT-221 Privilege Escalation with Persistence and Execution   Closed obfuscated@email.com Execution, Privilege Escalation, Persistence, Execution 2021-05-12T21:47:08.297222   HIGH 2021-05-12T21:47:08.132251 obfuscated@email.com 2021-02-22T16:24:07.959000 5.6.7.8 No Action

sumologic-sec-signal-search


Search signals using available filters

Base Command

sumologic-sec-signal-search

Input

Argument Name Description Required
query Use a query string to search, see API documentation for more details. Optional
created When the Signal was created. Defaults to ‘All time’ if no time arguments are specified. Possible values are: All time, Last week, Last 48 hours, Last 24 hours. Default is All time. Optional
contentType Content type associated with the signals. Options: ANOMALY, DEFAULT, THREATINTEL, RULE. Possible values are: ANOMALY, DEFAULT, THREATINTEL, RULE. Optional
offset The number of items to skip before starting to collect the result set. Default is 0. Optional
limit The maximum number of items to return. Default is 10. Optional

Context Output

Path Type Description
SumoLogicSec.SignalList.ContentType String Type of content that triggered the Signal
SumoLogicSec.SignalList.Description String Description of the Signal
SumoLogicSec.SignalList.Entity String Entity name associated with the Signal
SumoLogicSec.SignalList.Id String The ID of the Signal
SumoLogicSec.SignalList.Name String The name of the Signal
SumoLogicSec.SignalList.RecordCount Number Number of records associated with the Signal
SumoLogicSec.SignalList.RuleId String Rule ID associated with the Signal
SumoLogicSec.SignalList.Severity Number The severity of the Signal
SumoLogicSec.SignalList.Stage String The stage of the Signal
SumoLogicSec.SignalList.Suppressed Boolean Whether or not the Signal was suppressed
SumoLogicSec.SignalList.Timestamp Date Signal timestamp

Command Example

!sumologic-sec-signal-search query="timestamp:NOW-7D.NOW name:contains(\"Internal\")"

Human Readable Output

Signals:

Id Name Entity Rule Id Description Severity Stage Timestamp Content Type Tags
b50fd570-341b-576d-85b5-8b5cd17c0aee IP Address Scan - Internal 1.2.3.4 LEGACY-S00050 A scan of IP addresses 3 Discovery 2021-04-22T04:08:13.514000 RULE _mitreAttackTactic:TA0007,
_mitreAttackTactic:TA0043,
_mitreAttackTechnique:T1046,
_mitreAttackTechnique:T1595

sumologic-sec-entity-search


Search entities using the available filters

Base Command

sumologic-sec-entity-search

Input

Argument Name Description Required
query Use a query string to search, see API documentation for more details. Optional
ip IP Address to search for e.g. 1.2.3.4. Optional
hostname Hostname to search for e.g. host.example.com. Optional
username Username to search for e.g. admin. Optional
type Entity type to search for. Options: username, hostname, ip, mac. Possible values are: username, hostname, ip, mac. Optional
whitelisted Is the Entity whitelisted? true/false. Possible values are: true, false. Optional
tag Tag contains value. Optional
offset The number of items to skip before starting to collect the result set. Default is 0. Optional
limit The maximum number of items to return. Default is 10. Optional

Context Output

Path Type Description
SumoLogicSec.EntityList.ActivityScore Number Entity Activity Score
SumoLogicSec.EntityList.FirstSeen Date When the Entity was first seen
SumoLogicSec.EntityList.Id String Entity ID
SumoLogicSec.EntityList.IpHostname String Hostname associated with IP Entity
SumoLogicSec.EntityList.IsWhitelisted Boolean Whether or not the Entity is on allow list
SumoLogicSec.EntityList.LastSeen Date When the Entity was last seen
SumoLogicSec.EntityList.Name String The Entity name
SumoLogicSec.EntityList.OperatingSystem String Entity Operating System (observed or from inventory)
SumoLogicSec.EntityList.InventoryData Boolean Whether or not this Entity was ingested from inventory e.g. Active Directory
SumoLogicSec.EntityList.Hostname String Entity hostname
SumoLogicSec.EntityList.Department String Username Entity department
SumoLogicSec.EntityList.EmployeeId String Username Entity employee ID

Command Example

!sumologic-sec-entity-search query="type:\"ip\" activityScore:>=3"

Human Readable Output

Entities:

Id Name First Seen Last Seen Activity Score Is Whitelisted Operating System Inventory Data
_ip-specops_analysis_lab-1.2.3.4 1.2.3.4   2021-04-22T04:08:13.514000 3 false   false

sumologic-sec-insight-set-status


Change status of Insight

Base Command

sumologic-sec-insight-set-status

Input

Argument Name Description Required
insight_id The insight to change status for. Required
status The desired Insight status. Possible values are: new, inprogress, closed. Default is in-progress. Optional
resolution Resolution for closing Insight. Valid values are: “Resolved”, “False Positive”, “No Action”, “Duplicate”. Possible values are: Resolved, False Positive, No Action, Duplicate. Default is Resolved. Optional
sub_resolution Custom sub resolution for closing Insight. If populated, it will override the resolution field. Please make sure the resolution matches exactly your Sumo Resolutions Optional

Context Output

Path Type Description
SumoLogicSec.Insight.Assignee String User or team assigned to the Insight
SumoLogicSec.Insight.Closed Date Closed date
SumoLogicSec.Insight.ClosedBy String Closed by user
SumoLogicSec.Insight.Created Date Created date
SumoLogicSec.Insight.Description String Description of the Insight
SumoLogicSec.Insight.Entity String Entity name associated with the Insight
SumoLogicSec.Insight.Id String The ID of the Insight
SumoLogicSec.Insight.LastUpdated Date The time the Insight was last updated
SumoLogicSec.Insight.LastUpdatedBy String The last user to update the Insight
SumoLogicSec.Insight.Name String The name of the Insight
SumoLogicSec.Insight.ReadableId String The ID of the Insight in readable form
SumoLogicSec.Insight.Resolution String Resolution for closed Insight
SumoLogicSec.Insight.Severity String The severity of the Insight
SumoLogicSec.Insight.Signals.contentType String Type of content that triggered the Signal
SumoLogicSec.Insight.Signals.description String Description of the Signal
SumoLogicSec.Insight.Signals.id String The ID of the Signal
SumoLogicSec.Insight.Signals.name String The name of the Signal
SumoLogicSec.Insight.Signals.recordCount Number Number of records associated with the Signal
SumoLogicSec.Insight.Signals.ruleId String Rule ID associated with the Signal
SumoLogicSec.Insight.Signals.severity Number The severity of the Signal
SumoLogicSec.Insight.Signals.stage String The stage of the Signal
SumoLogicSec.Insight.Signals.timestamp Date Signal timestamp
SumoLogicSec.Insight.Source String The source of the Insight
SumoLogicSec.Insight.Status String The status of the Insight
SumoLogicSec.Insight.TimeToDetection Number Insight time to detection
SumoLogicSec.Insight.TimeToRemediation Number Insight time to remediation
SumoLogicSec.Insight.TimeToResponse Number Insight time to response
SumoLogicSec.Insight.Timestamp Date Insight timestamp

Command Example

!sumologic-sec-insight-set-status insight-id=INSIGHT-116 status=closed resolution="No Action"

Human Readable Output

Insight Details:

Id Readable Id Name Action Status Assignee Description Last Updated Last Updated By Severity Closed Closed By Timestamp Entity Resolution
c6c97d84-983d-303e-a03b-86f53d657fc8 INSIGHT-116 Lateral Movement with Discovery and Credential Access   Closed   Initial Access, Lateral Movement, Discovery, Initial Access, Credential Access 2021-05-13T01:28:32.648352   HIGH 2021-05-13T01:28:32.580039 obfuscated@email.com 2021-02-18T22:04:08.330000 1.2.3.4 No Action

sumologic-sec-match-list-get


Get match lists

Base Command

sumologic-sec-match-list-get

Input

Argument Name Description Required
offset The number of items to skip before starting to collect the result set. Default is 0. Optional
limit Number of match lists returned. Default is 10. Optional
sort Sort expression. Default is name. Optional
sortDir Sort direction. Possible values are: ASC, DESC. Default is ASC. Optional

Context Output

Path Type Description
SumoLogicSec.MatchLists.Created String When the Match List was created
SumoLogicSec.MatchLists.CreatedBy String User that created the Match List
SumoLogicSec.MatchLists.DefaultTtl Number Default TTL for entries in the Match List
SumoLogicSec.MatchLists.Description String Description of the Match List
SumoLogicSec.MatchLists.Id String ID of the Match List
SumoLogicSec.MatchLists.LastUpdated String When the Match List was last updated
SumoLogicSec.MatchLists.LastUpdatedBy String The last user to update the Match List
SumoLogicSec.MatchLists.Name String Name of Match List
SumoLogicSec.MatchLists.TargetColumn String Match List Target Column

Command Example

!sumologic-sec-match-list-get limit=3

Human Readable Output

Match lists:

Id Name Target Column Default Ttl
173 admin_ips SrcIp 0
24 auth_servers Ip  
162 auth_servers_dst DstIp  

sumologic-sec-match-list-update


Add item to match list

Base Command

sumologic-sec-match-list-update

Input

Argument Name Description Required
match_list_id ID of match list. Required
active Item active or disabled. Required
description Description of match list item. Required
expiration Expiration of match list item, e.g. “2021-03-25T23:52:23.508Z”. Required
value Value of match list item. Required

Context Output

Path Type Description
SumoLogicSec.UpdateResult.Result String Result (Success or Failed)
SumoLogicSec.UpdateResult.Server response Boolean Server response (True or False)

Command Example

!sumologic-sec-match-list-update match-list-id=166 description="My description" expiration=2021-04-25T22:36:10.925Z value="10.20.30.40" active=true

Human Readable Output

Result:

Result Server Response
Success true

sumologic-sec-threat-intel-search-indicators


Search Threat Intel Indicators

Base Command

sumologic-sec-threat-intel-search-indicators

Input

Argument Name Description Required
q Use a query string to search, see API documentation for more details. Optional
value The value to search for. Required
offset The number of items to skip before starting to collect the result set. Default is 0. Optional
limit The numbers of items to return. Default is 10. Optional
sourceIds Comma separated list of threat intelligence source IDs to search, e.g. 1,2,3. Required

Context Output

Path Type Description
SumoLogicSec.ThreatIntelIndicators.Active Boolean Whether or not the Threat Intel Indicator is Active
SumoLogicSec.ThreatIntelIndicators.Expiration Date Date and time the Threat Intel Indicator is set to expire
SumoLogicSec.ThreatIntelIndicators.Id String ID of Threat Intel Indicator
SumoLogicSec.ThreatIntelIndicators.Meta.created.username String User that created the Threat Intel Indicator
SumoLogicSec.ThreatIntelIndicators.Meta.created.when Date When the Threat Intel Indicator was created
SumoLogicSec.ThreatIntelIndicators.Meta.description String Description of Threat Intel Indicator
SumoLogicSec.ThreatIntelIndicators.Meta.updated Date When the Threat Intel Indicator was last updated
SumoLogicSec.ThreatIntelIndicators.Value String Value of Threat Intel Indicator

Command Example

!sumologic-sec-threat-intel-search-indicators value=1.2.3.4 sourceIds=54

Human Readable Output

Threat Intel Indicators:

Id Value Active Expiration
f396ae69aa223c049ff639b3649ba1dd6465ec74397c3126916786bbcd6d76017468726561745f49705f44656d6973746f5f54657374 1.2.3.4 true 2021-04-29T00:00:00

sumologic-sec-threat-intel-get-sources


Get Threat Intel Sources

Base Command

sumologic-sec-threat-intel-get-sources

Input

Argument Name Description Required
offset The number of items to skip before starting to collect the result set. Default is 0. Optional
limit The numbers of items to return. Default is 10. Optional
sort Sort expression. Default is name. Optional
sortDir Sort direction. Possible values are: ASC, DESC. Default is ASC. Optional

Context Output

Path Type Description
SumoLogicSec.ThreatIntelSources.Created String When the Threat Intel Source was created
SumoLogicSec.ThreatIntelSources.CreatedBy String User that created the Threat Intel Source
SumoLogicSec.ThreatIntelSources.Description String Description of Threat Intel Source
SumoLogicSec.ThreatIntelSources.Id String ID of Threat Intel Source
SumoLogicSec.ThreatIntelSources.LastUpdated String When the Threat Intel Source was last updated
SumoLogicSec.ThreatIntelSources.LastUpdatedBy String User that last updated the Threat Intel Source
SumoLogicSec.ThreatIntelSources.Name String Name of Threat Intel Source
SumoLogicSec.ThreatIntelSources.SourceType String Source type of Threat Intel Source

Command Example

!sumologic-sec-threat-intel-get-sources limit=3

Human Readable Output

Threat intel sources:

Id Name Description Source Type
35 abuse.ch   CUSTOM
25 Alienvault OTX Alienvault TAXII
24 Anomali   TAXII

sumologic-sec-threat-intel-update-source


Add Threat Intel Indicator to Threat Intel Source

Base Command

sumologic-sec-threat-intel-update-source

Input

Argument Name Description Required
threat-intel-source-id ID of Threat Intel Source. Required
active Indicator active or disabled. Default is true. Required
description Description of indicator. Required
expiration Expiration of match list item, e.g. “2021-03-25T23:52:23.508Z”. Required
value Indicator value. Required

Context Output

Path Type Description
SumoLogicSec.UpdateResult.Result String Result (Success or Failed)
SumoLogicSec.UpdateResult.Server response Boolean Server response (True or False)

Command Example

!sumologic-sec-threat-intel-update-source threat-intel-source-id=54 active=true value=1.2.3.4 description=test expiration=2021-04-29T00:00:00.000Z

Human Readable Output

Result:

Result Server Response
Success true

Configuration parameters

  • api_endpoint — Sumo Logic API Endpoint (required)
  • instance_endpoint — Sumo Logic Instance Endpoint
  • isFetch — Fetch incidents
  • incidentType — Incident type
  • access_id — Access ID (required)
  • access_key — Access Key (required)
  • incidentFetchInterval — Incidents Fetch Interval
  • max_fetch — Fetch Limit
  • fetch_query — Override default fetch query
  • first_fetch — First fetch time
  • pull_signals — Pull associated Sumo Logic Signals as Incidents
  • mirror_direction — Incident Mirroring Direction
  • close_incident — Close Mirrored XSOAR Incident
  • close_insight — Close Mirrored Sumo Logic Insight
  • record_summary_fields — Override Record Summary Fields

Commands (14)

  • sumologic-sec-entity-get-details

    Get entity details for a specific entity ID.

  • sumologic-sec-entity-search

    Search entities using the available filters.

  • sumologic-sec-insight-add-comment

    Add a comment for a specific Insight ID (Users can post and update comments on the Sumo Logic Cloud SIEM portal for any Insight ID).

  • sumologic-sec-insight-get-comments

    Get comments for a specific Insight ID (Users can post and update comments on the Sumo Logic Cloud SIEM portal for any Insight ID).

  • sumologic-sec-insight-get-details

    Get Insight details for a specific Insight ID.

  • sumologic-sec-insight-search

    Search insights using available filters.

  • sumologic-sec-insight-set-status

    Change the status of an Insight.

  • sumologic-sec-match-list-get

    Get match lists.

  • sumologic-sec-match-list-update

    Add an item to a match list.

  • sumologic-sec-signal-get-details

    Get Signal details for a specific Signal ID. Signal details command references signals in Sumo Logic Cloud SIEM which are created when records exhibit suspicious properties and mate with patterns or other detection logic.

  • sumologic-sec-signal-search

    Search signals using available filters.

  • sumologic-sec-threat-intel-get-sources

    Get Threat Intel Sources.

  • sumologic-sec-threat-intel-search-indicators

    Search Threat Intel Indicators.

  • sumologic-sec-threat-intel-update-source

    Add a Threat Intel Indicator to an Threat Intel Source.

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401

"""Integration for Sumo Logic Cloud SIEM

"""
import traceback
from datetime import datetime
from typing import Any, cast

""" CONSTANTS """

MAX_INCIDENTS_TO_FETCH = 20
DEFAULT_HEADERS = {"Content-Type": "application/json"}
DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"  # ISO8601 format with UTC, default in XSOAR
DEFAULT_LOOKBACK_SECONDS = 300  # 5 minutes
# =========== Mirroring Mechanism Globals ===========
MIRROR_DIRECTION = {"None": None, "Incoming": "In", "Outgoing": "Out", "Incoming And Outgoing": "Both"}

OUTGOING_MIRRORED_FIELDS = ["comment", "status"]
XSOAR_SUMO_CLOSE_REASON_MAP = {
    "False Positive": "False Positive",
    "Duplicate": "Duplicate",
    "Resolved": "Resolved",
    "Other": "Resolved",
}

""" CLIENT CLASS """


class Client(BaseClient):
    """Client class to interact with the service API

    This Client implements API calls, and does not contain any XSOAR logic.
    Should only do requests and return data.
    """

    def req(self, method, url_suffix, params=None, json_data=None, headers=None):
        """
        Send the request to Sumo Logic and return the JSON response
        """
        if headers is None:
            headers = DEFAULT_HEADERS
        json_data = {} if json_data is None else json_data
        r = self._http_request(
            headers=headers, method=method, params=params, json_data=json_data, url_suffix=url_suffix, resp_type="json"
        ).get("data")
        return r

    def set_extra_params(self, args: dict[str, Any]) -> None:
        """
        Set any extra params (in the form of a dictionary) for this client
        """
        self.extra_params = args

    def get_extra_params(self) -> dict[str, Any]:
        """
        Set any extra params (in the form of a dictionary) for this client
        """
        return self.extra_params


""" HELPER FUNCTIONS """


def translate_severity(severity):
    """
    Translate from Sumo Logic CSE insight severity to Demisto severity
    """
    _severities = {"LOW": 1, "MEDIUM": 2, "HIGH": 3}
    return _severities.get(severity, 4)


def add_to_query(q):
    if len(q) > 0:
        return f"{q} "  # No need for 'AND' here
    else:
        return q


def arg_time_query_to_q(q, argval, timefield):
    """
    Convert created argument to querystring
    """
    if not argval or argval == "All time":
        return q
    if argval == "Last week":
        return add_to_query(q) + f"{timefield}:NOW-7D..NOW"
    if argval == "Last 48 hours":
        return add_to_query(q) + f"{timefield}:NOW-48h..NOW"
    if argval == "Last 24 hours":
        return add_to_query(q) + f"{timefield}:NOW-24h..NOW"
    return None


def add_list_to_q(q, fields, args):
    """
    Add arguments to querystring
    """
    for arg_field in fields:
        arg_value = args.get(arg_field, None)
        if arg_value:
            if "," in arg_value:
                quoted_values = [f'"{v}"' for v in arg_value.split(",")]
                q = add_to_query(q) + "{}:in({})".format(arg_field, ",".join(quoted_values))
            else:
                q = add_to_query(q) + f'{arg_field}:"{arg_value}"'
    return q


def insight_signal_to_readable(obj):
    """
    Construct a readable json output from an original insight/signal object
    """
    if obj is None:
        return {}

    # Capitalize fields
    cap_obj = {(k[0].capitalize() + k[1:]): v for k, v in obj.items()}

    # Only show Entity name (Insights and Signals)
    cap_obj["Entity"] = ""
    if obj.get("entity") and "name" in obj["entity"]:
        cap_obj["Entity"] = obj["entity"]["name"]

    # Only show status displayName (Insights only)
    if obj.get("status") and "displayName" in obj["status"]:
        cap_obj["Status"] = obj["status"]["displayName"]

    # For Assignee show username (email)
    cap_obj["Assignee"] = ""
    if obj.get("assignee") and "username" in obj["assignee"]:
        cap_obj["Assignee"] = obj["assignee"]["username"]

    # Remove some deprecated fields, replaced by "Assignee"
    cap_obj.pop("AssignedTo", None)
    cap_obj.pop("TeamAssignedTo", None)

    return cap_obj


def entity_to_readable(obj):
    """
    Readable json output from entity object
    """
    if obj is None:
        return {}

    # Capitalize fields
    cap_obj = {(k[0].capitalize() + k[1:]): v for k, v in obj.items()}

    # For Entities, show 'OperatingSystem'
    if "Os" in cap_obj:
        cap_obj["OperatingSystem"] = cap_obj.pop("Os", None)
    else:
        cap_obj["OperatingSystem"] = None

    if len(cap_obj.get("Inventory", [])) > 0:
        invdata = cap_obj["Inventory"][0]
        if "metadata" in invdata and "operatingSystem" in invdata["metadata"]:
            cap_obj["OperatingSystem"] = invdata["metadata"]["operatingSystem"]
        cap_obj["InventoryData"] = True
    else:
        cap_obj["InventoryData"] = False

    cap_obj.pop("Inventory", None)  # don't need to display data from inventory
    cap_obj.pop("Ip", None)  # don't need to display Ip object

    return cap_obj


def get_update_result(resp_json: bool):
    """
    Readable json output from update
    """
    return {"Result": "Success" if resp_json is True else "Failed", "Server Response": resp_json}


def insight_timestamp_to_created_format(timestamp_int):
    """
    Querying Insights using 'created' as opposed to 'timestamp' requires this conversion
    """
    created_time = datetime.utcfromtimestamp(timestamp_int)
    return datetime.strftime(created_time, "%Y-%m-%dT%H:%M:%S.%f")


def convert_timestampstr_to_epochms(timestampstr: str) -> int:
    """
    Convert a Signal or Insight timestamp string to epoch millisecs
    """
    try:
        incident_datetime = datetime.strptime(timestampstr, "%Y-%m-%dT%H:%M:%S.%f")
    except ValueError:
        incident_datetime = datetime.strptime(timestampstr, "%Y-%m-%dT%H:%M:%S")

    incident_created_time = int((incident_datetime - datetime.utcfromtimestamp(0)).total_seconds())
    return incident_created_time * 1000


def craft_sumo_url(svc_url: str, resource_type: str, id: str) -> str:
    """
    Craft a full URL to a Sumo Logic insight/signal based on its Id
    """
    if resource_type == "insight":
        return f"{svc_url}/sec/insight/{id}"
    elif resource_type == "signal":
        return f"{svc_url}/sec/signal/{id}"
    else:
        return ""


def is_inmirrorable_object(readable_remote_id: str) -> bool:
    """
    Check if a remote object ID is mirrorable into XSOAR. Currently on Sumo Logic Insights
    can be in-mirrored into XSOAR. Note the readable_remote_id must be in reable form, not
    the raw ID
    """
    return bool(readable_remote_id.startswith("INSIGHT"))


""" COMMAND FUNCTIONS """


def test_module(client: Client) -> str:
    """Tests API connectivity and authentication'

    Returning 'ok' indicates that the integration works like it is supposed to.
    Connection to the service is successful.
    Raises exceptions if something goes wrong.

    :type client: ``Client``
    :param Client: client to use

    :return: 'ok' if test passed, anything else will fail the test.
    :rtype: ``str``
    """

    message: str = ""
    try:
        # test client and auth
        client.req("GET", "sec/v1/insights", {})

        # test fetch_incidents command
        first_fetch_time = arg_to_datetime(
            arg="1 day",  # using '1 day' here since we're just testing connectivity and auth
            arg_name="First fetch time",
        )
        first_fetch_timestamp = int(first_fetch_time.timestamp()) if first_fetch_time else None
        # Using assert as a type guard (since first_fetch_time is always an int when required=True)
        assert isinstance(first_fetch_timestamp, int)
        fetch_incidents(
            client=client,
            max_results=20,
            last_run={},  # getLastRun() gets the last run dict
            first_fetch_time=first_fetch_timestamp,
            fetch_query="",  # defaults to status:in("new", "inprogress")
            pull_signals=False,
            record_summary_fields="",
            other_args=None,
        )
        message = "ok"
    except DemistoException as e:
        if "Unauthorized" in str(e):
            message = "Authorization Error: make sure Access ID and Access Key are correctly set"
        else:
            raise e
    return message


def insight_get_details(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Get insight details
    """
    insight_id = args.get("insight_id")
    if not insight_id:
        raise ValueError("insight_id not specified")

    record_summary_fields = args.get("record_summary_fields")

    query = {}
    query["exclude"] = "signals.allRecords"
    if record_summary_fields:
        query["recordSummaryFields"] = record_summary_fields

    resp_json = client.req("GET", f"sec/v1/insights/{insight_id}", query)
    insight = insight_signal_to_readable(resp_json)
    insight["SumoUrl"] = craft_sumo_url(client.get_extra_params()["instance_endpoint"], "insight", insight_id)

    readable_output = tableToMarkdown(
        "Insight Details:",
        [insight],
        [
            "Id",
            "ReadableId",
            "Name",
            "Action",
            "Status",
            "Assignee",
            "Description",
            "LastUpdated",
            "LastUpdatedBy",
            "Severity",
            "Closed",
            "ClosedBy",
            "Timestamp",
            "Entity",
            "Resolution",
            "SumoUrl",
        ],
        headerTransform=pascalToSpace,
    )

    return CommandResults(
        readable_output=readable_output, outputs_prefix="SumoLogicSec.Insight", outputs_key_field="Id", outputs=insight
    )


def insight_add_comment(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Add a comment to an insight
    """
    insight_id = args.get("insight_id")

    reqbody = {}
    reqbody["body"] = args.get("comment")
    c = client.req("POST", f"sec/v1/insights/{insight_id}/comments", None, reqbody)

    comment = [
        {
            "Id": c.get("id"),
            "Body": c.get("body"),
            "Author": c.get("author").get("username"),
            "Timestamp": c.get("timestamp"),
            "InsightId": insight_id,
        }
    ]
    readable_output = tableToMarkdown(
        "Insight Added Comment:", comment, ["Id", "InsightId", "Author", "Body", "Timestamp"], headerTransform=pascalToSpace
    )

    return CommandResults(
        readable_output=readable_output, outputs_prefix="SumoLogicSec.InsightComments", outputs_key_field="Id", outputs=comment
    )


def insight_get_comments(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Get comments for insight
    """
    insight_id = args.get("insight_id")
    resp_json = client.req("GET", f"sec/v1/insights/{insight_id}/comments")
    comments = [
        {
            "Id": c.get("id"),
            "Body": c.get("body"),
            "Author": c.get("author").get("username"),
            "Timestamp": c.get("timestamp"),
            "InsightId": insight_id,
        }
        for c in resp_json.get("comments")
    ]
    readable_output = tableToMarkdown(
        "Insight Comments:",
        comments,
        ["Id", "InsightId", "Author", "Body", "LastUpdated", "Timestamp"],
        headerTransform=pascalToSpace,
    )

    return CommandResults(
        readable_output=readable_output, outputs_prefix="SumoLogicSec.InsightComments", outputs_key_field="Id", outputs=comments
    )


def signal_get_details(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Get signal details
    """
    signal_id = args.get("signal_id", None)
    if not signal_id:
        raise ValueError("signal_id not specified")

    signal = client.req("GET", f"sec/v1/signals/{signal_id}")
    signal.pop("allRecords", None)  # don't need to display records from signal
    signal = insight_signal_to_readable(signal)
    signal["SumoUrl"] = craft_sumo_url(client.get_extra_params()["instance_endpoint"], "signal", signal_id)
    readable_output = tableToMarkdown(
        "Signal Details:",
        [signal],
        ["Id", "Name", "RuleId", "Description", "Severity", "ContentType", "Timestamp", "Entity", "SumoUrl"],
        headerTransform=pascalToSpace,
    )

    return CommandResults(
        readable_output=readable_output, outputs_prefix="SumoLogicSec.Signal", outputs_key_field="Id", outputs=signal
    )


def entity_get_details(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Get entity details
    """
    entity_id = args.get("entity_id", None)
    if not entity_id:
        raise ValueError("entity_id not specified")

    resp_json = client.req("GET", f"sec/v1/entities/{entity_id}", {"expand": "inventory"})
    entity = entity_to_readable(resp_json)
    readable_output = tableToMarkdown(
        "Entity Details:",
        [entity],
        ["Id", "Name", "FirstSeen", "LastSeen", "ActivityScore", "IsWhitelisted", "OperatingSystem", "InventoryData"],
        headerTransform=pascalToSpace,
    )

    return CommandResults(
        readable_output=readable_output, outputs_prefix="SumoLogicSec.Entity", outputs_key_field="Id", outputs=entity
    )


def insight_search(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Search insights using available filters

    The search query string is a custom DSL that is used to filter the results.

    Operators:
    - `exampleField:"bar"`: The value of the field is equal to "bar".
    - `exampleField:in("bar", "baz", "qux")`: The value of the field is equal to either "bar", "baz", or "qux".
    - `exampleTextField:contains("foo bar")`: The value of the field contains the phrase "foo bar".
    - `exampleNumField:>5`: The value of the field is greater than 5. There are similar `<`, `<=`, and `>=` operators.
    - `exampleNumField:5..10`: The value of the field is between 5 and 10 (inclusive).
    - `exampleDateField:>2019-02-01T05:00:00+00:00`: The value of the date field is after 5 a.m. UTC time on February 2,
        2019.
    - `exampleDateField:2019-02-01T05:00:00+00:00..2019-02-01T08:00:00+00:00`: The value of the date field is between 5 a.m.
        and 8 a.m. UTC time on February 2, 2019.

    Fields:
    - id
    - readableId
    - status
    - name
    - insightId
    - description
    - created
    - timestamp
    - closed
    - assignee
    - entity.ip
    - entity.hostname
    - entity.username
    - entity.type
    - enrichment
    - tag
    - severity
    - resolution
    - ruleId
    - records

    For example, the query `timestamp:>2021-03-18T12:00:00+00:00 severity:"HIGH` will return insights of high severity
    created after 12 PM UTC time on March 18th, 2021.
    """
    record_summary_fields = args.get("record_summary_fields")

    query = {}
    q = args.get("query", "")
    q = arg_time_query_to_q(q, args.get("created"), "created")
    q = add_list_to_q(q, ["status", "assignee"], args)
    query["q"] = q
    query["offset"] = args.get("offset")
    query["limit"] = args.get("limit")
    query["exclude"] = "signals.allRecords"
    if record_summary_fields:
        query["recordSummaryFields"] = record_summary_fields

    resp_json = client.req("GET", "sec/v1/insights", query)
    insights = []
    for insight in resp_json.get("objects"):
        insights.append(insight_signal_to_readable(insight))

    readable_output = tableToMarkdown(
        "Insights:",
        insights,
        [
            "Id",
            "ReadableId",
            "Name",
            "Action",
            "Status",
            "Assignee",
            "Description",
            "LastUpdated",
            "LastUpdatedBy",
            "Severity",
            "Closed",
            "ClosedBy",
            "Timestamp",
            "Entity",
            "Resolution",
        ],
        headerTransform=pascalToSpace,
    )

    return CommandResults(
        readable_output=readable_output, outputs_prefix="SumoLogicSec.InsightList", outputs_key_field="Id", outputs=insights
    )


def entity_search(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Search entities using the available filters
    """
    query = {}
    q = args.get("query", "")
    q = add_list_to_q(q, ["ip", "hostname", "username", "type", "whitelisted", "tag"], args)
    query["q"] = q
    query["offset"] = args.get("offset")
    query["limit"] = args.get("limit")
    query["sort_by"] = args.get("sort")
    query["expand"] = "inventory"

    resp_json = client.req("GET", "sec/v1/entities", query)
    entities = []
    for entity in resp_json.get("objects"):
        entities.append(entity_to_readable(entity))

    readable_output = tableToMarkdown(
        "Entities:",
        entities,
        ["Id", "Name", "FirstSeen", "LastSeen", "ActivityScore", "IsWhitelisted", "OperatingSystem", "InventoryData"],
        headerTransform=pascalToSpace,
    )

    return CommandResults(
        readable_output=readable_output, outputs_prefix="SumoLogicSec.EntityList", outputs_key_field="Id", outputs=entities
    )


def signal_search(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Search signals using available filters
    """
    query = {}
    q = args.get("query", "")
    q = arg_time_query_to_q(q, args.get("created"), "created")
    q = add_list_to_q(q, ["category", "contentType"], args)
    query["q"] = q
    query["offset"] = args.get("offset")
    query["limit"] = args.get("limit")

    resp_json = client.req("GET", "sec/v1/signals", query)
    signals = []
    for signal in resp_json.get("objects"):
        signal.pop("allRecords", None)  # don't need to display records from signal
        signals.append(insight_signal_to_readable(signal))

    readable_output = tableToMarkdown(
        "Signals:",
        signals,
        ["Id", "Name", "Entity", "RuleId", "Description", "Severity", "Stage", "Timestamp", "ContentType", "Tags"],
        headerTransform=pascalToSpace,
    )

    return CommandResults(
        readable_output=readable_output, outputs_prefix="SumoLogicSec.SignalList", outputs_key_field="Id", outputs=signals
    )


def insight_set_status(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Change status of insight

    Provide "reason" arg when closing an Insight with status=closed.
    """
    insight_id = args.get("insight_id")
    reqbody = {}
    reqbody["status"] = args.get("status")
    resolution = args.get("sub_resolution") or args.get("resolution")

    if args.get("status") == "closed" and resolution:
        # resolution should only be specified when the status is set to "closed"
        reqbody["resolution"] = resolution

    resp_json = client.req("PUT", f"sec/v1/insights/{insight_id}/status", None, reqbody)

    for s in resp_json.get("signals"):
        s.pop("allRecords", None)

    insight = insight_signal_to_readable(resp_json)

    readable_output = tableToMarkdown(
        "Insight Details:",
        [insight],
        [
            "Id",
            "ReadableId",
            "Name",
            "Action",
            "Status",
            "Assignee",
            "Description",
            "LastUpdated",
            "LastUpdatedBy",
            "Severity",
            "Closed",
            "ClosedBy",
            "Timestamp",
            "Entity",
            "Resolution",
        ],
        headerTransform=pascalToSpace,
    )

    return CommandResults(
        readable_output=readable_output, outputs_prefix="SumoLogicSec.Insight", outputs_key_field="Id", outputs=insight
    )


def match_list_get(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Get match lists
    """
    query = {}
    query["offset"] = args.get("offset")
    query["limit"] = args.get("limit")
    query["sort"] = args.get("sort")
    query["sortDir"] = args.get("sortDir")

    resp_json = client.req("GET", "sec/v1/match-lists", query)
    match_lists = []
    for match_list in resp_json.get("objects"):
        cap_match_list = {(k[0].capitalize() + k[1:]): v for k, v in match_list.items()}
        match_lists.append(cap_match_list)
    readable_output = tableToMarkdown(
        "Match lists:", match_lists, headers=["Id", "Name", "TargetColumn", "DefaultTtl"], headerTransform=pascalToSpace
    )
    # Filtered out from readable output: 'Description', 'Created', 'CreatedBy', 'LastUpdated', 'LastUpdatedBy'

    return CommandResults(
        readable_output=readable_output, outputs_prefix="SumoLogicSec.MatchLists", outputs_key_field="Id", outputs=match_lists
    )


def match_list_update(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Add to match list
    """
    match_list_id = args.get("match_list_id")
    item = {}
    item["active"] = args.get("active")
    item["description"] = args.get("description")
    item["expiration"] = args.get("expiration")
    item["value"] = args.get("value")

    resp_json = client.req("POST", f"sec/v1/match-lists/{match_list_id}/items", None, {"items": [item]})
    result = get_update_result(resp_json)
    readable_output = tableToMarkdown("Result:", [result], ["Result", "Server Response"])

    return CommandResults(readable_output=readable_output, outputs_prefix="SumoLogicSec.UpdateResult", outputs=result)


def threat_intel_search_indicators(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Search Threat Intel Indicators

    The search query string in our custom DSL that is used to filter the results.

    Operators:
    - `exampleField:"bar"`: The value of the field is equal to "bar".
    - `exampleField:in("bar", "baz", "qux")`: The value of the field is equal to either "bar", "baz", or "qux".
    - `exampleTextField:contains("foo bar")`: The value of the field contains the phrase "foo bar".
    - `exampleNumField:>5`: The value of the field is greater than 5. There are similar `<`, `<=`, and `>=` operators.
    - `exampleNumField:5..10`: The value of the field is between 5 and 10 (inclusive).
    - `exampleDateField:>2019-02-01T05:00:00+00:00`: The value of the date field is after 5 a.m. UTC time on February 2,
        2019.
    - `exampleDateField:2019-02-01T05:00:00+00:00..2019-02-01T08:00:00+00:00`: The value of the date field is between 5 a.m.
        and 8 a.m. UTC time on February 2, 2019.

    Fields:
    - id
    - targetColumn
    - value
    - active
    - expirationDate
    - listName
    - description
    - created
    """
    query = {}
    if args.get("query"):
        query["q"] = args.get("query")
    query["value"] = args.get("value")
    if args.get("sourceIds"):
        query["sourceIds"] = args["sourceIds"].split(",")
    query["offset"] = args.get("offset")
    query["limit"] = args.get("limit")

    resp_json = client.req("GET", "sec/v1/threat-intel-indicators", query)
    indicators = []
    for indicator in resp_json.get("objects"):
        cap_indicator = {(k[0].capitalize() + k[1:]): v for k, v in indicator.items()}
        indicators.append(cap_indicator)

    readable_output = tableToMarkdown("Threat Intel Indicators:", indicators, ["Id", "Value", "Active", "Expiration"])
    # Filtered out from readable output: Meta

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="SumoLogicSec.ThreatIntelIndicators",
        outputs_key_field="Id",
        outputs=indicators,
    )


def threat_intel_get_sources(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Get the list of Threat Intel Sources
    """
    query = {}
    query["offset"] = args.get("offset")
    query["limit"] = args.get("limit")
    query["sort"] = args.get("sort")
    query["sortDir"] = args.get("sortDir")

    resp_json = client.req("GET", "sec/v1/threat-intel-sources", query)
    threat_intel_sources = []
    for threat_intel_source in resp_json.get("objects"):
        cap_threat_intel_source = {(k[0].capitalize() + k[1:]): v for k, v in threat_intel_source.items()}
        threat_intel_sources.append(cap_threat_intel_source)
    readable_output = tableToMarkdown(
        "Threat intel sources:",
        threat_intel_sources,
        headers=["Id", "Name", "Description", "SourceType"],
        headerTransform=pascalToSpace,
    )
    # Filtered out from readable output: Created', 'CreatedBy', 'LastUpdated', 'LastUpdatedBy'

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="SumoLogicSec.ThreatIntelSources",
        outputs_key_field="Id",
        outputs=threat_intel_sources,
    )


def threat_intel_update_source(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Add Indicator to a Threat Intel Source
    """
    threat_intel_source_id = args.get("threat_intel_source_id")
    item = {}
    item["active"] = args.get("active")
    item["description"] = args.get("description")
    item["expiration"] = args.get("expiration")
    item["value"] = args.get("value")

    resp_json = client.req("POST", f"sec/v1/threat-intel-sources/{threat_intel_source_id}/items", None, {"indicators": [item]})
    result = get_update_result(resp_json)
    readable_output = tableToMarkdown("Result:", [result], ["Result", "Response"])

    return CommandResults(readable_output=readable_output, outputs_prefix="SumoLogicSec.UpdateResult", outputs=result)


def cleanup_records(signal: Optional[dict[str, Any]]) -> Optional[dict[str, Any]]:
    """
    Function to clean up all "bro" fields of the records under a Signal object
    """
    if (signal is None) or ("allRecords" not in signal):
        return None
    for rec in signal["allRecords"]:
        field_names = list(rec.keys())
        for field_name in field_names:
            if field_name.startswith("bro") and rec.get(field_name) in ([], (), {}, ""):
                rec.pop(field_name, None)
            if field_name == "timestamp":
                rec["lastlog_timestamp"] = rec["timestamp"]
                rec.pop(field_name, None)

    return signal


def get_remote_data_command(client: Client, args: dict, close_incident: bool):
    """get-remote-data command: Returns an updated Sumo Logic Cloud SIEM Insight incident

    Args:
        client: Client object to call a Sumo Logic SIEM
        args (dict): The command arguments
        close_incident (bool): Whether to close the corresponding XSOAR incident if the Sumo Logic SIEM Insight has been
        closed

    Returns:
        GetRemoteDataResponse: The Response containing the update to mirror and the entries
    """
    entries = []
    remote_args = GetRemoteDataArgs(args)
    last_update = remote_args.last_update
    # last_update_utc = dateparser.parse(last_update, settings={'TIMEZONE': 'UTC'})
    insight_id = remote_args.remote_incident_id
    demisto.debug(f"Get-Remote-Data-Command for {insight_id} {last_update}")
    if not is_inmirrorable_object(insight_id):
        demisto.debug(f"Not in-mirrorable object with {insight_id}")
        return GetRemoteDataResponse(mirrored_object={}, entries={})
    else:
        insight = insight_get_details(client, {"insight_id": insight_id}).outputs
        insight_resolution = insight.get("Resolution")  # type: ignore
        if insight["Status"] == "Closed" and close_incident:  # type: ignore
            resolution = insight_resolution
            if resolution == "No Action":
                resolution = "Other"
            demisto.info(f"Closing incident related to Sumo Logic Insight {insight_id} with resolution {insight_resolution}, \
                which is mapped to XSOAR reason: {resolution}")
            entries = [
                {
                    "Type": EntryType.NOTE,
                    "Contents": {
                        "dbotIncidentClose": True,
                        "closeReason": resolution,
                        "closeNotes": f"Insight {insight_id} was closed on Sumo Logic SIEM with resolution {insight_resolution}, \
                        which is mapped to XSOAR reason: {resolution}.",
                    },
                    "ContentsFormat": EntryFormat.JSON,
                }
            ]
        demisto.debug(f"Updated Sumo Logic Insight {insight_id}")
        return GetRemoteDataResponse(mirrored_object=insight, entries=entries)


def update_remote_system_command(client: Client, args: dict[str, Any], params: dict[str, Any]) -> str:
    """Pushes changes in XSOAR incident into the corresponding Sumo Logic Insight.

    Args:
        args (dict): Demisto args
        params (dict): Demisto params
        client: Client to connect to Sumo Logic SIEM

    Returns:
        insight_id (str): The Sumo Logic Insight Id

    """
    parsed_args = UpdateRemoteSystemArgs(args)
    delta = parsed_args.delta
    # Insight ID on Sumo Logic SIEM side
    insight_id = parsed_args.remote_incident_id
    # Incident ID on XSOAR side
    incident_id = parsed_args.data["id"]
    if not is_inmirrorable_object(insight_id):
        demisto.info(f"Not an Insight incident: {insight_id} so won't mirror")
        return insight_id

    if parsed_args.incident_changed and delta:
        demisto.debug(f"Got the following delta keys {list(delta.keys())!s} to update incident \
            corresponding to Insight {insight_id}")
        demisto.debug(f"Got the following delta {delta} to update incident corresponding to Insight {insight_id}")
        demisto.debug(f"Incident Id: {incident_id}")
        changed_data = {field: "" for field in OUTGOING_MIRRORED_FIELDS}
        for field in delta:
            if field in OUTGOING_MIRRORED_FIELDS:
                changed_data[field] = delta[field]

        if "closeReason" in delta:
            if parsed_args.inc_status == IncidentStatus.ACTIVE:
                changed_data["status"] = "inprogress"
            if parsed_args.inc_status == IncidentStatus.PENDING:
                changed_data["status"] = "new"
            # Close Insight if relevant
            if parsed_args.inc_status == IncidentStatus.DONE and params.get("close_insight"):
                demisto.debug(f"Closing Sumo Logic Insight {insight_id}")
                changed_data["status"] = "closed"
            # XSOAR has by default: False Positive, Duplicate, Resolved and Other which can be
            # mapped directly to Sumo Logic default resolutions (except for Other). For any custom
            # resolution, there must be a 1-1 mapping between Sumo SIEM and XSOAR side.
            reason = delta["closeReason"]
            if reason == "Other":
                reason = "Resolved"
            changed_data["resolution"] = reason
            changed_data["insight_id"] = insight_id
            demisto.debug(f"Sending update status request to Sumo Logic for Insight {insight_id}, data: {changed_data}")

            insight_add_comment(
                client,
                {
                    "insight_id": insight_id,
                    "comment": f"Close since the corresponding XSOAR Insight incident: {incident_id} was closed",
                },
            )
            insight_obj = insight_set_status(client, changed_data).outputs  # type: ignore
            return insight_obj.get("ReadableId")  # type: ignore
    else:
        demisto.debug(f"Incident corresponding to Sumo Logic Insight {insight_id} was not changed.")

    return insight_id


def get_modified_remote_data_command(client: Client, args: Any) -> Any:
    """Gets all Sumo Logic Insights that have changed since a given time. Currently not used
    since Sumo Logic API does not allow filtering insights by update time

    Args:
        client: Client object to call a Sumo Logic SIEM
        args (dict): The command arguments

    Returns:
        GetModifiedRemoteDataResponse: The response containing the list of ids of Insights changed
    """
    raise NotImplementedError("get-modified-remote-data not implemented")


def fetch_incidents(
    client: Client,
    max_results: int,
    last_run: dict[str, Any],
    first_fetch_time: Optional[int],
    fetch_query: Optional[str],
    pull_signals: Optional[bool],
    record_summary_fields: Optional[str],
    other_args: dict[str, Any] | None,
) -> tuple[dict[str, Any], list[dict]]:
    """
    Retrieve new incidents periodically based on pre-defined instance parameters.
    Implements a lookback window and deduplication to avoid missing delayed insights.
    """

    # Get the last fetch time, if exists
    # last_run is a dict with a single key, called last_fetch
    demisto.debug(f"Sumo Logic Integration last run: {last_run}")
    last_fetch = last_run.get("last_fetch", None)

    # Track IDs from previous run for deduplication during lookback window overlap
    last_seen_ids = last_run.get("last_fetch_ids", [])
    last_seen_ids = set(last_seen_ids) if last_seen_ids else set()  # convert to set for O(1) deduplication lookup
    current_fetch_ids: list[str] = []

    # Set lookback seconds based on whether this is first run
    lookback_seconds = 0 if last_fetch is None else DEFAULT_LOOKBACK_SECONDS
    if last_fetch is None:
        # First run: no lookback, use provided first_fetch_time
        if first_fetch_time is None:
            raise ValueError("first_fetch_time must be provided on first run")
        fetch_start_time = int(first_fetch_time)
        # Store first_fetch_time to persist across cycles
        stored_first_fetch_time = fetch_start_time
    else:
        # Subsequent runs: apply lookback window to catch delayed insights
        last_fetch = int(last_fetch)
        fetch_start_time = last_fetch - lookback_seconds

        # Retrieve stored first_fetch_time or initialize it for backwards compatibility
        # For existing instances without stored value, use current fetch_start_time as the floor
        # This value will be persisted to prevent infinite backward regression
        stored_first_fetch_time = last_run.get("first_fetch_time", fetch_start_time)

        # Cap fetch_start_time to never go below the original first_fetch_time
        # This prevents infinite backward regression when hitting fetch limits
        fetch_start_time = max(fetch_start_time, stored_first_fetch_time)

        # Throw error if fetch_start_time is negative to prevent querying from Unix epoch
        if fetch_start_time <= 0:
            raise ValueError(
                f"Calculated fetch_start_time ({fetch_start_time}) is negative. "
                f"last_fetch={last_fetch}, lookback_seconds={lookback_seconds}"
            )

    # Track the latest timestamp for next run's last_fetch
    latest_created_time = last_fetch if last_fetch else fetch_start_time

    # Initialize an empty list of incidents to return
    # Each incident is a dict with a string as a key
    incidents: list[dict[str, Any]] = []

    # Set query values that do not change with pagination
    q = f"created:>={insight_timestamp_to_created_format(fetch_start_time)}"
    offset = 0

    # ensure consistent ordering for pagination, even if new insights are created during the fetch process
    query = {
        "sort": "CREATED",
        "sortDir": "ASC",
    }
    if fetch_query:
        query["q"] = q + " " + fetch_query
    else:
        query["q"] = q + ' status:in("new", "inprogress")'
    query["limit"] = str(max_results)
    if record_summary_fields:
        query["recordSummaryFields"] = record_summary_fields
    hasNextPage = True
    instance_endpoint = client.get_extra_params()["instance_endpoint"]
    signal_ids = []
    counter = 0
    # Retrieve Insights
    while hasNextPage and counter < max_results:
        # only query parameter that changes loop to loop is the offset
        query["offset"] = str(offset)
        resp_json = client.req("GET", "sec/v1/insights", query)
        if not resp_json:
            break
        for insight in resp_json.get("objects", []):
            # If no created_time set is as epoch (0). We use time in ms so we must
            # convert it from the API response
            insight_timestamp = insight.get("created")
            insight_id = insight.get("id")
            insight_readableid = insight.get("readableId")
            # add sumoUrl to raw insight:
            insight["sumoUrl"] = craft_sumo_url(instance_endpoint, "insight", insight_id)
            if other_args is not None:
                insight["mirror_instance"] = other_args["mirror_instance"]
                insight["mirror_direction"] = other_args["mirror_direction"]

            # Skip already processed insights to prevent duplicates during lookback overlap
            if insight_id and insight_timestamp and insight_id not in last_seen_ids:
                incident_created_time_ms = convert_timestampstr_to_epochms(insight_timestamp)
                incident_created_time = int(incident_created_time_ms / 1000)

                # Process signals associated with this insight
                signals = insight.get("signals", [])
                for signal in signals:
                    # add sumoUrl to signal:
                    signal_id = signal["id"]
                    signal_ids.append(signal_id)
                    signal["sumoUrl"] = craft_sumo_url(instance_endpoint, "signal", signal_id)
                    cleanup_records(signal)

                # Create incident
                incidents.append(
                    {
                        "name": insight.get("name", "No name") + " - " + insight_readableid,
                        "occurred": timestamp_to_datestring(incident_created_time_ms),
                        "details": insight.get("description"),
                        "severity": translate_severity(insight.get("severity")),
                        "rawJSON": json.dumps(insight),
                    }
                )
                counter += 1
                # Update latest timestamp if this insight is newer
                if incident_created_time > latest_created_time:
                    latest_created_time = incident_created_time

            # Track this insight ID for deduplication in the next run if it's within the lookback window
            current_fetch_ids.append(insight_id)

        if not resp_json.get("hasNextPage"):
            hasNextPage = False
        else:
            offset += len(resp_json.get("objects", []))

    final_incidents = []
    if pull_signals:
        # Retrieve Signals associated with the insights
        signal_query = {}
        i = 0
        batch_size = 10
        signal_incidents = []
        while i < len(signal_ids):
            signal_list_str = ",".join([f'"{x}"' for x in signal_ids[i : i + batch_size]])
            signal_query["q"] = f"id:in({signal_list_str})"
            resp_json = client.req("GET", "sec/v1/signals", signal_query)
            if not resp_json:
                break
            for signal in resp_json.get("objects", []):
                signal_id = signal.get("id")
                # add sumoUrl to signal:
                signal["sumoUrl"] = craft_sumo_url(instance_endpoint, "signal", signal_id)
                # field inserted for classifier
                signal["readableId"] = "SIGNAL-" + signal_id
                cleanup_records(signal)
                signal_created_time_ms = convert_timestampstr_to_epochms(signal["timestamp"])
                signal_incidents.append(
                    {
                        "name": signal.get("name", "No name") + " - " + signal_id,
                        "occurred": timestamp_to_datestring(signal_created_time_ms),
                        "details": signal.get("description"),
                        "severity": translate_severity(signal.get("severity")),
                        "rawJSON": json.dumps(signal),
                    }
                )
                if other_args is not None:
                    signal["mirror_instance"] = other_args["mirror_instance"]
                    signal["mirror_direction"] = other_args["mirror_direction"]
            i += batch_size

        # Append incidents to the signal list so the signals will be created first:
        final_incidents.extend(signal_incidents)
        del signal_incidents

    final_incidents.extend(incidents)
    del incidents

    # Save the next_run as a dict with the last_fetch and last_seen_ids keys to be stored
    next_run = cast(
        dict[str, Any],
        {
            "last_fetch": latest_created_time,
            "last_fetch_ids": current_fetch_ids if len(current_fetch_ids) > 0 else list(last_seen_ids),
            "first_fetch_time": stored_first_fetch_time,
        },
    )
    return next_run, final_incidents


""" MAIN FUNCTION """


def main() -> None:  # pragma: no cover
    """main function, parses params and runs command functions

    :return:
    :rtype:
    """

    proxy = demisto.params().get("proxy", False)
    base_url = demisto.getParam("api_endpoint")
    access_id = demisto.getParam("access_id")
    access_key = demisto.getParam("access_key")
    verify_certificate = not demisto.params().get("insecure", False)

    # How much time before the first fetch to retrieve incidents
    first_fetch_time = arg_to_datetime(
        arg=demisto.params().get("first_fetch", "1 day"), arg_name="First fetch time", required=True
    )
    first_fetch_timestamp = int(first_fetch_time.timestamp()) if first_fetch_time else None
    # Using assert as a type guard (since first_fetch_time is always an int when required=True)
    assert isinstance(first_fetch_timestamp, int)

    fetch_query = demisto.getParam("fetch_query")
    record_summary_fields = demisto.getParam("record_summary_fields")
    pull_signals = demisto.getParam("pull_signals")
    other_args = {
        "mirror_instance": demisto.integrationInstance(),
        "mirror_direction": MIRROR_DIRECTION.get(demisto.params().get("mirror_direction")),
    }

    command = demisto.command()
    demisto.debug(f"Command being called is {command}")
    try:
        client = Client(
            base_url=base_url,
            verify=verify_certificate,
            headers=DEFAULT_HEADERS,
            proxy=proxy,
            auth=(access_id, access_key),
            ok_codes=[200],
        )
        client.set_extra_params({"instance_endpoint": demisto.getParam("instance_endpoint")})
        if command == "test-module":
            # This is the call made when pressing the integration Test button.
            result = test_module(client)
            return_results(result)
        elif command == "sumologic-sec-insight-get-details":
            return_results(insight_get_details(client, demisto.args()))
        elif command == "sumologic-sec-insight-get-comments":
            return_results(insight_get_comments(client, demisto.args()))
        elif command == "sumologic-sec-insight-add-comment":
            return_results(insight_add_comment(client, demisto.args()))
        elif command == "sumologic-sec-signal-get-details":
            return_results(signal_get_details(client, demisto.args()))
        elif command == "sumologic-sec-entity-get-details":
            return_results(entity_get_details(client, demisto.args()))
        elif command == "sumologic-sec-insight-search":
            return_results(insight_search(client, demisto.args()))
        elif command == "sumologic-sec-entity-search":
            return_results(entity_search(client, demisto.args()))
        elif command == "sumologic-sec-signal-search":
            return_results(signal_search(client, demisto.args()))
        elif command == "sumologic-sec-insight-set-status":
            return_results(insight_set_status(client, demisto.args()))
        elif command == "sumologic-sec-match-list-get":
            return_results(match_list_get(client, demisto.args()))
        elif command == "sumologic-sec-match-list-update":
            return_results(match_list_update(client, demisto.args()))
        elif command == "sumologic-sec-threat-intel-search-indicators":
            return_results(threat_intel_search_indicators(client, demisto.args()))
        elif command == "sumologic-sec-threat-intel-get-sources":
            return_results(threat_intel_get_sources(client, demisto.args()))
        elif command == "sumologic-sec-threat-intel-update-source":
            return_results(threat_intel_update_source(client, demisto.args()))
        elif command == "update-remote-system":
            demisto.info("########### MIRROR OUT FROM XSOAR #############")
            update_remote_system_command(client, demisto.args(), demisto.params())
        elif command == "get-remote-data":
            demisto.info("########### MIRROR INTO XSOAR #############")
            return_results(get_remote_data_command(client, demisto.args(), other_args["mirror_direction"] is not None))
        elif command == "get-modified-remote-data":
            return_results(get_modified_remote_data_command(client, demisto.args()))

        elif command == "fetch-incidents":
            # Convert the argument to an int using helper function or set to MAX_INCIDENTS_TO_FETCH
            max_results = arg_to_number(arg=demisto.params().get("max_fetch"), arg_name="max_fetch", required=False)
            if not max_results or max_results > MAX_INCIDENTS_TO_FETCH:
                max_results = MAX_INCIDENTS_TO_FETCH

            next_run, incidents = fetch_incidents(
                client=client,
                max_results=max_results,
                last_run=demisto.getLastRun(),  # getLastRun() gets the last run dict
                first_fetch_time=first_fetch_timestamp,
                fetch_query=fetch_query,
                pull_signals=pull_signals,
                record_summary_fields=record_summary_fields,
                other_args=other_args,
            )

            # saves next_run for the time fetch-incidents is invoked
            demisto.setLastRun(next_run)

            # fetch-incidents calls ``demisto.incidents()`` to provide the list
            # of incidents to create
            demisto.incidents(incidents)

    # Log exceptions and return errors
    except Exception as e:
        demisto.error(traceback.format_exc())  # print the traceback
        return_error(f"Failed to execute {demisto.command()} command.\nError:\n{e!s}")


""" ENTRY POINT """

if __name__ in ("__main__", "__builtin__", "builtins"):
    main()