Sekoia XDR

Fetch alerts and events from SEKOIA.IO XDR.\nTo use this integration, please create an API Key with the appropriate permissions.

Analytics & SIEM · SekoiaXDR

Details

IDSekoia XDR
ProviderSekoia
CategoryAnalytics & SIEM
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10404775
Supported ModulesAgentix XSIAM

README

Fetch alerts and events from SEKOIA.IO XDR.
To use this integration, please create an API Key with the appropriate permissions.
This integration was integrated and tested with version 1.0 of Sekoia XDR.

Configure Sekoia XDR in Cortex

Parameter Description Required
API Key   True
Server URL (i.e. https://api.sekoia.io)   True
Trust any certificate (not secure)   False
Use system proxy settings   False
Fetch incidents   False
Incident type   False
First fetch timestamp (<sign + or -><number><time unit>, e.g., -7d, -1h)   True
Maximum incidents to fetch per interval. By default the max_fetch is set to 10 True
Incidents Fetch Interval   False
Alerts status. Filter alerts to fetch by status. You can write and press enter to insert new types. False
Alerts types. Filter alerts to fetch by types. You can write and press enter to insert new types. False
Alerts urgency levels ( “MINurgency,MAXurgency”. i.e: 80,100 ). Filter alerts by their urgency levels. Use the format “MINurgency, MAXurgency” False
Fetch mode If there’s no max_fetch it will fetch 10 incidents by default. True
Replace “dots” in event field names with another character. Replacing dots in events will make names look pretty good for users True
Events fields to exclude from the events search result. These are the names of the headers presented in the events table. If the header is not in the dropdown list write it and press enter. False
Include assets information in the alerts when fetching. When selected, it includes the assets information in the alert when fetched from Sekoia.
And also If there’s no max_fetch it will fetch 10 incidents by default.
False
Include kill chain information in the alerts when fetching. When selected, it includes the kill chain information in the alert when fetched from Sekoia.
And also If there’s no max_fetch it will fetch 10 incidents by default.
False
Incident Mirroring Direction. Choose the direction to mirror the incident: None(Disable mirroring), Incoming (from Sekoia XDR to Cortex XSOAR) , Outgoing (from Cortex XSOAR to Sekoia XDR), or Incoming and Outgoing (from/to Cortex XSOAR and Sekoia XDR). Cortex XSOAR only parameter. False
Include events in the mirroring of the alerts. When selected, it includes the events in the mirrored alerts when an alert is updated in Sekoia. False
Include kill chain information in the mirroring of the alerts. When selected, it includes the kill chain information of the alert in the mirrored alerts when an alert is updated in Sekoia. False
Reopen Mirrored Cortex XSOAR Incidents (Incoming Mirroring) When selected, reopening the Sekoia XDR alert will reopen the Cortex XSOAR incident. False
Close Mirrored Cortex XSOAR Incidents (Incoming Mirroring) When selected, closing the Sekoia XDR alert with a “Closed” or “Reject” status will close the Cortex XSOAR incident. Cortex XSOAR only parameter. False
Close notes. Change the closing notes that will be added to the tickets closed automatically by the automation. True
Timezone ( TZ format ) This will be used to present dates in the appropiate timezones, used for comment timestamps, etc. True

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.

sekoia-xdr-list-alerts


Command to retrieve a list of Alerts from Sekoia XDR.

Base Command

sekoia-xdr-list-alerts

Input

Argument Name Description Required
limit The number of alerts to return. The allowed range is 1-100, default is 20. Default is 20. Optional
status Match alerts by their status name (separated by commas). Possible values are: Pending, Acknowledged, Ongoing, Rejected, Closed. Optional
created_at Filter alerts by their creation dates, starting date followed by ending date, i.e: “-3d,now” , “-1w,now” or “2023-01-15,2023-01-17”. Optional
updated_at Filter alerts by their update dates starting date followed by ending date, i.e: “-3d,now” , “-1w,now” or “2023-01-15,2023-01-17”. Optional
urgency Filter alerts by their urgencies range in the following format: “MINurgency,MAXurgency”. i.e: 80,100. Optional
Alerts type Match alerts by their categories (separated by commas). Possible values are: spam, ddos, outage, phishing, unauthorized-use-of-resources, unauthorised-information-access, appscan, scanner, brute-force, exploit. Optional
sort_by Sort the alerts by any information. Possible values are: created_at, updated_at, target, urgency, status. Default is created_at. Optional

Context Output

Path Type Description
SekoiaXDR.ListAlerts.updated_at unknown The time at which the alert was updated.
SekoiaXDR.ListAlerts.updated_by unknown The user who last updated the alert.
SekoiaXDR.ListAlerts.uuid unknown The unique identifier of the alert.
SekoiaXDR.ListAlerts.title unknown The title of the alert.
SekoiaXDR.ListAlerts.time_to_respond unknown The time it took to respond to the alert.
SekoiaXDR.ListAlerts.short_id unknown The short identifier of the alert.
SekoiaXDR.ListAlerts.community_uuid unknown The unique identifier of the community associated with the alert.
SekoiaXDR.ListAlerts.kill_chain_short_id unknown The short identifier of the kill chain associated with the alert.
SekoiaXDR.ListAlerts.number_of_unseen_comments unknown The number of unseen comments on the alert.
SekoiaXDR.ListAlerts.updated_by_type unknown The type of user who last updated the alert.
SekoiaXDR.ListAlerts.source unknown The source of the alert.
SekoiaXDR.ListAlerts.alert_type.value unknown The type of the alert.
SekoiaXDR.ListAlerts.alert_type.category unknown The category type of the alert.
SekoiaXDR.ListAlerts.time_to_acknowledge unknown The time it took to acknowledge the alert.
SekoiaXDR.ListAlerts.stix unknown The STIX data associated with the alert.
SekoiaXDR.ListAlerts.first_seen_at unknown The time the alert was first seen.
SekoiaXDR.ListAlerts.ttps.type unknown The type of the TTP associated with the alert.
SekoiaXDR.ListAlerts.ttps.name unknown The name of the TTP associated with the alert.
SekoiaXDR.ListAlerts.ttps.id unknown The unique identifier of the TTP associated with the alert.
SekoiaXDR.ListAlerts.ttps.description unknown The description of the TTP associated with the alert.
SekoiaXDR.ListAlerts.adversaries.type unknown The type of the adversary associated with the alert.
SekoiaXDR.ListAlerts.adversaries.name unknown The name of the adversary associated with the alert.
SekoiaXDR.ListAlerts.adversaries.id unknown The unique identifier of the adversary associated with the alert.
SekoiaXDR.ListAlerts.adversaries.description unknown The description of the adversary associated with the alert.
SekoiaXDR.ListAlerts.time_to_ingest unknown The time it took to ingest the alert.
SekoiaXDR.ListAlerts.target unknown The target of the alert.
SekoiaXDR.ListAlerts.time_to_resolve unknown The time it took to resolve the alert.
SekoiaXDR.ListAlerts.created_at unknown The time at which the alert was created.
SekoiaXDR.ListAlerts.last_seen_at unknown The time at which the alert was last seen.
SekoiaXDR.ListAlerts.assets unknown The assets associated with the alert.
SekoiaXDR.ListAlerts.rule.severity unknown The severity level of the rule that triggered the alert.
SekoiaXDR.ListAlerts.rule.type unknown The type of rule that triggered the alert.
SekoiaXDR.ListAlerts.rule.uuid unknown The unique identifier of the rule that triggered the alert.
SekoiaXDR.ListAlerts.rule.name unknown The name of the rule that triggered the alert.
SekoiaXDR.ListAlerts.rule.description unknown The description of the rule that triggered the alert.
SekoiaXDR.ListAlerts.rule.pattern unknown The pattern of the rule that triggered the alert.
SekoiaXDR.ListAlerts.similar unknown The number of similar alerts to this one.
SekoiaXDR.ListAlerts.status.name unknown The name of the status of the alert.
SekoiaXDR.ListAlerts.status.description unknown The description of the status of the alert.
SekoiaXDR.ListAlerts.status.uuid unknown The unique identifier of the status of the alert.
SekoiaXDR.ListAlerts.urgency.criticity unknown The level of criticity of the urgency of the alert.
SekoiaXDR.ListAlerts.urgency.current_value unknown The current value of the urgency of the alert.
SekoiaXDR.ListAlerts.urgency.severity unknown The severity level of the urgency of the alert.
SekoiaXDR.ListAlerts.urgency.display unknown The display of the urgency of the alert.
SekoiaXDR.ListAlerts.urgency.value unknown The value of the urgency of the alert.
SekoiaXDR.ListAlerts.created_by unknown The user who created the alert.
SekoiaXDR.ListAlerts.number_of_total_comments unknown The total number of comments on the alert.
SekoiaXDR.ListAlerts.time_to_detect unknown The time it took to detect the alert.
SekoiaXDR.ListAlerts.entity.name unknown The name of the entity associated with the alert.
SekoiaXDR.ListAlerts.entity.uuid unknown The unique identifier of the entity associated with the alert.
SekoiaXDR.ListAlerts.created_by_type unknown The type of user who created the alert.
SekoiaXDR.ListAlerts.details unknown The details of the alert.

sekoia-xdr-get-alert


Command to retrieve a specific alert by uuid or short_id from Sekoia XDR.

Base Command

sekoia-xdr-get-alert

Input

Argument Name Description Required
id The uuid or short_id of the alert to retrieve from sekoia-xdr-list-alerts or from sekoia plateform. i.e: “f5dcb81c-8d81-4332-9f1e-f119a1b31217” or “ALUnyZCYZ9Ga”. Required

Context Output

Path Type Description
SekoiaXDR.Alert.short_id string Short ID of the alert.
SekoiaXDR.Alert.title string Title of the alert.
SekoiaXDR.Alert.urgency string urgency of the alert.

sekoia-xdr-events-execute-query


Command to create an event search job on Sekoia XDR, after this execute “sekoia-xdr-status-events-query” to see the status of the query job and “sekoia-xdr-results-events-query” to retrieve the results..

Base Command

sekoia-xdr-events-execute-query

Input

Argument Name Description Required
earliest_time Valid formats <sign + or -><number><time unit> or ISO 8601 e.g -3d, -2w, -7d, 2023-01-15T00:00:00Z. Required
lastest_time Valid formats <sign + or -><number><time unit> or ISO 8601 e.g +3d, +2w, now, 2023-01-15T00:00:00Z. Required
query The query to use, i.e: “alert_short_ids:ALUnyZCYZ9Ga”. Optional
max_last_events Maximum number of listed events. Optional

Context Output

Path Type Description
SekoiaXDR.Events.Query.uuid unknown The unique identifier for the event.
SekoiaXDR.Events.Query.term unknown The search term associated with the event.
SekoiaXDR.Events.Query.started_at unknown The time at which the event started.
SekoiaXDR.Events.Query.short_histogram.cases unknown The number of cases associated with the event.
SekoiaXDR.Events.Query.short_histogram.total unknown The total number of events associated with the search term.
SekoiaXDR.Events.Query.short_histogram.alerts unknown The number of alerts associated with the event.
SekoiaXDR.Events.Query.short_histogram.earliest_time unknown The earliest time associated with the event.
SekoiaXDR.Events.Query.short_histogram.length unknown The length of the histogram for the event.
SekoiaXDR.Events.Query.created_by unknown The user who created the event.
SekoiaXDR.Events.Query.expired unknown A boolean indicating whether the event has expired.
SekoiaXDR.Events.Query.latest_time unknown The latest time associated with the event.
SekoiaXDR.Events.Query.expiration_date unknown The date on which the event will expire.
SekoiaXDR.Events.Query.created_at unknown The time at which the event was created.
SekoiaXDR.Events.Query.status unknown The status of the event.
SekoiaXDR.Events.Query.view_uuid unknown The unique identifier for the view associated with the event.
SekoiaXDR.Events.Query.canceled_at unknown The time at which the event was canceled.
SekoiaXDR.Events.Query.only_eternal unknown A boolean indicating whether the event is only eternal.
SekoiaXDR.Events.Query.results_ttl unknown The time-to-live for the event results.
SekoiaXDR.Events.Query.canceled_by unknown The user who canceled the event.
SekoiaXDR.Events.Query.term_lang unknown The language of the search term associated with the event.
SekoiaXDR.Events.Query.ended_at unknown The time at which the event ended.
SekoiaXDR.Events.Query.earliest_time unknown The earliest time associated with the event.
SekoiaXDR.Events.Query.max_last_events unknown The maximum number of events to include in the results.
SekoiaXDR.Events.Query.canceled_by_type unknown The type of the user who canceled the event.
SekoiaXDR.Events.Query.total unknown The total number of events associated with the event.
SekoiaXDR.Events.Query.created_by_type unknown The type of the user who created the event.
SekoiaXDR.Events.Query.community_uuids unknown The list of community UUIDs associated with the event.
SekoiaXDR.Events.Query.filters.field unknown The field associated with the filter.
SekoiaXDR.Events.Query.filters.value unknown The value associated with the filter.
SekoiaXDR.Events.Query.filters.operator unknown The operator used in the filter.
SekoiaXDR.Events.Query.filters.excluded unknown Indicates whether the filter is excluded or not.
SekoiaXDR.Events.Query.filters.disabled unknown Indicates whether the filter is disabled or not.

sekoia-xdr-events-status-query


Command to query the status of the search job on Sekoia XDR.

Base Command

sekoia-xdr-events-status-query

Input

Argument Name Description Required
uuid UUID of the query executed previously with the “sekoia-xdr-query-events” command. Required

Context Output

Path Type Description
SekoiaXDR.Events.Status.uuid unknown The unique identifier for the event.
SekoiaXDR.Events.Status.term unknown The term associated with the event.
SekoiaXDR.Events.Status.started_at unknown The time at which the event started.
SekoiaXDR.Events.Status.short_histogram.cases unknown The number of cases associated with the event.
SekoiaXDR.Events.Status.short_histogram.total unknown The total number of events associated with the event.
SekoiaXDR.Events.Status.short_histogram.alerts unknown The number of alerts associated with the event.
SekoiaXDR.Events.Status.short_histogram.earliest_time unknown The earliest time associated with the event.
SekoiaXDR.Events.Status.short_histogram.length unknown The length associated with the event.
SekoiaXDR.Events.Status.created_by unknown The user who created the event.
SekoiaXDR.Events.Status.expired unknown Whether the event is expired.
SekoiaXDR.Events.Status.latest_time unknown The latest time associated with the event.
SekoiaXDR.Events.Status.expiration_date unknown The date when the event expires.
SekoiaXDR.Events.Status.created_at unknown The time when the event was created.
SekoiaXDR.Events.Status.status unknown The current status of the event.
SekoiaXDR.Events.Status.view_uuid unknown The view associated with the event.
SekoiaXDR.Events.Status.canceled_at unknown The time when the event was canceled.
SekoiaXDR.Events.Status.only_eternal unknown Whether only eternal events are associated with the event.
SekoiaXDR.Events.Status.results_ttl unknown The time-to-live for the event results.
SekoiaXDR.Events.Status.canceled_by unknown The user who canceled the event.
SekoiaXDR.Events.Status.term_lang unknown The language associated with the term for the event.
SekoiaXDR.Events.Status.ended_at unknown The time when the event ended.
SekoiaXDR.Events.Status.earliest_time unknown The earliest time associated with the event.
SekoiaXDR.Events.Status.max_last_events unknown The maximum number of events to retrieve.
SekoiaXDR.Events.Status.canceled_by_type unknown The type of user who canceled the event.
SekoiaXDR.Events.Status.total unknown The total number of events associated with the event.
SekoiaXDR.Events.Status.created_by_type unknown The type of user who created the event.
SekoiaXDR.Events.Status.community_uuids[0] unknown The community associated with the event.
SekoiaXDR.Events.Status.filters.field unknown The field used for filtering events.
SekoiaXDR.Events.Status.filters.field unknown The field used in the filter.
SekoiaXDR.Events.Status.filters.value unknown The value of the filter.
SekoiaXDR.Events.Status.filters.operator unknown The operator used in the filter.
SekoiaXDR.Events.Status.filters.excluded unknown A boolean indicating whether the filter is excluded or not.
SekoiaXDR.Events.Status.filters.disabled unknown A boolean indicating whether the filter is disabled or not.

sekoia-xdr-events-results-query


Command to retrieve the events from the search job “sekoia-xdr-execute-events-query” previously done on Sekoia XDR.

Base Command

sekoia-xdr-events-results-query

Input

Argument Name Description Required
uuid UUID from response of the query executed previously with the “sekoia-xdr-query-events” command. Required

Context Output

Path Type Description
SekoiaXDR.Events.Results unknown The outputs are different for each event, they will be output inside SekoiaXDR.Events.Results.

sekoia-xdr-search-events


Command to search and retrieve the events from an alert. This is a combination of 3 commands: sekoia-xdr-events-execute-query, sekoia-xdr-events-status-query and sekoia-xdr-events-results-query.

Base Command

sekoia-xdr-search-events

Input

Argument Name Description Required
earliest_time Valid formats <sign + or -><number><time unit> or ISO 8601 e.g -3d, -2w, -7d, 2023-01-15T00:00:00Z. Required
lastest_time Valid formats <sign + or -><number><time unit> or ISO 8601 e.g +3d, +2w, now, 2023-01-15T00:00:00Z. Required
query The query to use, i.e: “alert_short_ids:ALUnyZCYZ9Ga”. Optional
max_last_events Maximum number of listed events. Optional
exclude_info Indicate if there is any information you want to exclude from the results of the events. i.e: original.message, message, agent.name, etc. These are the names of the headers presented in the table. If the header you want to exclude is not in the list write it and press enter. Possible values are: original.message, message, __event_id, agent.name, alert_short_ids, client.address, client.ip, client.user.id, customer.community_name, customer.community_uuid, customer.id, customer.intake_key, customer.intake_name, customer.intake_uuid, ecs.version, entity.id, entity.name, entity.uuid, event.created, event.dialect, event.dialect_uuid, event.id, event.outcome, http.request.method, http.request.referrer, related.ip, sekoiaio.activity.client.id, sekoiaio.activity.client.type, sekoiaio.customer.community_name, sekoiaio.customer.community_uuid, sekoiaio.customer.id, sekoiaio.entity.id, sekoiaio.entity.name, sekoiaio.entity.uuid, sekoiaio.intake.dialect, sekoiaio.intake.dialect_uuid, sekoiaio.intake.key, sekoiaio.intake.name, sekoiaio.intake.parsing_status, sekoiaio.intake.uuid, timestamp, url.domain, url.original, url.path, url.port, url.query, url.registered_domain, url.scheme, url.subdomain, url.top_level_domain, user_agent.original. Optional
job_uuid The job UUID to retrieve query results. Optional

Context Output

Path Type Description
SekoiaXDR.Events.Results unknown The outputs are different for each event, they will be output inside SekoiaXDR.Events.Results.

sekoia-xdr-update-status-alert


Command to update the status of a specific Alert by uuid or short_id.

Base Command

sekoia-xdr-update-status-alert

Input

Argument Name Description Required
id The uuid or short_id of the alert to retrieve from sekoia-xdr-list-alerts or from sekoia plateform. i.e: “f5dcb81c-8d81-4332-9f1e-f119a1b31217” or “ALUnyZCYZ9Ga”. Required
status The status you want to apply. (Acknowledged, Rejected, Ongoing, Closed)). Possible values are: Acknowledged, Rejected, Ongoing, Closed. Required
comment Comment to describe why the alert status has changed. Optional

Context Output

There is no context output for this command.

sekoia-xdr-post-comment-alert


Command to post comments to alerts in Sekoia XDR.

Base Command

sekoia-xdr-post-comment-alert

Input

Argument Name Description Required
id The uuid or short_id of the alert to retrieve from sekoia-xdr-list-alerts command. i.e: “f5dcb81c-8d81-4332-9f1e-f119a1b31217” or “ALUnyZCYZ9Ga”. Required
comment Content of the comment to be posted on the alert. Required
author Author of the comment. Optional

Context Output

There is no context output for this command.

sekoia-xdr-get-comments


Command to get all the comments from an alert in Sekoia XDR.

Base Command

sekoia-xdr-get-comments

Input

Argument Name Description Required
id The uuid or short_id of the alert to retrieve from sekoia-xdr-list-alerts command or from sekoia plateform. i.e: “f5dcb81c-8d81-4332-9f1e-f119a1b31217” or “ALUnyZCYZ9Ga”. Required

Context Output

Path Type Description
SekoiaXDR.Comments.total unknown The total number of items in the comments.
SekoiaXDR.Comments.items.date unknown The date at which the comment was created.
SekoiaXDR.Comments.items.created_by unknown The user who created the comment.
SekoiaXDR.Comments.items.uuid unknown The unique identifier for the comment.
SekoiaXDR.Comments.items.content unknown The content of the comment.
SekoiaXDR.Comments.items.created_by_type unknown The type of the user who created the comment.
SekoiaXDR.Comments.items.unseen unknown Indicates whether the comment has been seen by the user.
SekoiaXDR.Comments.items.author unknown The author of the comment.

sekoia-xdr-get-workflow-alert


Command to get the possible transitions of status on the alert.

Base Command

sekoia-xdr-get-workflow-alert

Input

Argument Name Description Required
id The uuid or short_id of the alert to retrieve from sekoia-xdr-list-alerts command or from sekoia plateform. i.e: “f5dcb81c-8d81-4332-9f1e-f119a1b31217” or “ALUnyZCYZ9Ga”. Required

Context Output

Path Type Description
SekoiaXDR.StatusTransitions.actions.name unknown The name of the action.
SekoiaXDR.StatusTransitions.actions.description unknown The description of the action.
SekoiaXDR.StatusTransitions.actions.id unknown The ID of the action.

sekoia-xdr-get-cases-alert


Command to retrieve the cases related to an Alert from Sekoia XDR. If a case_id is given, returns the information about it, and if not it will give all cases in this alert.

Base Command

sekoia-xdr-get-cases-alert

Input

Argument Name Description Required
alert_id The uuid or short_id of the alert to retrieve from sekoia-xdr-list-alerts command or from sekoia plateform. i.e: “f5dcb81c-8d81-4332-9f1e-f119a1b31217” or “ALUnyZCYZ9Ga”. Required
case_id The short_id of the case to retrieve from sekoia plateform or from this command without case_is param i.e: “CAQNurTJM8q2”. Optional

Context Output

Path Type Description
SekoiaXDR.Cases.updated_at unknown The time at which the case was updated.
SekoiaXDR.Cases.updated_by unknown The user who updated the case.
SekoiaXDR.Cases.uuid unknown The unique identifier of the case.
SekoiaXDR.Cases.title unknown The title of the case.
SekoiaXDR.Cases.priority unknown The priority level of the case.
SekoiaXDR.Cases.short_id unknown The short ID of the case.
SekoiaXDR.Cases.community_uuid unknown The unique identifier of the community to which the case belongs.
SekoiaXDR.Cases.updated_by_type unknown The type of user who updated the case.
SekoiaXDR.Cases.first_seen_at unknown The time at which the case was first seen.
SekoiaXDR.Cases.number_of_comments unknown The number of comments on the case.
SekoiaXDR.Cases.alerts.updated_at unknown The time at which the alert was updated.
SekoiaXDR.Cases.alerts.updated_by unknown The user who updated the alert.
SekoiaXDR.Cases.alerts.uuid unknown The unique identifier of the alert.
SekoiaXDR.Cases.alerts.title unknown The title of the alert.
SekoiaXDR.Cases.alerts.time_to_respond unknown The time it took to respond to the alert.
SekoiaXDR.Cases.alerts.short_id unknown The short ID of the alert.
SekoiaXDR.Cases.alerts.community_uuid unknown The unique identifier of the community to which the alert belongs.
SekoiaXDR.Cases.alerts.kill_chain_short_id unknown The short ID of the kill chain.
SekoiaXDR.Cases.alerts.number_of_unseen_comments unknown The number of unseen comments on the alert.
SekoiaXDR.Cases.alerts.updated_by_type unknown The type of user who updated the alert.
SekoiaXDR.Cases.alerts.source unknown The source of the alert.
SekoiaXDR.Cases.alerts.alert_type.value unknown The type of the alert.
SekoiaXDR.Cases.alerts.alert_type.category unknown The category type of the alert.
SekoiaXDR.Cases.alerts.time_to_acknowledge unknown The time it took to acknowledge the alert.
SekoiaXDR.Cases.alerts.stix unknown The STIX data of the alert.
SekoiaXDR.Cases.alerts.first_seen_at unknown The time at which the alert was first seen.
SekoiaXDR.Cases.alerts.ttps.type unknown The type of TTP associated with the alert.
SekoiaXDR.Cases.alerts.ttps.name unknown The name of the TTP associated with the alert.
SekoiaXDR.Cases.alerts.ttps.id unknown The ID of the TTP associated with the alert.
SekoiaXDR.Cases.alerts.ttps.description unknown The description of the TTP associated with the alert.
SekoiaXDR.Cases.alerts.adversaries.type unknown The type of adversary associated with the alert.
SekoiaXDR.Cases.alerts.adversaries.name unknown The name of the adversary associated with the alert.
SekoiaXDR.Cases.alerts.adversaries.id unknown The ID of the adversary associated with the alert.
SekoiaXDR.Cases.alerts.adversaries.description unknown The description of the adversary associated with the alert.
SekoiaXDR.Cases.alerts.time_to_ingest unknown The time it took to ingest the alert.
SekoiaXDR.Cases.alerts.target unknown The target of the alert.
SekoiaXDR.Cases.alerts.time_to_resolve unknown The time it took to resolve the alert.
SekoiaXDR.Cases.alerts.created_at unknown The time at which the alert was created.
SekoiaXDR.Cases.alerts.last_seen_at unknown The time at which the alert was last seen.
SekoiaXDR.Cases.alerts.assets unknown The assets associated with the alert.
SekoiaXDR.Cases.alerts.rule.severity unknown The severity level of the rule associated with the alert.
SekoiaXDR.Cases.alerts.rule.type unknown The type of rule associated with the alert.
SekoiaXDR.Cases.alerts.rule.uuid unknown The unique identifier of the rule associated with the alert.
SekoiaXDR.Cases.alerts.rule.name unknown The name of the rule associated with the alert.
SekoiaXDR.Cases.alerts.rule.description unknown The description of the rule associated with the alert.
SekoiaXDR.Cases.alerts.rule.pattern unknown The pattern of the rule associated with the alert.
SekoiaXDR.Cases.alerts.similar unknown The number of similar alerts.
SekoiaXDR.Cases.alerts.status.name unknown The name of the status of the alert.
SekoiaXDR.Cases.alerts.status.description unknown The description of the status of the alert.
SekoiaXDR.Cases.alerts.status.uuid unknown The unique identifier of the status of the alert.
SekoiaXDR.Cases.alerts.urgency.criticity unknown The level of criticality of the urgency of the alert.
SekoiaXDR.Cases.alerts.urgency.current_value unknown The current value of the urgency of the alert.
SekoiaXDR.Cases.alerts.urgency.severity unknown The severity level of the urgency of the alert.
SekoiaXDR.Cases.alerts.urgency.display unknown The display value of the urgency of the alert.
SekoiaXDR.Cases.alerts.urgency.value unknown The value of the urgency of the alert.
SekoiaXDR.Cases.alerts.created_by unknown The user who created the alert.
SekoiaXDR.Cases.alerts.number_of_total_comments unknown The total number of comments on the alert.
SekoiaXDR.Cases.alerts.time_to_detect unknown The time it took to detect the alert.
SekoiaXDR.Cases.alerts.entity.name unknown The name of the entity associated with the alert.
SekoiaXDR.Cases.alerts.entity.uuid unknown The unique identifier of the entity associated with the alert.
SekoiaXDR.Cases.alerts.created_by_type unknown The type of user who created the alert.
SekoiaXDR.Cases.alerts.details unknown The details of the alert.
SekoiaXDR.Cases.number_of_alerts unknown The number of alerts in the case.
SekoiaXDR.Cases.created_at unknown The time at which the case was created.
SekoiaXDR.Cases.last_seen_at unknown The time at which the case was last seen.
SekoiaXDR.Cases.status unknown The status of the case.
SekoiaXDR.Cases.description unknown The description of the case.
SekoiaXDR.Cases.status_uuid unknown The unique identifier of the status of the case.
SekoiaXDR.Cases.created_by unknown The user who created the case.
SekoiaXDR.Cases.tags unknown The tags associated with the case.
SekoiaXDR.Cases.created_by_type unknown The type of user who created the case.
SekoiaXDR.Cases.subscribers.avatar_uuid unknown The unique identifier of the avatar of the subscriber.
SekoiaXDR.Cases.subscribers.type unknown The type of subscriber.

sekoia-xdr-get-asset


Get an asset by its UUID from Sekoia XDR.

Base Command

sekoia-xdr-get-asset

Input

Argument Name Description Required
asset_uuid UUID of the asset to get, the UUID should appear with “sekoia-xdr-list-assets” if that alert have assets related, example: “d4cc3b05-a78d-4f29-b27c-c637d86fa03a”. Required

Context Output

Path Type Description
SekoiaXDR.Asset.owners unknown The owners of the asset.
SekoiaXDR.Asset.category.types.description unknown The description of the category type of the asset.
SekoiaXDR.Asset.category.types.name unknown The name of the category type of the asset.
SekoiaXDR.Asset.category.types.uuid unknown The UUID of the category type of the asset.
SekoiaXDR.Asset.category.description unknown The description of the category of the asset.
SekoiaXDR.Asset.category.name unknown The name of the category of the asset.
SekoiaXDR.Asset.category.uuid unknown The UUID of the category of the asset.
SekoiaXDR.Asset.created_at unknown The time at which the asset was created.
SekoiaXDR.Asset.keys.value unknown The value of the keys of the asset.
SekoiaXDR.Asset.keys.name unknown The name of the keys of the asset.
SekoiaXDR.Asset.keys.uuid unknown The UUID of the keys of the asset.
SekoiaXDR.Asset.attributes.value unknown The value of the attributes of the asset.
SekoiaXDR.Asset.attributes.name unknown The name of the attributes of the asset.
SekoiaXDR.Asset.attributes.uuid unknown The UUID of the attributes of the asset.
SekoiaXDR.Asset.updated_at unknown The time at which the asset was updated.
SekoiaXDR.Asset.asset_type.description unknown The description of the asset type.
SekoiaXDR.Asset.asset_type.name unknown The name of the asset type.
SekoiaXDR.Asset.asset_type.uuid unknown The UUID of the asset type.
SekoiaXDR.Asset.criticity.value unknown The criticality value of the asset.
SekoiaXDR.Asset.criticity.display unknown The display value of the criticality of the asset.
SekoiaXDR.Asset.description unknown The description of the asset.
SekoiaXDR.Asset.community_uuid unknown The UUID of the community of the asset.
SekoiaXDR.Asset.name unknown The name of the asset.
SekoiaXDR.Asset.uuid unknown The UUID of the asset.

get-remote-data


This command gets new information about the incidents in the remote system and updates existing incidents in Cortex XSOAR.

Base Command

get-remote-data

Input

Argument Name Description Required
id The remote alert ID. Optional
lastUpdate ISO format date with timezone, e.g., 2023-03-01T16:41:30.589575+02:00. The incident is only updated if it was modified after the last update time. Default is 0. Optional

Context Output

There is no context output for this command.

get-modified-remote-data


available from Cortex XSOAR version 6.1.0. This command queries for incidents that were modified since the last update.

Base Command

get-modified-remote-data

Input

Argument Name Description Required
lastUpdate ISO format date with timezone, e.g., 2023-03-01T16:41:30.589575+02:00. The incident is only returned if it was modified after the last update time. Default is 0. Optional

Context Output

There is no context output for this command.

get-mapping-fields


This command pulls the remote schema for the different incident types, and their associated incident fields, from the remote system.

Base Command

get-mapping-fields

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

There is no context output for this command.

sekoia-xdr-list-assets


Command to retrieve a list of Assets from Sekoia XDR.

Base Command

sekoia-xdr-list-assets

Input

Argument Name Description Required
limit Limit a number of items. Default is 10. Optional
assets_type Type of assets to list (computer, network, etc). Optional

Context Output

Path Type Description
SekoiaXDR.Assets.total unknown The total number of items in the response.
SekoiaXDR.Assets.items.0.owners.0 unknown The ID of the owner of the asset.
SekoiaXDR.Assets.items.0.category.types.0.description unknown The description of the type of the asset category.
SekoiaXDR.Assets.items.0.category.types.0.name unknown The name of the type of the asset category.
SekoiaXDR.Assets.items.0.category.types.0.uuid unknown The UUID of the type of the asset category.
SekoiaXDR.Assets.items.0.category.description unknown The description of the asset category.
SekoiaXDR.Assets.items.0.category.name unknown The name of the asset category.
SekoiaXDR.Assets.items.0.category.uuid unknown The UUID of the asset category.
SekoiaXDR.Assets.items.0.created_at unknown The time at which the asset was created.
SekoiaXDR.Assets.items.0.keys.0.value unknown The value of the asset key.
SekoiaXDR.Assets.items.0.keys.0.name unknown The name of the asset key.
SekoiaXDR.Assets.items.0.keys.0.uuid unknown The UUID of the asset key.
SekoiaXDR.Assets.items.0.attributes.0.value unknown The value of the asset attribute.
SekoiaXDR.Assets.items.0.attributes.0.name unknown The name of the asset attribute.
SekoiaXDR.Assets.items.0.attributes.0.uuid unknown The UUID of the asset attribute.
SekoiaXDR.Assets.items.0.updated_at unknown The time at which the asset was last updated.
SekoiaXDR.Assets.items.0.asset_type.description unknown The description of the asset type.
SekoiaXDR.Assets.items.0.asset_type.name unknown The name of the asset type.
SekoiaXDR.Assets.items.0.asset_type.uuid unknown The UUID of the asset type.
SekoiaXDR.Assets.items.0.criticity.value unknown The numeric value of the asset criticality.
SekoiaXDR.Assets.items.0.criticity.display unknown The display value of the asset criticality.
SekoiaXDR.Assets.items.0.description unknown The description of the asset.
SekoiaXDR.Assets.items.0.community_uuid unknown The UUID of the community to which the asset belongs.
SekoiaXDR.Assets.items.0.name unknown The name of the asset.
SekoiaXDR.Assets.items.0.uuid unknown The UUID of the asset.

sekoia-xdr-get-user


Command to get information about a user in Sekoia XDR. Used also in the command !sekoia-xdr-get-comments to have the name of the persons who made the comments.

Base Command

sekoia-xdr-get-user

Input

Argument Name Description Required
user_uuid UUID of the user, you get it from sekoia-xdr-get-comments for example. But make sure that created_by_type field is user. Required

Context Output

Path Type Description
SekoiaXDR.User.creator.description unknown The description of the creator.
SekoiaXDR.User.creator.name unknown The name of the creator.
SekoiaXDR.User.creator.uuid unknown The UUID of the creator.
SekoiaXDR.User.updated_at unknown The time at which the object was last updated.
SekoiaXDR.User.created_by unknown The UUID of the user who created the object.
SekoiaXDR.User.total_members unknown The total number of members in the community.
SekoiaXDR.User.subcommunities unknown The UUIDs of the subcommunities.
SekoiaXDR.User.parent_community_uuid unknown The UUID of the parent community.
SekoiaXDR.User.applications.description unknown The description of the application.
SekoiaXDR.User.applications.name unknown The name of the application.
SekoiaXDR.User.applications.uuid unknown The UUID of the application.
SekoiaXDR.User.is_parent unknown Whether the community is a parent community.
SekoiaXDR.User.name unknown The name of the community.
SekoiaXDR.User.members.uuid unknown The UUID of the member.
SekoiaXDR.User.members.created_at unknown The time at which the member was created.
SekoiaXDR.User.members.user.firstname unknown The first name of the user associated with the member.
SekoiaXDR.User.members.user.mfa_enabled unknown Whether multi-factor authentication is enabled for the user associated with the member.
SekoiaXDR.User.members.user.uuid unknown The UUID of the user associated with the member.
SekoiaXDR.User.members.user.company_name unknown The company name of the user associated with the member.
SekoiaXDR.User.members.user.lastname unknown The last name of the user associated with the member.
SekoiaXDR.User.members.user.created_at unknown The time at which the user associated with the member was created.
SekoiaXDR.User.members.user.picture_mode unknown The picture mode of the user associated with the member.
SekoiaXDR.User.members.user.last_activity unknown The last activity time of the user associated with the member.
SekoiaXDR.User.members.user.updated_at unknown The time at which the user associated with the member was last updated.
SekoiaXDR.User.members.user.auth_provider unknown The authentication provider of the user associated with the member.
SekoiaXDR.User.members.user.email unknown The email address of the user associated with the member.
SekoiaXDR.User.members.user.invitation_v2.email unknown The email address for the invitation associated with the user associated with the member.
SekoiaXDR.User.members.name unknown The name of the member.
SekoiaXDR.User.members.updated_at unknown The time at which the member was last updated.
SekoiaXDR.User.members.status_changed_at unknown The time at which the status of the member was last changed.
SekoiaXDR.User.members.status unknown The status of the member.
SekoiaXDR.User.session_timeout unknown The session timeout for the community.
SekoiaXDR.User.is_mfa_enforced unknown Whether multi-factor authentication is enforced for the community.
SekoiaXDR.User.uuid unknown The UUID of the community.
SekoiaXDR.User.created_at unknown The time at which the community was created.
SekoiaXDR.User.picture_mode unknown The picture mode for the community.
SekoiaXDR.User.homepage_url unknown The homepage URL for the community.
SekoiaXDR.User.created_by_type unknown The type of the user who created the community.
SekoiaXDR.User.disable_inactive_avatars unknown Whether inactive avatars are disabled for the community.
SekoiaXDR.User.description unknown The description of the community.

sekoia-xdr-add-attributes-asset


Command to add attributes to an asset in Sekoia XDR.

Base Command

sekoia-xdr-add-attributes-asset

Input

Argument Name Description Required
asset_uuid UUID of the asset to get, the UUID should appear with “sekoia-xdr-list-assets” if that alert have assets related, example: “d4cc3b05-a78d-4f29-b27c-c637d86fa03a”. Required
name The name of attributes. Required
value The value of attributes. Required

Context Output

There is no context output for this command.

sekoia-xdr-add-keys-asset


Command to add keys to an asset in Sekoia XDR.

Base Command

sekoia-xdr-add-keys-asset

Input

Argument Name Description Required
asset_uuid UUID of the asset to get, the UUID should appear with “sekoia-xdr-list-assets” if that alert have assets related, example: “d4cc3b05-a78d-4f29-b27c-c637d86fa03a”. Required
name The name of the key to be added. Required
value The value of the key to be added. Required

Context Output

There is no context output for this command.

sekoia-xdr-get-kill-chain


Command to retrieve the definition of a Cyber Kill Chain Step.

Base Command

sekoia-xdr-get-kill-chain

Input

Argument Name Description Required
kill_chain_uuid UUID or short_id of the kill chain the UUID should appear with “sekoia-xdr-list-alerts”. Required

Context Output

Path Type Description
SekoiaXDR.KillChain.stix_name unknown The name of the STIX object.
SekoiaXDR.KillChain.description unknown The description of the STIX object.
SekoiaXDR.KillChain.name unknown The common name of the STIX object.
SekoiaXDR.KillChain.uuid unknown The unique identifier of the STIX object.
SekoiaXDR.KillChain.short_id unknown The short identifier of the STIX object.
SekoiaXDR.KillChain.order_id unknown The order identifier of the STIX object.

sekoia-xdr-remove-attribute-asset


Command to remove an attribute from an asset in Sekoia XDR. Note: use !sekoia-xdr-get-asset to find the attribute_uuid to delete.

Base Command

sekoia-xdr-remove-attribute-asset

Input

Argument Name Description Required
asset_uuid UUID of the asset, the UUID should appear with “sekoia-xdr-list-assets” if that alert have assets related, example: “d4cc3b05-a78d-4f29-b27c-c637d86fa03a”. Required
attribute_uuid UUID of the attribute to delete. Note: use !sekoia-xdr-get-asset to find the attribute_uuid to delete. Required

Context Output

There is no context output for this command.

sekoia-xdr-remove-key-asset


Command to remove a key from an asset in Sekoia XDR. Note: use !sekoia-xdr-get-asset to find the key_uuid to delete.

Base Command

sekoia-xdr-remove-key-asset

Input

Argument Name Description Required
asset_uuid UUID of the asset, the UUID should appear with “sekoia-xdr-list-assets” if that alert have assets related, example: “d4cc3b05-a78d-4f29-b27c-c637d86fa03a”. Required
key_uuid UUID of the key to remove. Note: use !sekoia-xdr-get-asset to find the key_uuid to delete. Required

Context Output

There is no context output for this command.

sekoia-xdr-http-request


Command that performs a HTTP request to Sekoia using the integration authentication configured.

Base Command

sekoia-xdr-http-request

Input

Argument Name Description Required
method Method to use with the http request (GET,POST,etc). Default is GET. Required
url_sufix The URL suffix after https://api.sekoia.io, i.e. /v1/sic/alerts/ or /v1/asset-management/assets/. Required
parameters Query parameters, i.e. limit -> 10 , match[‘status_name’] -> Ongoing. Optional
data Body of the request Optional

Context Output

There is no context output for this command.

Incident Mirroring

You can enable incident mirroring between Cortex XSOAR incidents and Sekoia XDR corresponding events (available from Cortex XSOAR version 6.0.0).
To set up the mirroring:

  1. Enable Fetching incidents in your instance configuration.
  2. In the Mirroring Direction integration parameter, select in which direction the incidents should be mirrored:

    Option Description
    None Turns off incident mirroring.
    Incoming Any changes in Sekoia XDR events (mirroring incoming fields) will be reflected in Cortex XSOAR incidents.
    Outgoing Any changes in Cortex XSOAR incidents will be reflected in Sekoia XDR events (outgoing mirrored fields).
    Incoming and Outgoing Changes made in Sekoia will be reflected in Cortex, and vice versa, ensuring status updates are synchronized between both systems.
  3. Optional: Check the Close Mirrored XSOAR Incident integration parameter to close the Cortex XSOAR incident when the corresponding event is closed in Sekoia XDR.

  4. Optional: Check the Reopen Mirrored Cortex XSOAR Incidents integration parameter to reopen the Cortex XSOAR incident when the matching Sekoia XDR alert is reopened.

Newly fetched incidents will be mirrored in the chosen direction. However, this selection does not affect existing incidents.
Important Note: To ensure the mirroring works as expected, mappers are required, both for incoming and outgoing, to map the expected fields in Cortex XSOAR and Sekoia XDR.

Troubleshooting

To troubleshoot possible issues with the SEKOIA XDR integration, consider the following steps:

  • Debug Mode:
    • In your integration instance, enable the Debug option.
    • Navigate to Settings > About > Troubleshooting > Download logs to download the logs. Analyzing these logs can provide valuable insights into any issues.
  • Mirror Values:
    • To diagnose mirroring issues beyond what debug mode offers, you can inspect specific fields in the context data. Check if the following dbot fields are set:
      • dbotMirrorInstance: Indicates the instance managing the mirroring.
      • dbotMirrorDirection: Shows the direction of mirroring.
      • dbotMirrorId: The unique identifier for the mirroring process.
    • If these fields are not set, review the mappers to ensure that they are configured correctly.
  • dbotMirrorLastSync Field:
    • The dbotMirrorLastSync field in the context data will update when the mirroring process updates an incident.
    • You can observe these updates in the War Room as well, which will provide a log of the mirroring activity.

By following these troubleshooting steps, you can effectively diagnose and resolve issues within the SEKOIA XDR integration.

Best Practices

To make the most out of your SEKOIA XDR integration, consider the following best practices:

  • Mirroring Changes: When mirroring is enabled, please allow at least 1 minute for changes to be reflected. The mirroring process runs every 1 minute, ensuring that data between SEKOIA and Cortex is kept in sync.

  • Handling Reopened Incidents: If you have enabled the reopening option, the Cortex incident will be reopened under two specific conditions:

    • Reopened Alert in SEKOIA: If an alert is reopened in SEKOIA, the corresponding incident in Cortex will also be reopened. This ensures that the incident tracking is consistent across both platforms.
    • Reopened Incident in Cortex: If you reopen an incident directly in Cortex, you need to be cautious. After reopening the incident in Cortex, you should promptly change the status of the SEKOIA alert. Failing to do so might lead to the incident being automatically closed by the mirroring process.

By adhering to these best practices, you can ensure a smoother and more effective synchronization between SEKOIA and your incident management platform.

Additional documentation

The following documentation can be useful to understand the integration:

Information Description
Mirroring Adittional information for mirroring
Post process scripts Adittional information for post process scripts
Sekoia XDR documentation Sekoia XDR Documentation
Rest API Documentation Sekoia XDR API Documentation

Configuration parameters

  • credentials — API key (required)
  • url — Server URL (i.e. https://api.sekoia.io) (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetch — Fetch incidents
  • incidentType — Incident type
  • first_fetch — First fetch timestamp (<sign + or -><number><time unit>, e.g., -7d, -1h) (required)
  • max_fetch — Maximum incidents to fetch per interval. (required)
  • incidentFetchInterval — Incidents Fetch Interval
  • alerts_status — Alerts status.
  • alerts_type — Alerts types.
  • alerts_urgency — Alerts urgency levels ( "MINurgency,MAXurgency". i.e: 80,100 ).
  • fetch_mode — Fetch mode (required)
  • replace_dots_event — Replace "dots" in event field names with another character. (required)
  • exclude_info_events — Events fields to exclude from the events search result.
  • fetch_with_assets — Include assets information in the alerts when fetching.
  • fetch_with_kill_chain — Include kill chain information in the alerts when fetching.
  • mirror_direction — Incident Mirroring Direction.
  • mirror_events — Include events in the mirroring of the alerts.
  • mirror_kill_chain — Include kill chain information in the mirroring of the alerts.
  • reopen_incident — Reopen Mirrored Cortex XSOAR Incidents (Incoming Mirroring)
  • close_incident — Close Mirrored Cortex XSOAR Incidents (Incoming Mirroring)
  • close_notes — Close notes. (required)
  • timezone — Timezone ( TZ format ) (required)

Commands (23)

  • get-mapping-fields

    This command pulls the remote schema for the different incident types, and their associated incident fields, from the remote system.

  • get-modified-remote-data

    available from Cortex XSOAR version 6.1.0. This command queries for incidents that were modified since the last update.

  • get-remote-data

    This command gets new information about the incidents in the remote system and updates existing incidents in Cortex XSOAR.

  • sekoia-xdr-add-attributes-asset

    Command to add attributes to an asset in Sekoia XDR.

  • sekoia-xdr-add-keys-asset

    Command to add keys to an asset in Sekoia XDR.

  • sekoia-xdr-events-execute-query

    Command to create an event search job on Sekoia XDR, after this execute "sekoia-xdr-status-events-query" to see the status of the query job and "sekoia-xdr-results-events-query" to retrieve the results..

  • sekoia-xdr-events-results-query

    Command to retrieve the events from the search job "sekoia-xdr-execute-events-query" previously done on Sekoia XDR.

  • sekoia-xdr-events-status-query

    Command to query the status of the search job on Sekoia XDR.

  • sekoia-xdr-get-alert

    Command to retrieve a specific alert by uuid or short_id from Sekoia XDR.

  • sekoia-xdr-get-asset

    Get an asset by its UUID from Sekoia XDR.

  • sekoia-xdr-get-cases-alert

    Command to retrieve the cases related to an Alert from Sekoia XDR. If a case_id is given, returns the information about it, and if not it will give all cases in this alert.

  • sekoia-xdr-get-comments

    Command to get all the comments from an alert in Sekoia XDR.

  • sekoia-xdr-get-kill-chain

    Command to retrieve the definition of a Cyber Kill Chain Step.

  • sekoia-xdr-get-user

    Command to get information about a user in Sekoia XDR. Used also in the command !sekoia-xdr-get-comments to have the name of the persons who made the comments.

  • sekoia-xdr-get-workflow-alert

    Command to get the possible transitions of status on the alert.

  • sekoia-xdr-http-request

    Command that performs a HTTP request to Sekoia using the integration authentication configured.

  • sekoia-xdr-list-alerts

    Command to retrieve a list of Alerts from Sekoia XDR.

  • sekoia-xdr-list-assets

    Command to retrieve a list of Assets from Sekoia XDR.

  • sekoia-xdr-post-comment-alert

    Command to post comments to alerts in Sekoia XDR.

  • sekoia-xdr-remove-attribute-asset

    Command to remove an attribute from an asset in Sekoia XDR. Note: use !sekoia-xdr-get-asset to find the attribute_uuid to delete.

  • sekoia-xdr-remove-key-asset

    Command to remove a key from an asset in Sekoia XDR. Note: use !sekoia-xdr-get-asset to find the key_uuid to delete.

  • sekoia-xdr-search-events

    Command to search and retrieve the events from an alert. This is a combination of 3 commands: sekoia-xdr-events-execute-query, sekoia-xdr-events-status-query and sekoia-xdr-events-results-query.

  • sekoia-xdr-update-status-alert

    Command to update the status of a specific Alert by uuid or short_id.

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

import json
import urllib3
import dateparser  # type: ignore
from typing import Any, cast
from datetime import datetime, timedelta
import re
import pytz  # type: ignore

# Disable insecure warnings
urllib3.disable_warnings()


""" CONSTANTS """

DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"
INTERVAL_SECONDS_EVENTS = 1
TIMEOUT_EVENTS = 30
INCIDENT_TYPE_NAME = "Sekoia XDR"
# Retry configuration for transient Sekoia API errors (e.g. 502 Bad Gateway)
API_RETRIES = 3
API_BACKOFF_FACTOR = 1
API_STATUS_LIST_TO_RETRY = [429, 500, 502, 503, 504]
SEKOIA_INCIDENT_FIELDS = {
    "short_id": "The ID of the alert to edit",
    "status": "The name of the status.",
}

STATUS_TRANSITIONS = {
    "Ongoing": "Validate",
    "Acknowledged": "Acknowledge",
    "Rejected": "Reject",
    "Closed": "Close",
}

MIRROR_DIRECTION = {
    "None": None,
    "Incoming": "In",
    "Outgoing": None,
    "Incoming and Outgoing": "In",
}


""" CLIENT CLASS """


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

    def _http_request(self, *args, **kwargs) -> Any:  # type: ignore[override]
        """
        Wraps BaseClient._http_request to add default retry behavior on transient
        errors (e.g. 502 Bad Gateway) returned by the Sekoia API. Explicit retry
        arguments passed by a caller take precedence over these defaults.
        """
        kwargs.setdefault("retries", API_RETRIES)
        kwargs.setdefault("status_list_to_retry", API_STATUS_LIST_TO_RETRY)
        kwargs.setdefault("backoff_factor", API_BACKOFF_FACTOR)
        return super()._http_request(*args, **kwargs)

    def get_validate_resource(self) -> str:
        """
        Request Sekoia.io to validate the API Key
        """
        try:
            self._http_request(
                method="GET",
                url_suffix="/v1/auth/validate",
                raise_on_status=True,
            )
            return "ok"
        except DemistoException as e:
            raise DemistoException(f"Integration error: the request failed due to: {e}")

    def list_alerts(
        self,
        alerts_limit: int | None,
        alerts_status: str | None,
        alerts_created_at: str | None,
        alerts_updated_at: str | None,
        alerts_urgency: str | None,
        alerts_type: str | None,
        sort_by: str | None,
        offset: int | None = 0,
        direction: str = "asc",
    ) -> dict[str, Any]:
        request_params: dict[str, Any] = {}

        """ Normal parameters"""
        request_params["offset"] = offset
        if alerts_limit:
            request_params["limit"] = alerts_limit

        """ Matching parameters"""
        if alerts_status:
            request_params["match[status_name]"] = alerts_status
        if alerts_created_at:
            request_params["date[created_at]"] = alerts_created_at
        if alerts_updated_at:
            request_params["date[updated_at]"] = alerts_updated_at
        if alerts_urgency:
            request_params["range[urgency]"] = alerts_urgency
        if alerts_type:
            request_params["match[type_value]"] = alerts_type

        """ Sorting parameters"""
        if sort_by:
            request_params["sort"] = sort_by

        request_params["direction"] = direction

        return self._http_request(method="GET", url_suffix="/v1/sic/alerts", params=request_params)

    def get_alert(self, alert_uuid: str) -> dict[str, Any]:
        return self._http_request(method="GET", url_suffix=f"/v1/sic/alerts/{alert_uuid}")

    def update_status_alert(self, alert_uuid: str, action_uuid: str, comment: str | None) -> dict[str, Any]:
        request_params: dict[str, Any] = {"action_uuid": action_uuid}

        """ Normal parameters"""
        if comment:
            request_params["comment"] = comment

        return self._http_request(
            method="PATCH",
            url_suffix=f"/v1/sic/alerts/{alert_uuid}/workflow",
            json_data=request_params,
        )

    def post_comment_alert(self, alert_uuid: str, content: str, author: str | None) -> dict[str, Any]:
        request_params: dict[str, Any] = {"content": content}

        """ Normal parameters"""
        if author:
            request_params["author"] = author

        return self._http_request(
            method="POST",
            url_suffix=f"/v1/sic/alerts/{alert_uuid}/comments",
            json_data=request_params,
        )

    def get_comments_alert(self, alert_uuid: str) -> dict[str, Any]:
        return self._http_request(
            method="GET",
            url_suffix=f"/v1/sic/alerts/{alert_uuid}/comments",
        )

    def get_workflow_alert(self, alert_uuid: str) -> dict[str, Any]:
        return self._http_request(
            method="GET",
            url_suffix=f"/v1/sic/alerts/{alert_uuid}/workflow",
        )

    def query_events(
        self,
        events_earliest_time: str,
        events_latest_time: str,
        events_term: str,
        max_last_events: str | None,
    ) -> dict[str, Any]:
        request_params: dict[str, Any] = {
            "earliest_time": events_earliest_time,
            "latest_time": events_latest_time,
            "term": events_term,
        }

        """ Normal parameters"""
        if max_last_events:
            request_params["max_last_events"] = max_last_events

        return self._http_request(
            method="POST",
            url_suffix="/v1/sic/conf/events/search/jobs",
            json_data=request_params,
        )

    def query_events_status(self, event_search_job_uuid: str) -> dict[str, Any]:
        return self._http_request(
            method="GET",
            url_suffix=f"/v1/sic/conf/events/search/jobs/{event_search_job_uuid}",
        )

    def retrieve_events(self, event_search_job_uuid: str) -> dict[str, Any]:
        return self._http_request(
            method="GET",
            url_suffix=f"/v1/sic/conf/events/search/jobs/{event_search_job_uuid}/events",
        )

    def get_cases_alert(self, alert_uuid: str, case_id: str | None) -> dict[str, Any]:
        request_params: dict[str, Any] = {"match[alert_uuid]": alert_uuid}

        """ Matching parameters"""
        if case_id:
            request_params["match[short_id]"] = case_id

        return self._http_request(method="GET", url_suffix="v1/sic/cases", params=request_params)

    def get_asset(self, asset_uuid: str) -> dict[str, Any]:
        return self._http_request(
            method="GET",
            url_suffix=f"/v1/asset-management/assets/{asset_uuid}",
        )

    def list_asset(self, limit: str | None, assets_type: str | None) -> dict[str, Any]:
        request_params: dict[str, Any] = {}

        """ Normal parameters"""
        if limit:
            request_params["limit"] = limit

        """ Matching parameters"""
        if assets_type:
            request_params["match[type_name]"] = assets_type

        return self._http_request(
            method="GET",
            url_suffix="/v1/asset-management/assets",
            params=request_params,
        )

    def add_attributes_asset(self, asset_uuid: str, name: str, value: str) -> dict[str, Any]:
        request_params: dict[str, Any] = {"name": name, "value": value}

        return self._http_request(
            method="POST",
            url_suffix=f"/v1/asset-management/assets/{asset_uuid}/attr",
            params=request_params,
        )

    def add_keys_asset(self, asset_uuid: str, name: str, value: str) -> dict[str, Any]:
        request_params: dict[str, Any] = {"name": name, "value": value}

        return self._http_request(
            method="POST",
            url_suffix=f"/v1/asset-management/assets/{asset_uuid}/keys",
            params=request_params,
        )

    def remove_attribute_asset(self, asset_uuid: str, attribute_uuid: str) -> list[dict[str, Any]]:
        return self._http_request(
            method="DELETE",
            url_suffix=f"/v1/asset-management/assets/{asset_uuid}/attr/{attribute_uuid}",
            resp_type="text",
        )

    def remove_key_asset(self, asset_uuid: str, key_uuid: str) -> dict[str, Any]:
        return self._http_request(
            method="DELETE",
            url_suffix=f"/v1/asset-management/assets/{asset_uuid}/keys/{key_uuid}",
            resp_type="text",
        )

    def get_user(self, user_uuid: str) -> dict[str, Any]:
        return self._http_request(method="GET", url_suffix=f"/v1/users/{user_uuid}")

    def get_kill_chain(self, kill_chain_uuid: str) -> dict[str, Any]:
        return self._http_request(method="GET", url_suffix=f"/v1/sic/kill-chains/{kill_chain_uuid}")

    def http_request(self, method: str, url_suffix: str, params: dict, data: dict | None) -> dict[str, Any]:
        if not params:
            params = {}

        if data and method in ["POST", "PUT", "PATCH"]:
            # If data is provided, we use json_data to send it as JSON
            return self._http_request(method=method, url_suffix=url_suffix, params=params, data=data)

        return self._http_request(method=method, url_suffix=url_suffix, params=params)


""" HELPER FUNCTIONS """


def arg_to_timestamp(arg: Any, arg_name: str, required: bool = False) -> int:
    """
    Converts an XSOAR argument to a timestamp (seconds from epoch).
    This function is used to quickly validate an argument provided to XSOAR
    via ``demisto.args()`` into an ``int`` containing a timestamp (seconds
    since epoch). It will throw a ValueError if the input is invalid.
    If the input is None, it will throw a ValueError if required is ``True``,
    or ``None`` if required is ``False``.

    Args:
        arg: argument to convert
        arg_name: argument name.
        required: throws exception if ``True`` and argument provided is None

    Returns:
        returns an ``int`` containing a timestamp (seconds from epoch) if conversion works
        returns ``None`` if arg is ``None`` and required is set to ``False``
        otherwise throws an Exception
    """
    if arg is None and required is True:
        raise ValueError(f'Missing "{arg_name}"')

    if (isinstance(arg, str) and arg.isdigit()) or isinstance(arg, int | float):
        # timestamp is a str containing digits - we just convert it to int
        # or convert to int if the input is a float
        return int(arg)
    if isinstance(arg, str):
        # we use dateparser to handle strings either in ISO8601 format, or
        # relative time stamps.
        # For example: format 2019-10-23T00:00:00 or "3 days", etc
        date = dateparser.parse(arg, settings={"TIMEZONE": "UTC"})
        if date is None:
            # if d is None it means dateparser failed to parse it
            raise ValueError(f"Invalid date: {arg_name}")

        return int(date.timestamp())
    raise ValueError(f'Invalid date: "{arg_name}"')


def timezone_format(epoch: int) -> str:
    """
    Converts an epoch timestamp into a formatted date in
    a specific timezone defined in the integration parameter.

    Args:
        epoch: argument to convert in epoch format

    Returns:
        returns an ``str`` containing a formatted datestring in the timezone selected
    """
    utc_datetime = datetime.utcfromtimestamp(epoch)
    timezone = demisto.params().get("timezone", "UTC")
    timezoneFormat = pytz.timezone(timezone)
    format_datetime = utc_datetime.astimezone(timezoneFormat)
    return format_datetime.strftime("%Y-%m-%dT%H:%M:%S")


def time_converter(time) -> str:
    """
    Converts a given time string to a datetime object.

    Args:
        time (str): The time string to be converted.

    Returns:
        datetime: The converted datetime object.

    Raises:
        ValueError: If the time string is in an invalid format.
    """

    # Regular expression patterns
    iso_8601_pattern = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$")
    unix_timestamp_pattern = re.compile(r"^\d+$")

    if iso_8601_pattern.match(time):
        update_time = datetime.fromisoformat(time)
    elif unix_timestamp_pattern.match(time):
        update_time = datetime.fromtimestamp(int(time), pytz.utc)
    else:
        raise ValueError("Invalid time format")

    return update_time.strftime(DATE_FORMAT)


def convert_to_demisto_severity(severity: str) -> int:
    """
    Maps Sekoia XDR urgency to Cortex XSOAR severity.
    Converts the Sekoia XDR alert urgency level ('Low','Moderate','High','Major','Urgent') to Cortex XSOAR incident
    severity (1 to 4).

    Args:
        urgency (str): urgency display text as returned from the Sekoia XDR API.

    Returns:
        int: Cortex XSOAR Severity (1 to 4)
    """

    return {
        "Low": IncidentSeverity.LOW,
        "Moderate": IncidentSeverity.MEDIUM,
        "High": IncidentSeverity.HIGH,
        "Major": IncidentSeverity.HIGH,
        "Urgent": IncidentSeverity.CRITICAL,
    }[severity]


def exclude_info_events(event_info: dict, exclude_info: str) -> list:
    """
    Exclude information from the events.
    This function will exclude information from the events that is duplicated or not needed.

    Args:
        event_info (dict): JSON to be transformed removing some of the information.
        exclude_info (str): the event fields to be removed from the results.

    Returns:
        dict: JSON transformed with the information removed.
    """
    splitted_exclude_info = exclude_info.split(",")

    """ Exclude headers from the readable output """
    headers = list(event_info["items"][0].keys())
    for header in splitted_exclude_info:
        if header in headers:
            headers.remove(header)
    return headers


def undot(json_data: dict) -> dict:
    """
    Remove/Replace dots from the key names of a JSON.
    This function transform the name of the JSON keys that contain "dots" to make it easier to reference them in XSOAR.

    Args:
        json_data (dict): JSON to be transformed.

    Returns:
        dict: JSON with the key names that contain "dots" transformed.
    """
    replace_symbol = demisto.params().get("replace_dots_event", "_")

    if isinstance(json_data, str):
        data = json.loads(json_data)
    elif isinstance(json_data, dict):
        data = json_data
    else:
        raise TypeError("JSON data sent to undot function must be a string or a dictionary")

    # Iterate over each item in the items array
    for item in data["items"]:
        # Replace dots with underscores in each key
        for key in list(item.keys()):
            new_key = key.replace(".", replace_symbol)
            if new_key != key:
                item[new_key] = item.pop(key)
    # Convert back to JSON and return it
    return data


def filter_list_by_keys(dicts_list: list, keys_to_keep: list) -> list:
    """
    Filters a list of dictionaries by keeping only the specified keys.

    Args:
        dicts_list (list): A list of dictionaries.
        keys_to_keep (list): A list of keys to keep in the dictionaries.

    Returns:
        list: A new list of dictionaries with only the specified keys.
    """
    filtered_list = []
    for d in dicts_list:
        filtered_dict = {key: value for key, value in d.items() if key in keys_to_keep}
        filtered_list.append(filtered_dict)
    return filtered_list


def filter_dict_by_keys(input_dict: dict, keys_to_keep: list) -> dict:
    """
    Filters a dictionary by keeping only the key-value pairs whose keys are present in the given list.

    Args:
        input_dict (dict): The dictionary to filter.
        keys_to_keep (list): The list of keys to keep in the filtered dictionary.

    Returns:
        dict: The filtered dictionary containing only the key-value pairs whose keys are present in the keys_to_keep list.
    """
    return {key: value for key, value in input_dict.items() if key in keys_to_keep}


def fetch_alerts_asc_mode(
    client: Client,
    alert_status: str | None,
    alert_urgency: str | None,
    alert_type: str | None,
    max_results: int | None,
    alerts_created_at: str | None,
    alerts_updated_at: str | None,
    sort_by: str | None,
) -> List[Dict[str, Any]]:
    response = client.list_alerts(
        alerts_created_at=alerts_created_at,
        alerts_updated_at=alerts_updated_at,
        alerts_status=alert_status,
        alerts_urgency=alert_urgency,
        alerts_type=alert_type,
        alerts_limit=max_results,
        sort_by=sort_by,
        direction="asc",
    )

    alerts: List[Dict[str, Any]] = response.get("items", [])

    demisto.debug(f"Fetched {len(alerts)} alerts in ascending direction.")

    return alerts


def check_events_ready(client: Client, search_job_uuid: str, max_checks: int = 3) -> tuple[bool, dict | None]:
    """
    Check if an event search job is ready (without sleep/polling).
    Makes multiple rapid checks up to max_checks times.

    Args:
        client: The Sekoia client
        search_job_uuid: The UUID of the search job
        max_checks: Maximum number of status checks to perform (default 3)

    Returns:
        tuple: (is_ready: bool, events: dict | None)
            - is_ready: True if job finished (status==2), False otherwise
            - events: Events dict if ready, None if not ready
    """
    for attempt in range(max_checks):
        try:
            query_status = client.query_events_status(event_search_job_uuid=search_job_uuid)
            finished_status = query_status["status"] == 2

            if finished_status:
                events = client.retrieve_events(event_search_job_uuid=search_job_uuid)
                return True, events
        except Exception as e:
            demisto.debug(f"Error checking event status (attempt {attempt + 1}/{max_checks}): {e}")
            if attempt == max_checks - 1:
                raise

    # Not finished after all checks
    return False, None


def handle_alert_events_query(
    client: Client, alert: dict, earliest_time: str, latest_time: str, events_term: str
) -> tuple[dict[str, Any], bool]:
    """
    Handle event query for an alert with readiness check.

    Returns:
        tuple: (alert, is_ready)
            - alert: The alert dict with events (if ready) or job_uuid (if not ready)
            - is_ready: True if events are available, False if job still pending
    """
    buffered_earliest = apply_time_buffer(earliest_time, -1)
    buffered_latest = apply_time_buffer(latest_time, 1)

    # Create a query to get events
    search = client.query_events(
        events_earliest_time=buffered_earliest,
        events_latest_time=buffered_latest,
        events_term=events_term,
        max_last_events=None,
    )

    # Get the search job uuid
    search_job_uuid = search["uuid"]
    demisto.debug(f"Created event search job {search_job_uuid} for alert {alert.get('short_id')}")

    # Check if events are ready (with multiple rapid checks)
    is_ready, events = check_events_ready(client, search_job_uuid, max_checks=3)

    if is_ready and events:
        # Events are ready - add them to the alert
        undoted_events = undot(json_data=events)
        alert["events"] = undoted_events
        demisto.debug(f"Events ready for alert {alert.get('short_id')}")
    else:
        # Events not ready yet - store job_uuid for later retry
        alert["job_uuid"] = search_job_uuid
        demisto.debug(f"Events not ready for alert {alert.get('short_id')}, stored job_uuid {search_job_uuid}")

    return alert, is_ready


def check_id_in_context(alert_id: str, cache: dict[str, Any] | None) -> tuple[dict[str, Any], int] | None:
    """
    Check if the alert id is in the context cache
    Args:
        alert_id (str): The alert id to check.
        cache (dict): The context cache.
    Returns:
        tuple: The alert object and the index of the alert in the cache.
    """
    if cache:
        mirror_cache = cache.get("mirroring_cache")
        if mirror_cache:
            for item in mirror_cache:
                cached_alert_id = item.get("alert").get("short_id")
                if alert_id == cached_alert_id:
                    return item, mirror_cache.index(item)
    return None


def apply_time_buffer(date_str: str, delta_minutes: int) -> str:
    """
    Apply a time buffer to a given ISO 8601 date string.

    Args:
        date_str (str): The date string in ISO 8601 format.
        delta_minutes (int): The number of minutes to add (positive) or subtract (negative).

    Returns:
        str: The buffered date string in ISO 8601 format.
    """

    # Handle None gracefully
    if date_str is None:
        raise ValueError(
            "date_str cannot be None. Alert must have first_seen_at and last_seen_at. "
            "This indicates a data quality issue with the alert from Sekoia API."
        )

    try:
        clean_date_str = date_str.replace("Z", "+00:00") if date_str.endswith("Z") else date_str
        dt_obj = datetime.fromisoformat(clean_date_str)
        dt_buffered = dt_obj + timedelta(minutes=delta_minutes)
        iso_str = dt_buffered.isoformat()
        # Truncate fractional seconds to milliseconds if present, while preserving timezone/offset.
        iso_str = re.sub(r"(.*\.\d{3})\d*(Z|[+-]\d{2}:\d{2})$", r"\1\2", iso_str)
        # Normalize UTC offset to 'Z'.
        iso_str = iso_str.replace("+00:00", "Z")
        return iso_str
    except ValueError as e:
        demisto.debug(f"Error applying time buffer: {e}")
        return date_str


""" COMMAND FUNCTIONS """


def fetch_incidents(
    client: Client,
    max_results: int | None,
    last_run: dict[str, int | list[str]],
    first_fetch_time: int | None,
    alert_status: str | None,
    alert_urgency: str | None,
    alert_type: str | None,
    fetch_mode: str | None,
    mirror_direction: str | None,
    fetch_with_assets: bool | None,
    fetch_with_kill_chain: bool | None,
) -> tuple[dict[str, int | list[str]], list[dict[str, Any]]]:
    """
    This function retrieves new alerts every interval (default is 1 minute).
    It has to implement the logic of making sure that incidents are fetched only onces and no incidents are missed.
    By default it's invoked by XSOAR every minute. It will use last_run to save the timestamp of the last incident it
    processed. If last_run is not provided, it should use the integration parameter first_fetch_time to determine when
    to start fetching the first time.

    Args:
        client (Client): Sekoia XDR client to use.
        max_results (int): Maximum numbers of incidents per fetch.
        last_run (dict): A dict with a key containing the latest incident created time we got from last fetch.
        first_fetch_time(int): If last_run is None (first time we are fetching), it contains the timestamp in
            milliseconds on when to start fetching incidents.
        alert_status (str): status of the alert to search for.
        alert_urgency (str): alert urgency range to search for. Format: "MIN_urgency,MAX_urgency". i.e: 80,100.
        alert_type (str): type of alerts to search for.
        fetch_mode (str): If the alert will be fetched with or without the events.
        mirror_direction (str): The direction of the mirroring can be set to None or to Incoming.
        fetch_with_assets (bool): If the alert will include the assets information on the fetching.
        fetch_with_kill_chain (bool): If the alert will include the kill chain information on the fetching.
    Returns:
        dict: Next run dictionary containing the timestamp that will be used in ``last_run`` on the next fetch.
        list: List of incidents that will be created in XSOAR.
    """
    # Initialize an empty list of incidents to return
    # Each incident is a dict with a string as a key
    incidents: list[dict[str, Any]] = []

    # Getting the fetch cached context
    cached_context = get_integration_context()
    not_finished_incident = []  # List to store incidents that are not finished

    # Making sure that the context is not empty and contains the fetch cache
    if cached_context:
        fetch_context = cached_context.get("fetch_cache")
        if fetch_context:
            # Loop through the cached incidents and check their status
            # If the status is not finished, we will keep it in the cache
            # If the status is finished, we will add it to the incidents list
            for incident in fetch_context:
                incident_job_uuid = incident["rawJSON"]["job_uuid"]
                try:
                    query_status = client.query_events_status(event_search_job_uuid=incident_job_uuid)
                    finished_status = query_status["status"] == 2

                    if not finished_status:
                        not_finished_incident.append(incident)
                        continue

                    events = client.retrieve_events(event_search_job_uuid=incident_job_uuid)
                    undoted_events = undot(json_data=events)
                    incident["rawJSON"]["events"] = undoted_events
                    # Serialize the rawJSON to a string
                    incident["rawJSON"] = json.dumps(incident["rawJSON"])
                    incidents.append(incident)
                except Exception as e:
                    # Case where the job uuid is not valid anymore
                    demisto.debug(f"Error fetching incident {incident['rawJSON']['short_id']}: {e}")
                    # Rerun command to get events
                    earliest_time = incident["rawJSON"]["first_seen_at"]
                    # Upper bound "now": capture events timestamped/indexed slightly after last_seen_at
                    # (results stay scoped to this alert via the alert_short_ids term).
                    latest_time = "now"
                    term = f"alert_short_ids:{incident['rawJSON']['short_id']}"

                    alert, _ = handle_alert_events_query(client, incident["rawJSON"], earliest_time, latest_time, term)
                    incident["rawJSON"] = alert
                    # If the incident is not finished, we will keep it in the cache
                    not_finished_incident.append(incident)

        # If there are any incidents that are not finished, we will keep them in the cache
        cached_context["fetch_cache"] = not_finished_incident
        set_integration_context(cached_context)

    # Get the last fetch time and processed alert IDs from last_run
    last_fetch = last_run.get("last_fetch")
    processed_alert_ids: list[str] = cast(list[str], last_run.get("processed_ids", []))

    # The case where no last_fetch or first_fetch_time are present.
    if last_fetch is None and first_fetch_time is None:
        raise DemistoException(
            "Failure to fetch incidents. Can't find neither \
            last_fetch and first_fetch_time"
        )
    # Handle first fetch time
    elif last_fetch is None:
        # if missing, use what provided via first_fetch_time
        last_fetch = first_fetch_time
    else:
        # otherwise use the stored last fetch
        last_fetch = cast(int, last_fetch)

    # Convert time from epoch to ISO8601 in the correct format and add the ,now also
    alerts_created_at = f"{time_converter(str(last_fetch))},now"

    # for type checking, making sure that latest_created_time is int
    latest_created_time = cast(int, last_fetch)

    # Track IDs of alerts with the latest timestamp for next run
    # Carry forward previously processed IDs to prevent re-processing when no new alerts arrive
    current_processed_ids: list[str] = list(processed_alert_ids)

    # Increase the API limit to account for already-processed alerts at the boundary timestamp.
    # Without this, when all returned alerts are already processed, the fetch gets stuck
    # because there's no room in the API response for newer alerts.
    num_processed = len(processed_alert_ids)
    fetch_limit = max_results + num_processed if max_results else max_results

    alerts = fetch_alerts_asc_mode(
        client,
        alert_status,
        alert_urgency,
        alert_type,
        fetch_limit,
        alerts_created_at,
        None,
        "created_at",
    )

    # Initiate an empty list to store cached incidents
    cached_incidents: list[dict[str, Any]] = []

    for alert in alerts:
        # If no created_time set is as epoch (0). We use time in ms so we must
        # convert it from the Sekoia XDR API response
        alert_created_time = int(alert.get("created_at", "0"))
        alert_created_time_ms = alert_created_time * 1000
        alert_id = alert.get("short_id")

        # Skip alerts without a valid ID
        if not alert_id:
            demisto.debug(f"Skipping alert without short_id: {alert}")
            continue

        # Skip if this alert was already processed (same timestamp as last_fetch and in processed list)
        if alert_created_time == last_fetch and alert_id in processed_alert_ids:
            demisto.debug(f"Skipping already processed alert {alert_id} with timestamp {alert_created_time}")
            continue

        # to prevent duplicates, we are only adding incidents with creation_time > last fetched incident
        if last_fetch and alert_created_time < last_fetch:
            continue

        # Stop processing if we've reached the max_results limit for new incidents
        if max_results and (len(incidents) + len(cached_incidents)) >= max_results:
            break

        # If no name is present it will throw an exception
        alert_name = alert["title"]
        urgency = alert["urgency"]

        # Add assets information to the alert, if fetch_with_assets is set to True
        if fetch_with_assets:
            asset_list = []
            for asset in alert["assets"]:
                try:
                    asset_info = client.get_asset(asset_uuid=asset)
                    asset_list.append(asset_info)
                except Exception as e:
                    demisto.debug(f"Error fetching asset {asset}: {e}")
                    continue
            alert["assets"] = asset_list

        # Add kill chain information to the alert, if fetch_with_kill_chain is set to True
        if fetch_with_kill_chain and alert["kill_chain_short_id"]:
            try:
                kill_chain = client.get_kill_chain(kill_chain_uuid=alert["kill_chain_short_id"])
                alert["kill_chain"] = kill_chain
            except Exception as e:
                demisto.debug(f"Error fetching kill chain information {kill_chain}: {e}")

        # Add events information to the alert, if fetch_mode is set to "Fetch With All Events"
        if fetch_mode == "Fetch With All Events":
            earliest_time = alert["first_seen_at"]
            # Upper bound "now": capture events timestamped/indexed slightly after last_seen_at
            # (results stay scoped to this alert via the alert_short_ids term).
            latest_time = "now"
            term = f"alert_short_ids:{alert['short_id']}"

            alert, _ = handle_alert_events_query(client, alert, earliest_time, latest_time, term)

        # Start building the incident
        incident = {
            "name": alert_name,
            "occurred": timestamp_to_datestring(alert_created_time_ms),
            "severity": convert_to_demisto_severity(urgency.get("display", "Low")),
        }
        # If the integration parameter is set to mirror add the appropriate fields to the incident
        alert["mirror_instance"] = demisto.integrationInstance()
        alert["mirrorOut"] = str(mirror_direction) in [
            "Outgoing",
            "Incoming and Outgoing",
        ]
        incident["dbotMirrorDirection"] = MIRROR_DIRECTION.get(str(mirror_direction))
        incident["dbotMirrorId"] = alert["short_id"]

        # Add the alert to the incident only if events are ready (or events not requested)
        if not alert.get("job_uuid"):
            # Events are ready or not requested - create incident immediately
            incident["rawJSON"] = json.dumps(alert)
            incidents.append(incident)
        else:
            # Events still pending - cache the incident for next fetch cycle
            # Keep rawJSON as dict (not dumped) for easier manipulation in cache
            incident["rawJSON"] = alert
            cached_incidents.append(incident)
            demisto.debug(f"Alert {alert_id} cached pending event search {alert.get('job_uuid')}")

        # Update latest_created_time and track processed IDs
        # If timestamp is newer than last_fetch, reset the processed_ids list
        if alert_created_time > latest_created_time:
            latest_created_time = alert_created_time
            current_processed_ids = [alert_id]  # Reset for new timestamp
        elif alert_created_time == latest_created_time:
            # Same timestamp, add to list if not already there
            if alert_id not in current_processed_ids:
                current_processed_ids.append(alert_id)

    # Store the list of all alert in the cache
    if len(cached_incidents) > 0:
        context_cache = get_integration_context() or {}
        # If the context is empty, we will create a new one
        if not context_cache:
            context_cache["fetch_cache"] = cached_incidents
            set_integration_context(context_cache)
        else:
            # If the context is not empty, we will append the new incidents to the existing list
            fetch_cache_list = context_cache["fetch_cache"]
            fetch_cache_list.extend(cached_incidents)
            context_cache["fetch_cache"] = fetch_cache_list
            set_integration_context(context_cache)

    # Save the next_run with both timestamp and processed IDs in last_run (persistent)
    next_run: dict[str, int | list[str]] = {"last_fetch": latest_created_time, "processed_ids": current_processed_ids}
    return next_run, incidents


# =========== Mirroring Mechanism ===========


def get_remote_data_command(
    client: Client,
    args: dict,
    close_incident: bool,
    close_note: str,
    mirror_events: bool,
    mirror_kill_chain: bool,
    reopen_incident: bool,
):
    """get-remote-data command: Returns an updated alert and error entry (if needed)

    Args:
        client (Client): Sekoia XDR client to use.
        args (dict): The command arguments
        close_incident (bool): Indicates whether to close the corresponding XSOAR incident if the alert
            has been closed on Sekoia's end.
        close_note (str): Indicates the notes to be including when the incident gets closed by mirroring.
        mirror_events (bool): If the events will be included in the mirroring of the alerts or not.
        mirror_kill_chain: If the kill chain information from the alerts will be mirrored.
        reopen_incident: Indicates whether to reopen the corresponding XSOAR incident if the alert
            has been reopened on Sekoia's end.
    Returns:
        GetRemoteDataResponse: The Response containing the update alert to mirror and the entries
    """

    demisto.debug("#### Entering MIRRORING IN - get_remote_data_command ####")

    parsed_args = GetRemoteDataArgs(args)
    alert_id_args = parsed_args.remote_incident_id
    context_cache = get_integration_context()

    # Check if alert id is the same as the one in the context
    check_id: tuple[dict[str, Any], int] | None = check_id_in_context(alert_id_args, context_cache)

    if not check_id:
        entries = []  # List to store the entries

        # If the alert id is not in the context, we will get the alert from Sekoia
        alert = client.get_alert(alert_uuid=alert_id_args)
        alert_short_id, alert_status = alert["short_id"], alert["status"]["name"]
        last_update = arg_to_timestamp(arg=parsed_args.last_update, arg_name="lastUpdate", required=True)
        alert_last_update = arg_to_timestamp(arg=alert.get("updated_at"), arg_name="updated_at", required=False)

        demisto.debug(
            f"Alert {alert_short_id} with status {alert_status} : last_update is {last_update} , alert_last_update is {alert_last_update}"  # noqa: E501
        )

        # Add the kill chain information to the alert if mirror_kill_chain is set to True
        if mirror_kill_chain and alert["kill_chain_short_id"]:
            try:
                kill_chain = client.get_kill_chain(kill_chain_uuid=alert["kill_chain_short_id"])
                alert["kill_chain"] = kill_chain
            except Exception as e:
                demisto.debug(f"Error fetching kill_chain : {e}")

        # Add the events to the alert
        if mirror_events and alert["status"]["name"] not in ["Closed", "Rejected"]:
            earliest_time = alert["first_seen_at"]
            # Upper bound "now": capture events timestamped/indexed slightly after last_seen_at
            # (results stay scoped to this alert via the alert_short_ids term).
            latest_time = "now"
            term = f"alert_short_ids:{alert['short_id']}"

            alert, _ = handle_alert_events_query(client, alert, earliest_time, latest_time, term)

        # This adds all the information from the XSOAR incident.
        demisto.debug(f"Alert {alert_short_id} with status {alert_status} have this info updated: {alert}")

        investigation = demisto.investigation()
        demisto.debug(f"The investigation information is {investigation}")

        incident_id = investigation["id"]
        incident_status = investigation["status"]

        demisto.debug(
            f"The XSOAR incident is {incident_id} with status {incident_status} is being mirrored with the alert {alert_short_id} that have the status {alert_status}."  # noqa: E501
        )

        # Close the XSOAR incident using mirroring
        if (close_incident) and (alert_status in ["Closed", "Rejected"]) and (investigation["status"] != 1):
            demisto.debug(
                f"Alert {alert_short_id} with status {alert_status} was closed or rejected in Sekoia, closing incident {incident_id} in XSOAR"  # noqa: E501
            )
            entries = [
                {
                    "Type": EntryType.NOTE,
                    "Contents": {
                        "dbotIncidentClose": True,
                        "closeReason": f"{alert_status} - Mirror",
                        "closeNotes": close_note,
                    },
                    "ContentsFormat": EntryFormat.JSON,
                }
            ]

        # Reopen the XSOAR incident using mirroring
        if (reopen_incident) and (alert_status not in ["Closed", "Rejected"]) and (investigation["status"] == 1):
            demisto.debug(
                f"Alert {alert_short_id} with status {alert_status} was reopened in Sekoia, "
                f"reopening incident {incident_id} in XSOAR"
            )
            entries = [
                {
                    "Type": EntryType.NOTE,
                    "Contents": {"dbotIncidentReopen": True},
                    "ContentsFormat": EntryFormat.JSON,
                }
            ]

        if not alert.get("job_uuid"):
            # If the job uuid is not present, we will return the alert and the entries
            demisto.debug("#### Leaving MIRRORING IN - get_remote_data_command ####")
            return GetRemoteDataResponse(mirrored_object=alert, entries=entries)
        else:
            # If the job uuid is present, we will add the alert to the context
            context_cache = get_integration_context() or {}
            mirroring_cache = context_cache.get("mirroring_cache")
            alert_dict = {"alert": alert, "entries": entries}
            if not mirroring_cache:
                context_cache["mirroring_cache"] = [alert_dict]
                set_integration_context(context_cache)
            else:
                mirroring_cache.append(alert_dict)
                context_cache["mirroring_cache"] = mirroring_cache
                set_integration_context(context_cache)
            return None
    else:
        # If the alert id is in the context, we will get the alert from the context
        alert_object, index = check_id
        try:
            alert_job_uuid = alert_object["alert"]["job_uuid"]
            query_status = client.query_events_status(event_search_job_uuid=alert_job_uuid)
            finished_status = query_status["status"] == 2

            # If the alert is not finished, we will not return it
            if not finished_status:
                return None

            # If the alert is finished, we will get the events
            events = client.retrieve_events(event_search_job_uuid=alert_job_uuid)
            undoted_events = undot(json_data=events)
            alert_object["alert"]["events"] = undoted_events

            # Delete the object from the context
            del context_cache["mirroring_cache"][index]
            set_integration_context(context_cache)

            return GetRemoteDataResponse(mirrored_object=alert_object["alert"], entries=alert_object["entries"])
        except Exception as e:
            # Case where the job uuid is not valid anymore
            demisto.debug(f"Error fetching incident {alert_object['alert']['short_id']}: {e}")
            # Rerun command to get events
            earliest_time = alert_object["alert"]["first_seen_at"]
            # Upper bound "now": capture events timestamped/indexed slightly after last_seen_at
            # (results stay scoped to this alert via the alert_short_ids term).
            latest_time = "now"
            term = f"alert_short_ids:{alert_object['alert']['short_id']}"

            alert, _ = handle_alert_events_query(client, alert_object["alert"], earliest_time, latest_time, term)
            alert_object["alert"] = alert

            # Update the cached alert in the context
            context_cache["mirroring_cache"][index]["alert"] = alert_object["alert"]

            return None


def get_modified_remote_data_command(client: Client, args):
    """Gets the list of all alert ids that have change since a given time

    Args:
        client (Client): Sekoia XDR client to use.
        args (dict): The command argument

    Returns:
        GetModifiedRemoteDataResponse: The response containing the list of ids of notables changed
    """
    modified_alert_ids = []
    cached_context = get_integration_context()

    demisto.debug(f"Inside modified remote data This's context cache : {cached_context}")
    # Start by cached context
    # Check if the context is not empty and contains the mirroring cache
    # If it's not empty, we will add the alert ids to the list
    if cached_context:
        mirroring_context = cached_context.get("mirroring_cache")
        if mirroring_context:
            for alert_object in mirroring_context:
                modified_alert_ids.append(alert_object.get("alert", {}).get("short_id"))

    # Continue with normal way to get the modified alerts
    remote_args = GetModifiedRemoteDataArgs(args)
    last_update = remote_args.last_update
    last_update_utc = dateparser.parse(last_update, settings={"TIMEZONE": "UTC"})  # Converts to a UTC timestamp
    formatted_last_update = last_update_utc.strftime("%Y-%m-%dT%H:%M:%S.%f+00:00")  # type: ignore
    converted_time = time_converter(formatted_last_update)
    last_update_time = f"{converted_time},now"

    raw_alerts = fetch_alerts_asc_mode(client, None, None, None, 100, None, last_update_time, "updated_at")

    # Append the modified alert ids to the list
    # We can have alerts in this list that are already in the cache
    modified_alert_ids += [item["short_id"] for item in raw_alerts]

    return GetModifiedRemoteDataResponse(modified_incident_ids=modified_alert_ids)


def update_remote_system_command(client: Client, args):
    pass


def get_mapping_fields_command() -> GetMappingFieldsResponse:
    """
     this command pulls the remote schema for the different incident types, and their associated incident fields,
     from the remote system.
    :return: A list of keys you want to map
    """
    sekoia_incident_type_scheme = SchemeTypeMapping(type_name=INCIDENT_TYPE_NAME)
    for argument, description in SEKOIA_INCIDENT_FIELDS.items():
        sekoia_incident_type_scheme.add_field(name=argument, description=description)

    mapping_response = GetMappingFieldsResponse()
    mapping_response.add_scheme_type(sekoia_incident_type_scheme)

    return mapping_response


# =========== Mirroring Mechanism ===========


def list_alerts_command(client: Client, args: dict[str, Any]) -> CommandResults:
    alerts = client.list_alerts(
        alerts_limit=args.get("limit"),
        alerts_status=args.get("status"),
        alerts_created_at=args.get("created_at"),
        alerts_updated_at=args.get("updated_at"),
        alerts_urgency=args.get("urgency"),
        alerts_type=args.get("alerts_type"),
        sort_by=args.get("sort_by"),
    )

    header = ["title", "uuid", "short_id", "community_uuid"]
    command_output = filter_list_by_keys(alerts["items"], header)
    readable_output = tableToMarkdown("Alerts :", command_output, headers=header)

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="SekoiaXDR.ListAlerts",
        outputs_key_field="short_id",
        outputs=alerts["items"],
    )


def get_alert_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    alert_uuid = args["id"]

    alert = client.get_alert(alert_uuid=alert_uuid)
    header = [
        "alert_type",
        "short_id",
        "created_by_type",
        "kill_chain_short_id",
        "details",
        "rule",
    ]
    command_output = filter_dict_by_keys(alert, header)
    readable_output = tableToMarkdown(f"Alert {alert_uuid}:", command_output, headers=header)

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="SekoiaXDR.Alert",
        outputs_key_field="uuid",
        outputs=alert,
    )


def query_events_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    earliest_time = args["earliest_time"]
    latest_time = args["lastest_time"]
    term = args["query"]
    max_last_events = args.get("max_last_events")

    jobQuery = client.query_events(
        events_earliest_time=earliest_time,
        events_latest_time=latest_time,
        events_term=term,
        max_last_events=max_last_events,
    )
    readable_output = tableToMarkdown(f"Event search created using the term {term}:", jobQuery)

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="SekoiaXDR.Events",
        outputs_key_field="uuid",
        outputs=jobQuery,
    )


def query_events_status_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    search_job_uuid = args["uuid"]

    status = client.query_events_status(event_search_job_uuid=search_job_uuid)
    readable_output = tableToMarkdown(f"Status of the job {search_job_uuid}:", status)

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="SekoiaXDR.Events",
        outputs_key_field="search_job_uuid",
        outputs=status,
    )


def retrieve_events_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    search_job_uuid = args["uuid"]

    events = client.retrieve_events(event_search_job_uuid=search_job_uuid)
    readable_output = tableToMarkdown(f"Events retrieved for the search {search_job_uuid}:", events)

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="SekoiaXDR.Events",
        outputs_key_field="search_job_uuid",
        outputs=events,
    )


@polling_function(name="sekoia-xdr-search-events", requires_polling_arg=False)
def search_events_command(args: dict[str, Any], client: Client) -> PollResult:
    """Parameters"""
    earliest_time = args["earliest_time"]
    latest_time = args["lastest_time"]
    term = args["query"]
    max_last_events = args.get("max_last_events")
    exclude_info_arg = args.get("exclude_info")
    exclude_info_param = demisto.params().get("exclude_info_events")
    replace_symbol = demisto.params().get("replace_dots_event")

    if not (search_job_uuid := args.get("job_uuid")):
        search = client.query_events(
            events_earliest_time=earliest_time,
            events_latest_time=latest_time,
            events_term=term,
            max_last_events=max_last_events,
        )
        search_job_uuid = search["uuid"]

    query_status = client.query_events_status(event_search_job_uuid=search_job_uuid)
    finished_status = query_status["status"] == 2

    if not finished_status:
        return PollResult(
            response=None,
            continue_to_poll=True,
            args_for_next_run=(args | {"job_uuid": search_job_uuid}),
            partial_result=CommandResults(readable_output=f"Query is still running. Current state: {query_status['status']}."),
        )

    events = client.retrieve_events(event_search_job_uuid=search_job_uuid)
    total = max_last_events or events["total"]

    if len(events["items"]) > 0:
        if exclude_info_arg:
            headers = exclude_info_events(event_info=events, exclude_info=exclude_info_arg)
            headers = [header.replace(".", replace_symbol) for header in headers]
            undot(json_data=events)
            readable_output = tableToMarkdown(
                f"{total} events out of {str(events['total'])} retrieved for the {term}",
                events["items"],
                headers=headers,
            )
        elif exclude_info_param:
            headers = exclude_info_events(event_info=events, exclude_info=",".join(exclude_info_param))
            headers = [header.replace(".", replace_symbol) for header in headers]
            undot(json_data=events)
            readable_output = tableToMarkdown(
                f"{total} events out of {events['total']} retrieved for the {term}",
                events["items"],
                headers=headers,
            )
        else:
            undot(json_data=events)
            headers = list(events["items"][0].keys())
            readable_output = tableToMarkdown(
                f"{total} events out of {events['total']} retrieved for the {term}",
                events["items"],
                headers=headers,
            )
    else:
        readable_output = tableToMarkdown(
            f"{total} events out of {events['total']} retrieved for the {term}",
            events["items"],
        )

    return PollResult(
        response=CommandResults(
            readable_output=readable_output,
            outputs_prefix="SekoiaXDR.Events.Results",
            outputs_key_field="search_job_uuid",
            outputs=events["items"],
        ),
        continue_to_poll=False,
    )


def update_status_alert_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    alert_uuid, updated_status, comment = (
        args["id"],
        args["status"],
        args.get("comment"),
    )
    sekoia_transition = STATUS_TRANSITIONS.get(updated_status)
    readable_output = "Unknown alert"

    workflow = client.get_workflow_alert(alert_uuid=alert_uuid)

    for action in workflow["actions"]:
        if action["name"] == sekoia_transition:
            update = client.update_status_alert(alert_uuid=alert_uuid, action_uuid=action["id"], comment=comment)
            if update or update == {}:
                readable_output = f"### Alert {alert_uuid} updated to status: {updated_status}"
            else:
                raise DemistoException(
                    "Failure to update the status of the alert. \
                    Run the command !sekoia-xdr-get-workflow-alert to see the possible\
                    transitions and review the code."
                )

    return CommandResults(readable_output=readable_output, outputs=update)


def post_comment_alert_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    alert_uuid, comment, author = (
        args["id"],
        args["comment"],
        args.get("author"),
    )

    response = client.post_comment_alert(alert_uuid=alert_uuid, content=comment, author=author)
    response["date"] = timezone_format(response["date"])

    readable_output = tableToMarkdown(f"Alert {alert_uuid} updated with the comment: \n {comment}:", response)

    return CommandResults(readable_output=readable_output, outputs=response)


def get_comments_alert_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    alert_uuid = args["id"]

    response = client.get_comments_alert(alert_uuid=alert_uuid)

    for item in response["items"]:
        # Add author of the comment
        if item["author"].startswith("user"):
            item["user"] = f"User with id {item['created_by']}"
        elif item["author"].startswith("apikey"):
            item["user"] = "Commented via API"
        elif item["author"].startswith("application"):
            item["user"] = "Sekoia.io"
        else:
            item["user"] = item["author"]
        # Add formatted date of the comment
        item["date"] = timezone_format(item["date"])

    readable_output = tableToMarkdown(f"Alert {alert_uuid} have the following comments:", response["items"])

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="SekoiaXDR.Comments",
        outputs_key_field="alert_uuid",
        outputs=response["items"],
    )


def get_workflow_alert_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    alert_uuid = args["id"]

    response = client.get_workflow_alert(alert_uuid=alert_uuid)
    readable_output = tableToMarkdown(
        f"Alert {alert_uuid} have the following available status transitions:",
        response["actions"],
    )

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="SekoiaXDR.StatusTransitions",
        outputs_key_field="alert_uuid",
        outputs=response["actions"],
    )


def get_cases_alert_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    alert_uuid, case_id = args["alert_id"], args.get("case_id")

    response = client.get_cases_alert(alert_uuid=alert_uuid, case_id=case_id)
    header = [
        "title",
        "description",
        "priority",
        "short_id",
        "status",
        "community_uuid",
    ]
    command_output = filter_list_by_keys(response["items"], header)
    readable_output = tableToMarkdown(f"Alert {alert_uuid} have the following cases:", command_output, headers=header)

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="SekoiaXDR.Cases",
        outputs_key_field="alert_uuid",
        outputs=response["items"],
    )


def get_asset_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    asset_uuid = args["asset_uuid"]

    asset = client.get_asset(asset_uuid=asset_uuid)
    header = ["name", "uuid", "description"]
    command_output = filter_dict_by_keys(asset, header)
    readable_output = tableToMarkdown(
        f"Asset {asset_uuid} have the following information:",
        command_output,
        headers=header,
    )

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="SekoiaXDR.Asset",
        outputs_key_field="uuid",
        outputs=asset,
    )


def list_asset_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    limit, assets_type = args.get("limit"), args.get("assets_type")

    assets = client.list_asset(limit=limit, assets_type=assets_type)
    header = ["name", "uuid", "description"]
    command_output = filter_list_by_keys(assets["items"], header)
    readable_output = tableToMarkdown(f"List of {assets['total']} assets found:", command_output, headers=header)

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="SekoiaXDR.Assets",
        outputs_key_field="asset_uuid",
        outputs=assets["items"],
    )


def get_user_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    user_uuid = args["user_uuid"]

    user = client.get_user(user_uuid=user_uuid)
    readable_output = tableToMarkdown(f"User {user_uuid} have the following information:", user)

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="SekoiaXDR.User",
        outputs_key_field="user_uuid",
        outputs=user,
    )


def add_attributes_asset_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    asset_uuid, name, value = (
        args["asset_uuid"],
        args["name"],
        args["value"],
    )

    asset_attributes = client.add_attributes_asset(asset_uuid=asset_uuid, name=name, value=value)
    readable_output = tableToMarkdown(f"Asset {asset_uuid} was updated with new attributes:", asset_attributes)

    return CommandResults(
        readable_output=readable_output,
        outputs_key_field="asset_uuid",
        outputs=asset_attributes,
    )


def add_keys_asset_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    asset_uuid, name, value = (
        args["asset_uuid"],
        args["name"],
        args["value"],
    )

    asset_keys = client.add_keys_asset(asset_uuid=asset_uuid, name=name, value=value)
    readable_output = tableToMarkdown(f"Asset {asset_uuid} was updated with new keys:", asset_keys)

    return CommandResults(
        readable_output=readable_output,
        outputs_key_field="asset_uuid",
        outputs=asset_keys,
    )


def remove_attribute_asset_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    asset_uuid, attribute_uuid = args["asset_uuid"], args["attribute_uuid"]

    client.remove_attribute_asset(asset_uuid=asset_uuid, attribute_uuid=attribute_uuid)
    readable_output = f"Asset {asset_uuid} had the following attribute removed:\n{attribute_uuid}"

    return CommandResults(readable_output=readable_output)


def remove_key_asset_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    asset_uuid, key_uuid = args["asset_uuid"], args["key_uuid"]

    client.remove_key_asset(asset_uuid=asset_uuid, key_uuid=key_uuid)
    readable_output = f"Asset {asset_uuid} had the following key removed:\n{key_uuid}"

    return CommandResults(readable_output=readable_output)


def get_kill_chain_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    kill_chain_uuid = args["kill_chain_uuid"]

    kill_chain = client.get_kill_chain(kill_chain_uuid=kill_chain_uuid)
    readable_output = tableToMarkdown(f"Kill chain {kill_chain_uuid} have the following information:", kill_chain)

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="SekoiaXDR.KillChain",
        outputs_key_field="uuid",
        outputs=kill_chain,
    )


def http_request_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """Parameters"""
    method = args["method"]
    url_suffix = args.get("url_suffix") or args.get("url_sufix")

    if not url_suffix:
        raise DemistoException("Either 'url_suffix' or 'url_sufix' parameter must be provided")

    params = args.get("parameters", {})
    data_arg = args.get("data", "")

    # Parse data if provided
    data = None
    if data_arg:
        try:
            data = json.loads(data_arg.replace("'", '"'))
        except json.JSONDecodeError as e:
            raise DemistoException(f"Data argument is not a valid JSON: {e}")

    # Make HTTP request with client
    http_response = client.http_request(method=method, params=params, url_suffix=url_suffix, data=data)

    # Extract response items if available
    response = http_response.get("items", http_response) if isinstance(http_response, dict) else http_response

    # Build output text based on what parameters were provided
    output_parts = [f"### HTTP {method} request to {url_suffix}"]
    if params:
        output_parts.append(f"with params {params}")
    if data:
        output_parts.append(f"with data {data}")
    output_parts.append("returned:")
    output_text = " ".join(output_parts)

    readable_output = tableToMarkdown(output_text, response)

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="SekoiaXDR.http_request",
        outputs_key_field="uuid",
        outputs=response,
    )


def test_module(client: Client) -> str:
    """
    Tests API connectivity and authentication'
    When 'ok' is returned it indicates the integration works like it is supposed to and connection to the service is successful.
    Raises exceptions if something goes wrong.

    Args:
        client (Client): Sekoia XDR client to use.

    Returns:
        str: 'ok' if test passed, anything else will raise an exception and will fail the test.
    """
    # Check a JWT token's validity
    # https://docs.sekoia.io/develop/rest_api/identity_and_authentication/#tag/User-Authentication/operation/get_validate_resource

    try:
        client.get_validate_resource()
    except DemistoException as e:
        doc = """Please visit the API Key documentation for more information:
         https://docs.sekoia.io/getting_started/generate_api_keys/"""

        if "T300" in str(e):
            return f"Authorization Error: The token is invalid. {doc}"
        elif "T301" in str(e):
            return f"Authorization Error: The token has expired. {doc}"
        elif "T302" in str(e):
            return f"Authorization Error: The token has been revoked. {doc}"
        else:
            raise e
    return "ok"


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

    params, args, command = demisto.params(), demisto.args(), demisto.command()

    # get the service API url and api key
    api_key = params.get("credentials", {}).get("password")
    base_url = params.get("url")

    # if your Client class inherits from BaseClient, SSL verification is
    # handled out of the box by it, just pass ``verify_certificate`` and ``proxy`` to
    # the Client constructor
    verify_certificate = not params.get("insecure", False)
    proxy = params.get("proxy", False)

    # How much time before the first fetch to retrieve incidents
    first_fetch_time = arg_to_datetime(
        arg=params["first_fetch"],
        arg_name="First fetch time",
        required=True,
    )
    first_fetch_timestamp = int(first_fetch_time.timestamp()) if first_fetch_time else None

    demisto.debug(f"Command being called is {command}")
    try:
        headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        }
        client = Client(base_url=base_url, verify=verify_certificate, headers=headers, proxy=proxy)

        if command == "test-module":
            # This is the call made when pressing the integration Test button.
            result = test_module(client)
            return_results(result)

        elif command == "fetch-incidents":
            # Set and define the fetch incidents command to run after activated via integration settings.
            alerts_status = ",".join(params.get("alerts_status", ""))
            alerts_type = ",".join(params.get("alerts_type", ""))
            alerts_urgency = params.get("alerts_urgency", None)
            fetch_mode = params.get("fetch_mode")
            fetch_with_assets = params.get("fetch_with_assets")
            fetch_with_kill_chain = params.get("fetch_with_kill_chain")
            mirror_direction = params.get("mirror_direction", "None")

            # Convert the argument to an int using helper function or set to MAX_INCIDENTS_TO_FETCH
            max_results = arg_to_number(params["max_fetch"])
            last_run: dict[str, Any] = demisto.getLastRun()  # getLastRun() gets the last run dict

            next_run, incidents = fetch_incidents(
                client=client,
                max_results=max_results,
                last_run=last_run,
                first_fetch_time=first_fetch_timestamp,
                alert_status=alerts_status,
                alert_urgency=alerts_urgency,
                alert_type=alerts_type,
                fetch_mode=fetch_mode,
                mirror_direction=mirror_direction,
                fetch_with_assets=fetch_with_assets,
                fetch_with_kill_chain=fetch_with_kill_chain,
            )

            # 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)

        elif command == "sekoia-xdr-list-alerts":
            return_results(list_alerts_command(client, args))
        elif command == "sekoia-xdr-get-alert":
            return_results(get_alert_command(client, args))
        elif command == "sekoia-xdr-events-execute-query":
            return_results(query_events_command(client, args))
        elif command == "sekoia-xdr-events-status-query":
            return_results(query_events_status_command(client, args))
        elif command == "sekoia-xdr-events-results-query":
            return_results(retrieve_events_command(client, args))
        elif command == "sekoia-xdr-search-events":
            return_results(search_events_command(args, client))
        elif command == "sekoia-xdr-update-status-alert":
            return_results(update_status_alert_command(client, args))
        elif command == "sekoia-xdr-post-comment-alert":
            return_results(post_comment_alert_command(client, args))
        elif command == "sekoia-xdr-get-comments":
            return_results(get_comments_alert_command(client, args))
        elif command == "sekoia-xdr-get-workflow-alert":
            return_results(get_workflow_alert_command(client, args))
        elif command == "sekoia-xdr-get-cases-alert":
            return_results(get_cases_alert_command(client, args))
        elif command == "sekoia-xdr-get-asset":
            return_results(get_asset_command(client, args))
        elif command == "sekoia-xdr-list-assets":
            return_results(list_asset_command(client, args))
        elif command == "sekoia-xdr-get-user":
            return_results(get_user_command(client, args))
        elif command == "sekoia-xdr-add-attributes-asset":
            return_results(add_attributes_asset_command(client, args))
        elif command == "sekoia-xdr-add-keys-asset":
            return_results(add_keys_asset_command(client, args))
        elif command == "sekoia-xdr-remove-attribute-asset":
            return_results(remove_attribute_asset_command(client, args))
        elif command == "sekoia-xdr-remove-key-asset":
            return_results(remove_key_asset_command(client, args))
        elif command == "sekoia-xdr-get-kill-chain":
            return_results(get_kill_chain_command(client, args))
        elif command == "sekoia-xdr-http-request":
            return_results(http_request_command(client, args))
        elif command == "get-remote-data":
            return_results(
                get_remote_data_command(
                    client,
                    args,
                    close_incident=demisto.params().get("close_incident"),  # type: ignore
                    close_note=demisto.params().get("close_notes", "Closed by Sekoia."),  # type: ignore
                    mirror_events=demisto.params().get("mirror_events"),  # type: ignore
                    mirror_kill_chain=demisto.params().get("mirror_kill_chain"),  # type: ignore
                    reopen_incident=demisto.params().get("reopen_incident"),  # type: ignore
                )
            )
        elif command == "get-modified-remote-data":
            return_results(get_modified_remote_data_command(client, args))
        elif command == "get-mapping-fields":
            return_results(get_mapping_fields_command())
        else:
            raise NotImplementedError(f"Command {command} is not implemented")

    # Log exceptions and return errors
    except Exception as e:
        return_error(f"Failed to execute {command} command.\nError:\n{str(e)}")


""" ENTRY POINT """

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