Cortex Core - Platform

This integration uses the Cortex API to access all the core services and capabilities of the Cortex platform.

Endpoint · Core

Details

IDCortex Core - Platform
ProviderPalo Alto Networks
CategoryEndpoint
From Version6.2.0
Docker Imagedemisto/google-cloud-storage:1.0.0.10120494
Supported ModulesExposure Management Cortex Cloud Cloud Runtime Security Agentix Cloud Posture Security XSIAM EDR

README

This integration uses the Cortex API to access all the core services and capabilities of the Cortex platform.

Configure Cortex Platform Core in Cortex

Parameter Description Required
HTTP Timeout The timeout of the HTTP requests sent to Cortex API (in seconds). False

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.

core-get-asset-details


Get asset information.

Base Command

core-get-asset-details

Input

Argument Name Description Required
asset_id Asset unique identifier. Required

Context Output

Path Type Description
Core.CoreAsset unknown Asset additional information.
Core.CoreAsset.xdm__asset__provider unknown The cloud provider or source responsible for the asset.
Core.CoreAsset.xdm__asset__realm unknown The realm or logical grouping of the asset.
Core.CoreAsset.xdm__asset__last_observed unknown The timestamp when the asset was last observed, in ISO 8601 format.
Core.CoreAsset.xdm__asset__type__id unknown The unique identifier for the asset type.
Core.CoreAsset.xdm__asset__first_observed unknown The timestamp when the asset was first observed, in ISO 8601 format.
Core.CoreAsset.asset_hierarchy unknown The hierarchy or structure representing the asset.
Core.CoreAsset.xdm__asset__type__category unknown The asset category type.
Core.CoreAsset.xdm__asset__cloud__region unknown The cloud region where the asset resides.
Core.CoreAsset.xdm__asset__module_unstructured_fields unknown The unstructured fields or metadata associated with the asset module.
Core.CoreAsset.xdm__asset__source unknown The originating source of the asset’s information.
Core.CoreAsset.xdm__asset__id unknown The source unique identifier for the asset.
Core.CoreAsset.xdm__asset__type__class unknown The classification or type class of the asset.
Core.CoreAsset.xdm__asset__type__name unknown The specific name of the asset type.
Core.CoreAsset.xdm__asset__strong_id unknown The strong or immutable identifier for the asset.
Core.CoreAsset.xdm__asset__name unknown The name of the asset.
Core.CoreAsset.xdm__asset__raw_fields unknown The raw fields or unprocessed data related to the asset.
Core.CoreAsset.xdm__asset__normalized_fields unknown The normalized fields associated with the asset.
Core.CoreAsset.all_sources unknown A list of all sources providing information about the asset.
Command Example

!core-get-asset-details asset_id=123

Context Example
{
    "Core.CoreAsset": [
        {
            "asset_hierarchy": ["123"],
            "xdm__asset__type__category": "Policy",
            "xdm__asset__cloud__region": "Global",
            "xdm__asset__module_unstructured_fields": {},
            "xdm__asset__source": "XSIAM",
            "xdm__asset__id": "123",
            "xdm__asset__type__class": "Identity",
            "xdm__asset__normalized_fields": {},
            "xdm__asset__first_observed": 100000000,
            "xdm__asset__last_observed": 100000000,
            "xdm__asset__name": "Fake Name",
            "xdm__asset__type__name": "IAM",
            "xdm__asset__strong_id": "FAKE ID"
        }
    ]
}
Human Readable Output
asset_hierarchy xdm__asset__type__category xdm__asset__cloud__region xdm__asset__module_unstructured_fields xdm__asset__source xdm__asset__id xdm__asset__type__class xdm__asset__normalized_fields xdm__asset__first_observed xdm__asset__last_observed xdm__asset__name xdm__asset__type__name xdm__asset__strong_id
123 Policy Global   XSIAM 123 Identity   100000000 100000000 Fake Name IAM FAKE ID

core-get-issues


Returns a list of issues and their metadata, which you can filter by built-in arguments or use the custom_filter to input a JSON filter object.
Multiple filter arguments will be concatenated using the AND operator, while arguments that support a comma-separated list of values will use an OR operator between each value.

Base Command

core-get-issues

Input

Argument Name Description Required
issue_id The unique ID of the issue. Accepts a comma-separated list. Optional
severity The severity of the issue. Accepts a comma-separated list. Possible values are: low, medium, high, critical. Optional
custom_filter A custom filter. When using this argument, other filter arguments are not relevant. example:
{<br/> "OR": [<br/> {<br/> "SEARCH_FIELD": "actor_process_command_line",<br/> "SEARCH_TYPE": "EQ",<br/> "SEARCH_VALUE": "path_to_file"<br/> }<br/> ]<br/> }.
Optional
Identity_type Account type. Accepts a comma-separated list. Possible values are: ANONYMOUS, APPLICATION, COMPUTE, FEDERATED_IDENTITY, SERVICE, SERVICE_ACCOUNT, TEMPORARY_CREDENTIALS, TOKEN, UNKNOWN, USER. Optional
agent_id A unique identifier per agent. Accepts a comma-separated list. Optional
action_external_hostname The hostname to connect to. In case of a proxy connection, this value will differ from action_remote_ip. Accepts a comma-separated list. Optional
rule_id A string identifying the user rule. Accepts a comma-separated list. Optional
rule_name The name of the user rule. Accepts a comma-separated list. Optional
issue_name The issue name. Accepts a comma-separated list. Optional
issue_source The issue source. Accepts a comma-separated list. Possible values are: XDR Agent, XDR Analytics, XDR Analytics BIOC, PAN NGFW, XDR BIOC, XDR IOC, Threat Intelligence, XDR Managed Threat Hunting, Correlation, Prisma Cloud, Prisma Cloud Compute, ASM, IoT Security, Custom Alert, Health, SaaS Attachments, Attack Path, Cloud Network Analyzer, IaC Scanner, CAS Secret Scanner, CI/CD Risks, CLI Scanner, CIEM Scanner, API Traffic Monitor, API Posture Scanner, Agentless Disk Scanner, Kubernetes Scanner, Compute Policy, CSPM Scanner, CAS CVE Scanner, CAS License Scanner, Secrets Scanner, SAST Scanner, Data Policy, Attack Surface Test, Package Operational Risk, Vulnerability Policy, AI Security Posture. Optional
time_frame This argument is deprecated. Use start_time instead. Supports relative or custom time options. If you choose custom, use the start_time and end_time arguments. Possible values are: 60 minutes, 3 hours, 12 hours, 24 hours, 2 days, 7 days, 14 days, 30 days, custom. Optional
user_name The name assigned to the user_id during agent runtime. Accepts a comma-separated list. Optional
actor_process_image_name The file name of the binary file. Accepts a comma-separated list. Optional
causality_actor_process_image_command_line SHA256 Causality Graph Object command line. Accepts a comma-separated list. Optional
actor_process_image_command_line Command line used by the process image initiated by the causality actor. Accepts a comma-separated list. Optional
action_process_image_command_line SHA256 The command line of the process created. Accepts a comma-separated list. Optional
actor_process_image_sha256 SHA256 hash of the binary file. Accepts a comma-separated list. Optional
causality_actor_process_image_sha256 SHA256 hash of the binary file. Accepts a comma-separated list. Optional
action_process_image_sha256 SHA256 of the binary file. Accepts a comma-separated list. Optional
action_file_image_sha256 SHA256 of the file related to the event. Accepts a comma-separated list. Optional
action_registry_name The name of the registry. Accepts a comma-separated list. Optional
action_registry_key_data The key data of the registry. Accepts a comma-separated list. Optional
host_ip The host IP address. Accepts a comma-separated list. Optional
action_local_ip The local IP address for the connection. Accepts a comma-separated list. Optional
action_remote_ip Remote IP address for the connection. Accepts a comma-separated list. Optional
issue_action_status Issue action status. Possible values are: detected, detected (allowed the session), detected (download), detected (forward), detected (post detected), detected (prompt allow), detected (raised an alert), detected (reported), detected (on write), detected (scanned), detected (sinkhole), detected (syncookie sent), detected (wildfire upload failure), detected (wildfire upload success), detected (wildfire upload skip), detected (xdr managed threat hunting), prevented (block), prevented (blocked), prevented (block-override), prevented (blocked the url), prevented (blocked the ip), prevented (continue), prevented (denied the session), prevented (dropped all packets), prevented (dropped the session), prevented (dropped the session and sent a tcp reset), prevented (dropped the packet), prevented (override), prevented (override-lockout), prevented (post detected), prevented (prompt block), prevented (random-drop), prevented (silently dropped the session with an icmp unreachable message to the host or application), prevented (terminated the session and sent a tcp reset to both sides of the connection), prevented (terminated the session and sent a tcp reset to the client), prevented (terminated the session and sent a tcp reset to the server), prevented (on write). Optional
action_local_port The local port for the connection. Accepts a comma-separated list. Optional
action_remote_port The remote port for the connection. Accepts a comma-separated list. Optional
dst_action_external_hostname The hostname to connect to. In case of a proxy connection, this value will differ from action_remote_ip. Accepts a comma-separated list. Optional
sort_field The field by which to sort the results. Default is source_insert_ts. Optional
sort_order The order in which to sort the results. Possible values are: DESC, ASC. Optional
offset This argument is deprecated. Use page instead. The first page number to retrieve issues from. Default is 0. Optional
limit This argument is deprecated. Use page_size instead. The last page number to retrieve issues from. Default is 50. Optional
page The page number for the issues to return for pagination. Default is 0. Optional
page_size The number of issues to return per page. Default is 50. Optional
start_time Relevant when the time_frame argument is set to custom. Supports epoch timestamp and simplified extended ISO format (YYYY-MM-DDThh:mm:ss). Optional
end_time Relevant when the time_frame argument is set to custom. Supports epoch timestamp and simplified extended ISO format (YYYY-MM-DDThh:mm:ss). Optional
starred Whether the issue is starred. Possible values are: true, false. Optional
mitre_technique_id_and_name The MITRE attack technique. Accepts a comma-separated list. Optional
issue_category The category of the issue. Accepts a comma-separated list. Optional
issue_domain The domain of the issue. Accepts a comma-separated list. Possible values are: Health, Hunting, IT, Posture, Security. Optional
issue_description The description of the issue. Accepts a comma-separated list. Optional
os_actor_process_image_sha256 The SHA256 hash of the OS actor process image. Accepts a comma-separated list. Optional
action_file_macro_sha256 The SHA256 hash of the action file macro. Accepts a comma-separated list. Optional
status The status progress. Accepts a comma-separated list. Possible values are: New, In Progress, Resolved. Optional
not_status Not status progress. Accepts a comma-separated list. Possible values are: New, In Progress, Resolved. Optional
asset_ids The assets IDs related to the issue. Accepts a comma-separated list. Optional
assignee The assignee of the issue. Accepts a comma-separated list.
Use “unassigned” for unassigned issues or “assigned” for all assigned issues.
.
Optional
output_keys A comma separated list of outputs to include in the context. Optional

Context Output

Path Type Description
Core.Issue.internal_id String The unique ID of the issue.
Core.Issue.Identity_type String The identity type of the account.
Core.Issue.source_insert_ts Number The detection timestamp.
Core.Issue.issue_name String The name of the issue.
Core.Issue.issue_category String The category of the issue.
Core.Issue.issue_description String The issue description.
Core.Issue.agent_id List The agent IDs associated with the issue.
Core.Issue.asset_ids List The asset IDs related to the issue.
Core.Issue.severity String The severity of the issue.
Core.Issue.issue_domain String The domain of the issue.
Core.Issue.case_ids List The case IDs associated with the issue.
Core.Issue.issue_source String The source of the issue.
Core.Issue.starred Boolean Whether the issue is starred.
Core.Issue.status.progress String The progress status of the issue.
Core.Issue.assigned_to_pretty String The pretty name of the user assigned to the issue.
Core.Issue.assigned_to String The user assigned to the issue.
Core.Issue.agent_ip_addresses String The host IP address.
Core.Issue.agent_hostname String The hostname.
Core.Issue.mitre_tactic_id_and_name String The MITRE attack tactic.
Core.Issue.mitre_technique_id_and_name String The MITRE attack technique.
Core.Issue.issue_action_status String The issue action status.
Core.Issue.issue_action_status_readable String The issue action status in readable format.
Core.Issue.action_file_macro_sha256 String File Macro SHA256 hash of the action file macro.
Core.Issue.action_process_image_sha256 String Action process image SHA256 hash.
Core.Issue.causality_actor_process_image_sha256 String Causality actor process image SHA256 hash.
Core.Issue.os_actor_process_image_sha256 String OS Parent SHA256 hash of the OS actor process image.
Core.Issue.actor_process_image_sha256 String Actor process image SHA256 hash.
Core.IssueMetadata.returned_count Number The actual number of issues that match all filter criteria and returned in this specific response.
Core.IssueMetadata.filtered_count Number The total number of issues in the system that match all filter criteria.

core-get-case-extra-data


Get extra data fields of a specific case including issues and key artifacts.

Base Command

core-get-case-extra-data

Input

Argument Name Description Required
case_id A comma seperated list of case IDs. Required
issues_limit Maximum number of issues to return per case. The default and maximum is 1000. Default is 1000. Optional

Context Output

Path Type Description
Core.CaseExtraData.case.case_id String The unique identifier for the case.
Core.CaseExtraData.case.case_name String The name assigned to the case.
Core.CaseExtraData.case.creation_time Number The timestamp (in epoch format) when the case was created.
Core.CaseExtraData.case.modification_time Number The timestamp (in epoch format) when the case was last modified.
Core.CaseExtraData.case.detection_time String The timestamp when the activity related to the case was first detected.
Core.CaseExtraData.case.status String The current status of the case (e.g., ‘new’, ‘under_investigation’, ‘closed’).
Core.CaseExtraData.case.severity String The severity level of the case (e.g., ‘low’, ‘medium’, ‘high’, ‘critical’).
Core.CaseExtraData.case.description String A detailed textual description of the case.
Core.CaseExtraData.case.assigned_user_mail String The email address of the user assigned to the case.
Core.CaseExtraData.case.assigned_user_pretty_name String The display name of the user assigned to the case.
Core.CaseExtraData.case.issue_count Number The total number of issues associated with the case.
Core.CaseExtraData.case.low_severity_issue_count Number The total number of low-severity issues within the case.
Core.CaseExtraData.case.med_severity_issue_count Number The total number of medium-severity issues within the case.
Core.CaseExtraData.case.high_severity_issue_count Number The total number of high-severity issues within the case.
Core.CaseExtraData.case.critical_severity_issue_count Number The total number of critical-severity issues within the case.
Core.CaseExtraData.case.user_count Number The number of unique users involved in the case.
Core.CaseExtraData.case.host_count Number The number of unique hosts involved in the case.
Core.CaseExtraData.case.notes Array A collection of notes or comments added to the case by analysts.
Core.CaseExtraData.case.resolve_comment String The comment entered by a user when resolving the case.
Core.CaseExtraData.case.manual_severity String The severity level manually set by a user, which may override the calculated severity for the case.
Core.CaseExtraData.case.manual_description String A description of the case that was manually entered by a user.
Core.CaseExtraData.case.xdr_url String The direct URL to view the case in the XDR platform.
Core.CaseExtraData.case.starred Boolean A flag indicating whether the case has been starred or marked as a favorite.
Core.CaseExtraData.case.hosts Array A comma-separated list of hostnames involved in the case.
Core.CaseExtraData.case.case_sources String The products or sources that contributed issues to this case (e.g., ‘XDR Agent’, ‘Firewall’).
Core.CaseExtraData.case.rule_based_score Number The case’s risk score as calculated by automated detection rules.
Core.CaseExtraData.case.manual_score Number A risk score manually assigned to the case by a user.
Core.CaseExtraData.case.wildfire_hits Number The number of times a file associated with this case was identified as malicious by WildFire.
Core.CaseExtraData.case.issues_grouping_status String The current status of the issue grouping or clustering process for this case.
Core.CaseExtraData.case.mitre_techniques_ids_and_names String A list of MITRE ATT&CK technique IDs and names observed in the case.
Core.CaseExtraData.case.mitre_tactics_ids_and_names String A list of MITRE ATT&CK tactic IDs and names observed in the case.
Core.CaseExtraData.case.issue_categories String A comma-separated list of categories for the issues included in the case.
Core.CaseExtraData.issues.total_count Number The total number of individual issues that are part of the case.
Core.CaseExtraData.issues.data.external_id String The unique external identifier for an individual issue.
Core.CaseExtraData.issues.data.severity String The severity of the individual issue.
Core.CaseExtraData.issues.data.matching_status String The correlation status for the issue.
Core.CaseExtraData.issues.data.end_match_attempt_ts Date The timestamp of the last attempt to match the issue with others.
Core.CaseExtraData.issues.data.local_insert_ts Date The timestamp when the issue was first recorded in the system.
Core.CaseExtraData.issues.data.bioc_indicator String The specific Behavioral Indicator of Compromise (BIOC) that triggered the issue.
Core.CaseExtraData.issues.data.matching_service_rule_id String The ID of the matching service rule that identified the issue.
Core.CaseExtraData.issues.data.attempt_counter Number The number of times a matching attempt has been made for this issue.
Core.CaseExtraData.issues.data.bioc_category_enum_key String The key representing the category of the Behavioral Indicator of Compromise (BIOC).
Core.CaseExtraData.issues.data.case_id Number The ID of the case to which this issue belongs.
Core.CaseExtraData.issues.data.is_whitelisted Boolean A flag indicating whether this issue has been whitelisted or suppressed.
Core.CaseExtraData.issues.data.starred Boolean A flag indicating whether this individual issue has been starred.
Core.CaseExtraData.issues.data.deduplicate_tokens String Tokens used to identify and deduplicate similar issues.
Core.CaseExtraData.issues.data.filter_rule_id String The ID of any filter rule that was applied to this issue.
Core.CaseExtraData.issues.data.mitre_technique_id_and_name String The specific MITRE ATT&CK technique ID and name associated with the issue.
Core.CaseExtraData.issues.data.mitre_tactic_id_and_name String The specific MITRE ATT&CK tactic ID and name associated with the issue.
Core.CaseExtraData.issues.data.agent_version String The version of the agent installed on the endpoint related to the issue.
Core.CaseExtraData.issues.data.agent_device_domain String The domain of the endpoint device.
Core.CaseExtraData.issues.data.agent_fqdn String The fully qualified domain name (FQDN) of the agent’s host.
Core.CaseExtraData.issues.data.agent_os_type String The operating system type of the endpoint (e.g., ‘Windows’, ‘Linux’).
Core.CaseExtraData.issues.data.agent_os_sub_type String The specific version or distribution of the agent’s operating system.
Core.CaseExtraData.issues.data.agent_data_collection_status String The status of the agent’s data collection process.
Core.CaseExtraData.issues.data.mac String The primary MAC address of the endpoint.
Core.CaseExtraData.issues.data.mac_addresses Array A list of all MAC addresses associated with the endpoint.
Core.CaseExtraData.issues.data.agent_is_vdi Boolean A flag indicating whether the agent is installed on a Virtual Desktop Infrastructure (VDI) instance.
Core.CaseExtraData.issues.data.agent_install_type String The installation type of the agent.
Core.CaseExtraData.issues.data.agent_host_boot_time Date The last boot time of the host where the agent is installed.
Core.CaseExtraData.issues.data.event_sub_type String A more specific classification of the event type.
Core.CaseExtraData.issues.data.module_id String The identifier of the agent module that generated the event.
Core.CaseExtraData.issues.data.association_strength Number A score indicating the strength of the event’s association to the case.
Core.CaseExtraData.issues.data.dst_association_strength Number The association strength related to the destination entity in the event.
Core.CaseExtraData.issues.data.story_id String An identifier that groups a sequence of related events into a “story”.
Core.CaseExtraData.issues.data.event_id String The unique identifier for the event.
Core.CaseExtraData.issues.data.event_type String The primary type of the event (e.g., ‘Process Execution’, ‘Network Connection’).
Core.CaseExtraData.issues.data.events_length Number The number of raw events that were aggregated to create this issue.
Core.CaseExtraData.issues.data.event_timestamp Date The timestamp when the original event occurred.
Core.CaseExtraData.issues.data.actor_process_instance_id String The unique instance ID of the primary actor process.
Core.CaseExtraData.issues.data.actor_process_image_path String The full file path of the actor process’s executable.
Core.CaseExtraData.issues.data.actor_process_image_name String The filename of the actor process’s executable.
Core.CaseExtraData.issues.data.actor_process_command_line String The command line used to launch the actor process.
Core.CaseExtraData.issues.data.actor_process_signature_status String The digital signature status of the actor process executable (e.g., ‘Signed’, ‘Unsigned’).
Core.CaseExtraData.issues.data.actor_process_signature_vendor String The vendor name from the digital signature of the actor process.
Core.CaseExtraData.issues.data.actor_process_image_sha256 String The SHA256 hash of the actor process executable.
Core.CaseExtraData.issues.data.actor_process_image_md5 String The MD5 hash of the actor process executable.
Core.CaseExtraData.issues.data.actor_process_causality_id String The causality ID of the actor process, which links it to its parent process.
Core.CaseExtraData.issues.data.actor_causality_id String The causality ID of the primary actor in the event.
Core.CaseExtraData.issues.data.actor_process_os_pid String The operating system’s Process ID (PID) of the actor process.
Core.CaseExtraData.issues.data.actor_thread_thread_id String The ID of the specific thread within the actor process that initiated the action.
Core.CaseExtraData.issues.data.causality_actor_process_image_name String The image name of the process that initiated the actor process (the grandparent).
Core.CaseExtraData.issues.data.causality_actor_process_command_line String The command line of the causality actor process.
Core.CaseExtraData.issues.data.causality_actor_process_image_path String The file path of the causality actor process’s executable.
Core.CaseExtraData.issues.data.causality_actor_process_signature_vendor String The signature vendor of the causality actor process.
Core.CaseExtraData.issues.data.causality_actor_process_signature_status String The signature status of the causality actor process.
Core.CaseExtraData.issues.data.causality_actor_causality_id String The causality ID of the causality actor process.
Core.CaseExtraData.issues.data.causality_actor_process_execution_time Date The execution timestamp of the causality actor process.
Core.CaseExtraData.issues.data.causality_actor_process_image_md5 String The MD5 hash of the causality actor process’s executable.
Core.CaseExtraData.issues.data.causality_actor_process_image_sha256 String The SHA256 hash of the causality actor process’s executable.
Core.CaseExtraData.issues.data.action_file_path String The file path of the file that was the target of an action.
Core.CaseExtraData.issues.data.action_file_name String The name of the file that was the target of an action.
Core.CaseExtraData.issues.data.action_file_md5 String The MD5 hash of the file that was the target of an action.
Core.CaseExtraData.issues.data.action_file_sha256 String The SHA256 hash of the file that was the target of an action.
Core.CaseExtraData.issues.data.action_file_macro_sha256 String The SHA256 hash of a macro embedded within the target file.
Core.CaseExtraData.issues.data.action_registry_data String The data written to or read from a registry value during the action.
Core.CaseExtraData.issues.data.action_registry_key_name String The name of the registry key involved in the action.
Core.CaseExtraData.issues.data.action_registry_value_name String The name of the registry value involved in the action.
Core.CaseExtraData.issues.data.action_registry_full_key String The full path of the registry key involved in the action.
Core.CaseExtraData.issues.data.action_local_ip String The local IP address involved in a network action.
Core.CaseExtraData.issues.data.action_local_port String The local port number involved in a network action.
Core.CaseExtraData.issues.data.action_remote_ip String The remote IP address involved in a network action.
Core.CaseExtraData.issues.data.action_remote_port String The remote port number involved in a network action.
Core.CaseExtraData.issues.data.action_external_hostname String The external hostname or domain associated with the network action.
Core.CaseExtraData.issues.data.action_country String The country associated with the remote IP address in the network action.
Core.CaseExtraData.issues.data.action_process_instance_id String The instance ID of the process that was the target of an action.
Core.CaseExtraData.issues.data.action_process_causality_id String The causality ID of the target process.
Core.CaseExtraData.issues.data.action_process_image_name String The executable name of the target process.
Core.CaseExtraData.issues.data.action_process_image_sha256 String The SHA256 hash of the target process’s executable.
Core.CaseExtraData.issues.data.action_process_image_command_line String The command line of the target process.
Core.CaseExtraData.issues.data.action_process_signature_status String The signature status of the target process.
Core.CaseExtraData.issues.data.action_process_signature_vendor String The signature vendor of the target process.
Core.CaseExtraData.issues.data.os_actor_effective_username String The effective username of the OS-level actor responsible for the event.
Core.CaseExtraData.issues.data.os_actor_process_instance_id String The instance ID of the OS actor process.
Core.CaseExtraData.issues.data.os_actor_process_image_path String The file path of the OS actor process’s executable.
Core.CaseExtraData.issues.data.os_actor_process_image_name String The image name of the OS actor process.
Core.CaseExtraData.issues.data.os_actor_process_command_line String The command line of the OS actor process.
Core.CaseExtraData.issues.data.os_actor_process_signature_status String The signature status of the OS actor process.
Core.CaseExtraData.issues.data.os_actor_process_signature_vendor String The signature vendor of the OS actor process.
Core.CaseExtraData.issues.data.os_actor_process_image_sha256 String The SHA256 hash of the OS actor process’s executable.
Core.CaseExtraData.issues.data.os_actor_process_causality_id String The causality ID of the OS actor process.
Core.CaseExtraData.issues.data.os_actor_causality_id String The causality ID of the OS actor.
Core.CaseExtraData.issues.data.os_actor_process_os_pid String The operating system PID of the OS actor process.
Core.CaseExtraData.issues.data.os_actor_thread_thread_id String The thread ID of the OS actor.
Core.CaseExtraData.issues.data.fw_app_id String The firewall application ID for the traffic.
Core.CaseExtraData.issues.data.fw_interface_from String The firewall interface from which the traffic originated.
Core.CaseExtraData.issues.data.fw_interface_to String The firewall interface to which the traffic was destined.
Core.CaseExtraData.issues.data.fw_rule String The name of the firewall rule that matched the traffic.
Core.CaseExtraData.issues.data.fw_rule_id String The unique ID of the firewall rule that matched the traffic.
Core.CaseExtraData.issues.data.fw_device_name String The name of the firewall device that logged the event.
Core.CaseExtraData.issues.data.fw_serial_number String The serial number of the firewall device.
Core.CaseExtraData.issues.data.fw_url_domain String The domain visited, as logged by the firewall.
Core.CaseExtraData.issues.data.fw_email_subject String The subject line of an email, as logged by the firewall.
Core.CaseExtraData.issues.data.fw_email_sender String The sender of an email, as logged by the firewall.
Core.CaseExtraData.issues.data.fw_email_recipient String The recipient of an email, as logged by the firewall.
Core.CaseExtraData.issues.data.fw_app_subcategory String The application subcategory as identified by the firewall.
Core.CaseExtraData.issues.data.fw_app_category String The application category as identified by the firewall.
Core.CaseExtraData.issues.data.fw_app_technology String The application technology as identified by the firewall.
Core.CaseExtraData.issues.data.fw_vsys String The virtual system on the firewall that processed the traffic.
Core.CaseExtraData.issues.data.fw_xff String The X-Forwarded-For (XFF) header value from the traffic.
Core.CaseExtraData.issues.data.fw_misc String Miscellaneous firewall log data.
Core.CaseExtraData.issues.data.fw_is_phishing Boolean A flag indicating if the firewall identified the event as phishing.
Core.CaseExtraData.issues.data.dst_agent_id String The agent ID of the destination endpoint in a lateral movement event.
Core.CaseExtraData.issues.data.dst_causality_actor_process_execution_time Date The execution time of the causality actor process on the destination endpoint.
Core.CaseExtraData.issues.data.dns_query_name String The domain name in a DNS query event.
Core.CaseExtraData.issues.data.dst_action_external_hostname String The external hostname of the destination.
Core.CaseExtraData.issues.data.dst_action_country String The country of the destination.
Core.CaseExtraData.issues.data.dst_action_external_port String The external port of the destination.
Core.CaseExtraData.issues.data.issue_id String The unique identifier for the issue.
Core.CaseExtraData.issues.data.detection_timestamp Number The timestamp when the issue was first detected.
Core.CaseExtraData.issues.data.name String The name or title of the issue.
Core.CaseExtraData.issues.data.category String The category of the issue.
Core.CaseExtraData.issues.data.endpoint_id String The unique ID of the endpoint where the issue occurred.
Core.CaseExtraData.issues.data.description String A detailed description of the issue.
Core.CaseExtraData.issues.data.host_ip String The IP address of the host related to the issue.
Core.CaseExtraData.issues.data.host_name String The hostname of the endpoint related to the issue.
Core.CaseExtraData.issues.data.source String The source of the issue (e.g., ‘XDR’).
Core.CaseExtraData.issues.data.action String The action taken in response to the event (e.g., ‘detected’, ‘prevented’).
Core.CaseExtraData.issues.data.action_pretty String A user-friendly representation of the action taken.
Core.CaseExtraData.issues.data.user_name String The name of the user associated with the issue.
Core.CaseExtraData.issues.data.contains_featured_host Boolean A flag indicating if the issue involves a host marked as featured or critical.
Core.CaseExtraData.issues.data.contains_featured_user Boolean A flag indicating if the issue involves a user marked as featured or critical.
Core.CaseExtraData.issues.data.contains_featured_ip_address Boolean A flag indicating if the issue involves an IP address marked as featured or critical.
Core.CaseExtraData.issues.data.tags String Any tags that have been applied to the issue.
Core.CaseExtraData.issues.data.original_tags String The original set of tags applied to the issue before any modifications.
Core.CaseExtraData.network_artifacts.total_count Number The total number of network artifacts associated with the case.
Core.CaseExtraData.network_artifacts.data.type String The type of network artifact (e.g., ‘IP Address’, ‘Domain’).
Core.CaseExtraData.network_artifacts.data.issue_count Number The number of issues in the case that involve this network artifact.
Core.CaseExtraData.network_artifacts.data.is_manual Boolean A flag indicating if the network artifact was added manually by a user.
Core.CaseExtraData.network_artifacts.data.network_domain String The domain name of the network artifact.
Core.CaseExtraData.network_artifacts.data.network_remote_ip String The remote IP address of the network artifact.
Core.CaseExtraData.network_artifacts.data.network_remote_port String The remote port number of the network artifact.
Core.CaseExtraData.network_artifacts.data.network_country String The country associated with the network artifact’s IP address.
Core.CaseExtraData.file_artifacts.total_count Number The total number of file artifacts associated with the case.
Core.CaseExtraData.file_artifacts.data.issue_count Number The number of issues in the case that involve this file artifact.
Core.CaseExtraData.file_artifacts.data.file_name String The name of the file artifact.
Core.CaseExtraData.file_artifacts.data.File_sha256 String The SHA256 hash of the file artifact.
Core.CaseExtraData.file_artifacts.data.file_signature_status String The digital signature status of the file artifact.
Core.CaseExtraData.file_artifacts.data.file_wildfire_verdict String The verdict from WildFire for this file (e.g., ‘malicious’, ‘benign’).
Core.CaseExtraData.file_artifacts.data.is_malicous Boolean A flag indicating whether the file artifact is considered malicious.
Core.CaseExtraData.file_artifacts.data.is_manual Boolean A flag indicating if the file artifact was added manually by a user.
Core.CaseExtraData.file_artifacts.data.is_process Boolean A flag indicating if the file artifact is a process executable.
Core.CaseExtraData.file_artifacts.data.low_confidence Boolean A flag indicating if the verdict on the file artifact has low confidence.
Core.CaseExtraData.file_artifacts.data.type String The type of the file artifact.

core-get-cases


Get case information based on the specified filters.

Base Command

core-get-cases

Input

Argument Name Description Required
case_id_list A comma-separated list of case IDs to filter by. Optional
sort_by_creation_time Sorts returned cases by the date/time that the case was created (“asc” - ascending, “desc” - descending). Possible values are: ASC, DESC. Optional
sort_by_modification_time Sorts returned cases by the date/time that the case was modified (“asc” - ascending, “desc” - descending). Possible values are: ASC, DESC. Optional
page Page number (for pagination). The default is 0 (the first page). Default is 0. Optional
limit Maximum number of cases to return per page. The default and maximum value is 100. Default is 100. Optional
case_domain A comma-separated list of domains to filter cases by. Possible values are: DOMAIN_SECURITY, DOMAIN_POSTURE, DOMAIN_IT, DOMAIN_HEALTH, DOMAIN_HUNTING. Optional
status A comma-separated list of case statuses to filter cases by.
. Possible values are: new, in_progress, resolved.
Optional
not_status A comma-separated list of statuses to exclude. Possible values are: new, in_progress, resolved. Optional
severity A comma-separated list of severity levels to filter cases by.
. Possible values are: low, medium, high, critical.
Optional
asset_ids A comma-separated list of Asset IDs associated with the case by which to filter the cases. Optional
asset_groups A comma-separated list of Asset Group IDs, where the case is filtered by the assets contained within those groups. Optional
hosts A comma-separated list of hosts to filter cases by. Optional
assignee A comma-separated list of assignee names or emails to filter cases by. Note: all values must be either names or emails - mixing both in the same request is not supported. Use the special values: “assigned” - to get all cases with assignees, “unassigned” - to get cases with no assignees. Optional
starred Filter cases by whether they are starred or not. Possible values are: true, false. Optional
case_name A comma-separated list of names to filter cases by. Optional
case_description A comma-separated list of descriptions to filter cases by. Optional
lte_creation_time A datetime with the format 2019-12-31T23:59:00. Only cases that were created on or before the specified datetime will be retrieved. Optional
gte_creation_time A datetime with the format 2019-12-31T23:59:00. Only cases that were created on or after the specified datetime will be retrieved. Optional
since_creation_time Filters for returned cases that were created on or after the specified date range, for example, 1 month, 2 days, 1 hour, and so on. Optional
lte_modification_time Filters for returned cases that were created on or before the specified datetime with the format 2019-12-31T23:59:00. Optional
gte_modification_time Filters for returned cases that were modified on or after the specified datetime with the format 2019-12-31T23:59:00. Optional
since_modification_time Filters for returned cases that were modified on or after the specified date range, for example, 1 month, 2 days, 1 hour, and so on. Optional
get_enriched_case_data Whether to include enriched case data in the response (detection_time, notes, xdr_url, manual_description, starred_manually). Only supported for up to 10 cases. When more than 10 cases are returned, the command automatically falls back to standard case data. Possible values are: true, false. Default is false. Optional
tags A comma-separated list of tags to filter cases by. Optional

Context Output

Path Type Description
Core.Case.case_id String Unique ID assigned to each returned case.
Core.Case.case_name String Name of the case.
Core.Case.creation_time Number Timestamp when the case was created.
Core.Case.modification_time Number Timestamp when the case was last modified.
Core.Case.status String Current status of the case.
Core.Case.severity String Severity level of the case.
Core.Case.description String Description of the case.
Core.Case.assigned_user_mail String Email address of the assigned user. May be null.
Core.Case.assigned_user_pretty_name String Full name of the assigned user. May be null.
Core.Case.issue_count Number Total number of issues in the case.
Core.Case.low_severity_issue_count Number Number of issues with low severity.
Core.Case.med_severity_issue_count Number Number of issues with medium severity.
Core.Case.high_severity_issue_count Number Number of issues with high severity.
Core.Case.critical_severity_issue_count Number Number of issues with critical severity.
Core.Case.user_count Number Number of users involved in the case.
Core.Case.host_count Number Number of hosts involved in the case.
Core.Case.resolve_comment String Comments added when resolving the case. May be null.
Core.Case.resolve_reason String The reason for resolving the case (e.g. known_issue, duplicate, false_positive, other, true_positive, security_testing). May be null.
Core.Case.resolved_timestamp Number Timestamp when the case was resolved.
Core.Case.manual_severity Number Severity manually assigned by the user. May be null.
Core.Case.starred Boolean Indicates whether the case is starred.
Core.Case.hosts Array List of hosts involved in the case.
Core.Case.users Array List of users involved in the case.
Core.Case.case_sources Array Sources of the case.
Core.Case.manual_score Number Manually assigned score. May be null.
Core.Case.rule_based_score Number Score based on rules.
Core.Case.wildfire_hits Number Number of WildFire hits.
Core.Case.issues_grouping_status String Status of issue grouping.
Core.Case.mitre_tactics_ids_and_names Array List of MITRE ATT&CK tactic IDs and names associated with the case.
Core.Case.mitre_techniques_ids_and_names Array List of MITRE ATT&CK technique IDs and names associated with the case.
Core.Case.issue_categories Array Categories of issues associated with the case.
Core.Case.original_tags Array Original tags assigned to the case.
Core.Case.tags Array Current tags assigned to the case.
Core.Case.case_domain String Domain associated with the case.
Core.Case.custom_fields Unknown Custom fields for the case with standardized lowercase, whitespace-free names.
Core.Case.CaseExtraData.issue_ids Array List of issue IDs associated with the case.
Core.Case.CaseExtraData.file_artifacts Array File artifacts associated with the case.
Core.Case.CaseExtraData.network_artifacts Array Network artifacts associated with the case.
Core.Case.CaseExtraData.starred_manually Boolean True if the case was starred manually; false if starred by rules.
Core.Case.CaseExtraData.xdr_url String URL to view the case in Cortex XDR.
Core.Case.CaseExtraData.manual_description String Description manually provided by the user.
Core.Case.CaseExtraData.notes String The notes related to the case.
Core.Case.CaseExtraData.detection_time Date The timestamp when the first issue was detected in the case.
Core.CasesMetadata.returned_count Number The actual number of cases that match all filter criteria and returned in this specific response.
Core.CasesMetadata.filtered_count Number The total number of cases in the system that match all filter criteria.

core-update-case


Updates the properties of a case.

Base Command

core-update-case

Input

Argument Name Description Required
case_id A comma-separated list of case IDs to update. Required
case_name The new name for the case. Optional
description The new description for the case. Optional
assignee The email address of the new assignee. Use “unassigned” to remove an existing assignee. Optional
status The new status for the case. Possible values are: new, in_progress, resolved. Optional
notes Additional notes for the case. Optional
starred Whether the case should be starred. Possible values are: true, false. Optional
user_defined_severity The user-defined severity for the case. Possible values are: low, medium, high, critical. Optional
resolve_reason The reason for resolving the case. Only relevant when status is set to resolved. Possible values are: known_issue, duplicate, false_positive, true_positive, security_testing, other. Optional
resolved_comment Comment when resolving the case. Only relevant when status is set to resolved. Optional
resolve_all_alerts Whether to resolve all alerts associated with the case. Only relevant when status is set to resolved. Possible values are: true, false. Optional
custom_fields A JSON encoded string representing custom field name-value pairs to update. (e.g., {"field1": "value1", "multiselect_field": ["a", "b"]}). Optional

Context Output

Path Type Description
Core.Case.modifiedBy String User who last modified the case.
Core.Case.notes String Notes associated with the case.
Core.Case.userSeverity String User-defined severity for the case.
Core.Case.name.isUser Boolean Whether the case name is user-defined.
Core.Case.name.value String The name of the case.
Core.Case.creationTime Number The creation time of the case in milliseconds.
Core.Case.lastUpdateTime Number The last update time of the case in milliseconds.
Core.Case.topCounters.HOSTS Number Number of hosts in the case.
Core.Case.topCounters.MAL_ARTIFACTS Number Number of malicious artifacts in the case.
Core.Case.topCounters.USERS Number Number of users in the case.
Core.Case.assigned.mail String Email address of the assigned user.
Core.Case.assigned.pretty String Display name of the assigned user.
Core.Case.internalStatus String Internal status of the case.
Core.Case.status.resolveComment String Comment when resolving the case.
Core.Case.status.resolve_reason String Reason for resolving the case.
Core.Case.status.value String Status value of the case.
Core.Case.severityCounters.SEV_020_LOW Number Number of low severity alerts in the case.
Core.Case.severityCounters.SEV_030_MEDIUM Number Number of medium severity alerts in the case.
Core.Case.severityCounters.SEV_040_HIGH Number Number of high severity alerts in the case.
Core.Case.severityCounters.SEV_050_CRITICAL Number Number of critical severity alerts in the case.
Core.Case.caseDomain String Domain of the case.
Core.Case.groupingStatus.pretty String Pretty display of grouping status.
Core.Case.groupingStatus.raw String Raw grouping status value.
Core.Case.groupingStatus.reason String Reason for the grouping status.
Core.Case.tags.tag_id String Tag ID associated with the case.
Core.Case.tags.tag_name String Tag name associated with the case.

core-search-asset-groups


Retrieve asset groups from the Cortex platform with optional filtering.

Base Command

core-search-asset-groups

Input

name JSON list of asset groups to search for. (e.g. ["group1", "group2"]). Optional
type Filter asset groups by type. Optional
description JSON list of descriptions to search for. (e.g. ["description1", "description2"]). Optional
limit The maximum number of groups to return. Optional
id Comma separated list of ids to search for. Optional

Context Output

Core.AssetGroups.name String The name of the asset group.
Core.AssetGroups.filter String The filter criteria for the asset group.
Core.AssetGroups.membership_predicate String The predicate used to create the asset group.
Core.AssetGroups.type String The type of the asset group.
Core.AssetGroups.description String The description of the asset group.
Core.AssetGroups.modified_by String The user who modified the asset group.
Core.AssetGroups.modified_by_pretty String The formatted name of the user who created the asset group.
Core.AssetGroups.created_by String The user who created the asset group.
Core.AssetGroups.created_by_pretty String The formatted name of the user who created the asset group.

core-get-vulnerabilities


Retrieves vulnerabilities based on specified filters.

Base Command

core-get-vulnerabilities

Input

Argument Name Description Required
limit The maximum number of vulnerabilities to return. Default is 50. Optional
sort_field The field by which to sort the results. Possible values are: PLATFORM_SEVERITY, CVSS_SCORE, EPSS_SCORE, CORTEX_VULNERABILITY_RISK_SCORE, LAST_OBSERVED. Default is LAST_OBSERVED. Optional
sort_order The order in which to sort the results. Possible values are: DESC, ASC. Default is DESC. Optional
cve_id The CVE ID. Accepts a comma-separated list. Optional
issue_id The issue ID. Accepts a comma-separated list. Optional
cvss_score_gte The minimum CVSS score. Optional
epss_score_gte The minimum EPSS score. Optional
internet_exposed Filter by internet exposed assets. Possible values are: true, false. Optional
exploitable Filter by exploitable vulnerabilities. Possible values are: true, false. Optional
has_kev Filter by vulnerabilities that have a Known Exploited Vulnerability (KEV). Possible values are: true, false. Optional
affected_software Filter by affected software. Accepts a comma-separated list. Optional
on_demand_fields A comma-separated list of additional fields to retrieve. Optional
start_time The start time for filtering according to case creation time. Supports free-text relative and absolute times. For example: 7 days ago, 2023-06-15T10:30:00Z, 13/8/2025. Optional
end_time The end time for filtering according to case creation time. Supports free-text relative and absolute times. For example: 7 days ago, 2023-06-15T10:30:00Z, 13/8/2025. Optional
severity The severity of the vulnerability issue. Possible values are: info, low, medium, high, critical. Optional
assignee The email of the user assigned to the vulnerability. Accepts a comma-separated list. Use ‘unassigned’ for unassigned vulnerabilities or ‘assigned’ for all assigned vulnerabilities. Optional
finding_sources The finding sources of the vulnerability. Accepts a comma-separated list. Possible values are: CORTEX_AGENT, CORTEX_AGENTLESS_SCANNER, CORTEX_ATTACK_SURFACE_MANAGEMENT, CORTEX_ATTACK_SURFACE_TESTING, CORTEX_CLI_SCANNER, CORTEX_CONTAINER_REGISTRY_SCANNER, CORTEX_NETWORK_SCANNER, CORTEX_SERVERLESS_FUNCTION_SCANNER, QUALYS, TENABLE. Optional
cvrs_gte The minimum risk score assigned to the vulnerability (range 0-100). Optional
compensating_controls_effective_coverage The assessed effectiveness and coverage of detected compensating controls. Possible values are: EFFECTIVE, EFFECTIVE_REQUIRES_CONFIGURATION_UPDATE, EFFECTIVE_REQUIRES_CONTENT_UPDATE, EXPLOIT_CONFIRMED, EXPLOIT_UNREACHABLE, NOT_INSTALLED, NO_CONTROLS_FOUND, UNKNOWN_COVERAGE. Optional

Context Output

Path Type Description
Core.VulnerabilityIssue.ISSUE_ID String The unique identifier for the vulnerability issue.
Core.VulnerabilityIssue.CVE_ID String The CVE identifier for the vulnerability.
Core.VulnerabilityIssue.CVE_DESCRIPTION String The description of the CVE.
Core.VulnerabilityIssue.ASSET_NAME String The name of the affected asset.
Core.VulnerabilityIssue.PLATFORM_SEVERITY String The severity of the vulnerability as determined by the platform.
Core.VulnerabilityIssue.EPSS_SCORE Number The Exploit Prediction Scoring System (EPSS) score.
Core.VulnerabilityIssue.CVSS_SCORE Number The Common Vulnerability Scoring System (CVSS) score.
Core.VulnerabilityIssue.ASSIGNED_TO String The email of the user assigned to the vulnerability.
Core.VulnerabilityIssue.ASSIGNED_TO_PRETTY String The full name of the user assigned to the vulnerability.
Core.VulnerabilityIssue.AFFECTED_SOFTWARE Unknown The software affected by the vulnerability.
Core.VulnerabilityIssue.FIX_AVAILABLE Boolean Indicates if a fix is available for the vulnerability.
Core.VulnerabilityIssue.INTERNET_EXPOSED Boolean Indicates if the asset is exposed to the internet.
Core.VulnerabilityIssue.HAS_KEV Boolean Indicates if the vulnerability is a Known Exploited Vulnerability (KEV).
Core.VulnerabilityIssue.EXPLOITABLE Boolean Indicates if the vulnerability is exploitable.
Core.VulnerabilityIssue.ASSET_IDS String The unique identifier for the asset.
Core.VulnerabilityIssue.FINDING_SOURCES String The finding sources that originally generated the security finding of the vulnerability.
Core.VulnerabilityIssue.COMPENSATING_CONTROLS_DETECTED_COVERAGE String The coverage status of detected compensating controls, mirroring the input parameter enum values.
Core.VulnerabilityIssue.CORTEX_VULNERABILITY_RISK_SCORE Number The risk score assigned to the vulnerability.
Core.VulnerabilityIssue.FIX_VERSIONS Array The package versions that contain a fix for the vulnerability.
Core.VulnerabilityIssue.ASSET_TYPES Array The types of assets affected by the vulnerability.
Core.VulnerabilityIssue.COMPENSATING_CONTROLS_DETECTED_CONTROLS Array The compensating controls that were detected for the vulnerability.
Core.VulnerabilityIssue.EXPLOIT_LEVEL String The exploitability level or status of the vulnerability.
Core.VulnerabilityIssue.ISSUE_NAME String The name of the vulnerability issue.
Core.VulnerabilityIssue.PACKAGE_IN_USE Boolean Indicates whether the vulnerable package is actively used in the environment.
Core.VulnerabilityIssue.PROVIDERS Array The providers or sources of the vulnerability information.
Core.VulnerabilityIssue.OS_FAMILY String The operating system family of the affected asset.
Core.VulnerabilityIssue.IMAGE String Information related to the affected container or system image.

core-search-assets


Retrieves asset from the Cortex platform using optional filter criteria.

Base Command

core-search-assets

Input

Argument Name Description Required
page_size The number of assets to return per page. Default is 100. Maximum is 5000. Optional
page_number The page number for the assets to return for pagination. Default is 0. Optional
asset_names Comma-separated list of asset names to search for. (e.g., “asset_name1,asset_name2”). Optional
asset_types Comma-separated list of asset types to search for. (e.g., “asset_type1,asset_type2”). Optional
asset_tags A JSON encoded string representing a list of tag:value pairs to search for. (e.g., [{"tag1": "value1"}, {"tag2": "value2"}]).
.
Optional
asset_ids Comma-separated list of asset IDs to search for. (e.g., “asset_id1,asset_id2”). Optional
asset_providers Comma-separated list of asset providers to search for. (e.g., “provider1,provider2”). Optional
asset_realms Comma-separated list of asset realms to search for. (e.g., “realm1,realm2”). Optional
asset_groups A JSON encoded string representing a list of asset groups to search for. (e.g., ["group1", "group2"]).
.
Optional
asset_categories A Comma-separated list of asset categories to search for. (e.g., “category1,category2”). Optional
asset_classes A comma-separated list of asset classes to search for. Possible values are: AI, API, Application, Code, Compute, Data, Device, External Surface, Identity, Management, Network, Organization, Other, Security Services. Optional
software_package_versions A comma-separated list of software package versions to search for. (e.g., “0.23.0,5.2.0”). Optional
kubernetes_cluster_versions A comma-separated list of Kubernetes cluster versions to search for. (e.g., “1.22,1.3”). Optional

Context Output

Path Type Description
Core.Asset.external_provider_id unknown The external provider ID of the asset.
Core.Asset.first_observed unknown The first time the asset was observed.
Core.Asset.tags unknown The tags of the asset.
Core.Asset.realm unknown The realm of the asset.
Core.Asset.type.id unknown The ID of the asset type.
Core.Asset.related_issues.critical_issues unknown Critical issues related to the asset.
Core.Asset.id unknown The ID of the asset.
Core.Asset.last_observed unknown The last time the asset was observed.
Core.Asset.type.category unknown The category of the asset type.
Core.Asset.related_cases.critical_cases unknown Critical cases related to the asset.
Core.Asset.group_ids unknown The group IDs of the asset.
Core.Asset.type.class unknown The class of the asset type.
Core.Asset.related_issues.issues_breakdown unknown The related issues breakdown of the asset.
Core.Asset.type.name unknown The type of the asset.
Core.Asset.name unknown The name of the asset.
Core.Asset.strong_id unknown The strong ID of the asset.
Core.Asset.cloud.region unknown The cloud region of the asset.
Core.Asset.related_cases.cases_breakdown unknown The related cases breakdown of the asset.
Core.Asset.provider unknown The asset provider.
Core.Asset.kubernetes.cluster.version unknown The Kubernetes cluster version of the asset.
Core.Asset.software_package.version unknown The software package version of the asset.

core-get-issue-recommendations


Get comprehensive recommendations for an issue, including remediation steps, playbook suggestions, and recommended actions.

Base Command

core-get-issue-recommendations

Input

Argument Name Description Required
issue_ids Comma-separated list of IDs of the issues to get recommendations for (maximum 10 per request). Required

Context Output

Path Type Description
Core.IssueRecommendations.issue_id String The unique identifier for the issue.
Core.IssueRecommendations.issue_name String The name of the issue.
Core.IssueRecommendations.severity String The severity of the issue.
Core.IssueRecommendations.description String Description of the issue.
Core.IssueRecommendations.remediation String Remediation steps and recommendations for the issue.
Core.IssueRecommendations.playbook_suggestions.playbook_id String The ID of the suggested playbook.
Core.IssueRecommendations.playbook_suggestions.suggestion_rule_id String The ID of the suggestion rule that generated this recommendation.
Core.IssueRecommendations.playbook_suggestions.name String The name of the suggested playbook.
Core.IssueRecommendations.playbook_suggestions.comment String An explanation of the suggested playbook.
Core.IssueRecommendations.quick_action_suggestions.name String The name of the suggested quick action.
Core.IssueRecommendations.quick_action_suggestions.suggestion_rule_id String The ID of the suggestion quick action rule that generated this recommendation.
Core.IssueRecommendations.quick_action_suggestions.brand String The brand of the quick action.
Core.IssueRecommendations.quick_action_suggestions.category String The category of the quick action.
Core.IssueRecommendations.quick_action_suggestions.description String An explanation of the quick action.
Core.IssueRecommendations.quick_action_suggestions.pretty_name String The display name of the quick action.
Core.IssueRecommendations.quick_action_suggestions.arguments.name String The argument name.
Core.IssueRecommendations.quick_action_suggestions.arguments.prettyName String The argument display name.
Core.IssueRecommendations.quick_action_suggestions.arguments.prettyPredefined String The argument predefined display value.
Core.IssueRecommendations.quick_action_suggestions.arguments.description String The argument description.
Core.IssueRecommendations.quick_action_suggestions.arguments.required String Whether the argument is required.
Core.IssueRecommendations.existing_code_block String Original vulnerable code.
Core.IssueRecommendations.suggested_code_block String Code block fix suggestion.
Core.IssueRecommendations.network_reachability Json The Network reachability information for the issue.

core-enable-scanners


Enable or disable scanners with the specified configuration.

Base Command

core-enable-scanners

Input

Argument Name Description Required
asset_ids List of repository asset IDs to configure scanners for. Required
enable_scanners List of scanners to enable. Possible values are: SECRETS, IAC, SCA. Optional
disable_scanners List of scanners to disable. Possible values are: SECRETS, IAC, SCA. Optional
secret_validation Enable live validation of discovered secrets. Possible values are: true, false. Optional
pr_scanning Enable scanning on pull requests. This argument only relevant when SECRETS scanner is enabled. Possible values are: true, false. Optional
block_on_error Block deployment on scanner errors. Possible values are: true, false. Optional
tag_resource_blocks Enable tagging of resource blocks. Possible values are: true, false. Optional
tag_module_blocks Enable tagging of module blocks. Possible values are: true, false. Optional
exclude_paths List of file paths to exclude from scanning. Optional

core-get-asset-coverage-histogram


Calculates the distribution of values (counts and percentages) for specified categorical fields.

Base Command

core-get-asset-coverage-histogram

Input

Argument Name Description Required
asset_id The unique ID of the asset. Accepts a comma-separated list. Optional
asset_name The name of the asset. Accepts a comma-separated list. Optional
business_application_names Business application names. Accepts a comma-separated list. Optional
status_coverage The status coverage. Accepts a comma-separated list. Possible values are: FULLY SCANNED, NOT SCANNED, PARTIALLY SCANNED. Optional
is_scanned_by_vulnerabilities Is scanned by vulnerabilities. Accepts a comma-separated list. Possible values are: DISABLED, ENABLED, IRRELEVANT. Optional
is_scanned_by_code_weakness Is scanned by code weakness. Accepts a comma-separated list. Possible values are: DISABLED, ENABLED, IRRELEVANT. Optional
is_scanned_by_secrets Is scanned by secrets. Accepts a comma-separated list. Possible values are: DISABLED, ENABLED, IRRELEVANT. Optional
is_scanned_by_iac Is scanned by IaC. Accepts a comma-separated list. Possible values are: DISABLED, ENABLED, IRRELEVANT. Optional
is_scanned_by_malware Is scanned by malware. Accepts a comma-separated list. Possible values are: DISABLED, ENABLED, IRRELEVANT. Optional
is_scanned_by_cicd Is scanned by CICD. Accepts a comma-separated list. Possible values are: DISABLED, ENABLED, IRRELEVANT. Optional
last_scan_status The last scan status. Accepts a comma-separated list. Possible values are: NOT_SCANNED_YET, ERROR, COMPLETED. Optional
asset_type The asset type. Accepts a comma-separated list. Possible values are: CICD PIPELINE, CONTAINER IMAGE REPOSITORY, REPOSITORY. Optional
asset_provider The asset provider. Accepts a comma-separated list. Possible values are: AWS, AWS_CODE_BUILD, AWS_CODE_COMMIT, AZURE, AZURE_PIPELINES, AZURE_REPOS, BITBUCKET, CIRCLE_CI, DOCKER, GCP, GITHUB, GITHUB_ACTIONS, GITLAB, GITLAB_CI, HCP_TFC_RUN_TASKS, JENKINS, JFROG_ARTIFACTORY, OCI. Optional
vendor_name The vendor name. Accepts a comma-separated list. Possible values are: AWS, AWS_CODE_BUILD, AWS_CODE_COMMIT, AZURE, AZURE_REPOS, BITBUCKET, BITBUCKET_DATACENTER, CIRCLE_CI, DOCKER, GCP, GITHUB, GITHUB_ACTIONS, GITHUB_ENTERPRISE, GITLAB, GITLAB_SELF_MANAGED, HCP_TFC_RUN_TASKS, HCP_TFE_RUN_TASKS, JENKINS, JFROG_ARTIFACTORY, OCI. Optional
max_values_per_column The maximum number of distinct values to return for each column. Default is 100. Optional
columns A list of fields for which to generate histograms. Possible values are: asset_name, business_application_names, status_coverage, is_scanned_by_vulnerabilities, is_scanned_by_code_weakness, is_scanned_by_secrets, is_scanned_by_iac, is_scanned_by_malware, is_scanned_by_cicd, last_scan_status, asset_type, asset_provider, vendor_name. Required

Context Output

Path Type Description
Core.Coverage.Histogram.column_name String The column over which the histogram is generated.
Core.Coverage.Histogram.data.value String The distinct value.
Core.Coverage.Histogram.data.count Number The number of records with this value after filtering.
Core.Coverage.Histogram.data.percentage Number The percentage of filtered records with this value.
Core.Coverage.Histogram.data.pretty_name String A user-friendly label for the value.

core-get-asset-coverage


Retrieves a list of assets (e.g., Repositories, CI/CD Pipelines, Container Image Repositories) along with their scan coverage status.

Base Command

core-get-asset-coverage

Input

Argument Name Description Required
asset_id The unique ID of the asset. Accepts a comma-separated list. Optional
asset_name The name of the asset. Accepts a comma-separated list. Optional
business_application_names Business application names. Accepts a comma-separated list. Optional
status_coverage The status coverage. Accepts a comma-separated list. Possible values are: FULLY SCANNED, NOT SCANNED, PARTIALLY SCANNED. Optional
is_scanned_by_vulnerabilities Is scanned by vulnerabilities. Accepts a comma-separated list. Possible values are: DISABLED, ENABLED, IRRELEVANT. Optional
is_scanned_by_code_weakness Is scanned by code weakness. Accepts a comma-separated list. Possible values are: DISABLED, ENABLED, IRRELEVANT. Optional
is_scanned_by_secrets Is scanned by secrets. Accepts a comma-separated list. Possible values are: DISABLED, ENABLED, IRRELEVANT. Optional
is_scanned_by_iac Is scanned by IaC. Accepts a comma-separated list. Possible values are: DISABLED, ENABLED, IRRELEVANT. Optional
is_scanned_by_malware Is scanned by malware. Accepts a comma-separated list. Possible values are: DISABLED, ENABLED, IRRELEVANT. Optional
is_scanned_by_cicd Is scanned by CICD. Accepts a comma-separated list. Possible values are: DISABLED, ENABLED, IRRELEVANT. Optional
last_scan_status The last scan status. Accepts a comma-separated list. Possible values are: NOT_SCANNED_YET, ERROR, COMPLETED. Optional
asset_type The asset type. Accepts a comma-separated list. Possible values are: CICD PIPELINE, CONTAINER IMAGE REPOSITORY, REPOSITORY. Optional
asset_provider The asset provider. Accepts a comma-separated list. Possible values are: AWS, AWS_CODE_BUILD, AWS_CODE_COMMIT, AZURE, AZURE_PIPELINES, AZURE_REPOS, BITBUCKET, CIRCLE_CI, DOCKER, GCP, GITHUB, GITHUB_ACTIONS, GITLAB, GITLAB_CI, HCP_TFC_RUN_TASKS, JENKINS, JFROG_ARTIFACTORY, OCI. Optional
vendor_name The vendor name. Accepts a comma-separated list. Possible values are: AWS, AWS_CODE_BUILD, AWS_CODE_COMMIT, AZURE, AZURE_REPOS, BITBUCKET, BITBUCKET_DATACENTER, CIRCLE_CI, DOCKER, GCP, GITHUB, GITHUB_ACTIONS, GITHUB_ENTERPRISE, GITLAB, GITLAB_SELF_MANAGED, HCP_TFC_RUN_TASKS, HCP_TFE_RUN_TASKS, JENKINS, JFROG_ARTIFACTORY, OCI. Optional
limit The maximum number of assets to return. Default is 100. Optional
sort_field The field by which to sort the results. Possible values are: asset_id, asset_name, business_application_names, status_coverage, is_scanned_by_vulnerabilities, is_scanned_by_code_weakness, is_scanned_by_secrets, is_scanned_by_iac, is_scanned_by_malware, is_scanned_by_cicd, last_scan_status, asset_type, asset_provider, vendor_name. Optional
sort_order The order in which to sort the results. Possible values are: DESC, ASC. Optional

Context Output

Path Type Description
Core.Coverage.Asset.asset_id String The unique ID of the asset. Each asset is assigned a unique identifier in the system.
Core.Coverage.Asset.asset_name String The name of the asset. Typically corresponds to the repository, container image, or pipeline name.
Core.Coverage.Asset.asset_provider String The vendor or source platform of the asset. Indicates where the asset originates from. Possible values: AWS, AWS_CODE_BUILD, AWS_CODE_COMMIT, AZURE, AZURE_REPOS, BITBUCKET, BITBUCKET_DATACENTER, CIRCLE_CI, DOCKER, GCP, GITHUB, GITHUB_ACTIONS, GITHUB_ENTERPRISE, GITLAB, GITLAB_SELF_MANAGED, HCP_TFC_RUN_TASKS, HCP_TFE_RUN_TASKS, JENKINS, JFROG_ARTIFACTORY, OCI.
Core.Coverage.Asset.asset_type String The type or category of the asset. Determines the nature of the resource being scanned. Possible values: CICD PIPELINE, CONTAINER IMAGE REPOSITORY, REPOSITORY.
Core.Coverage.Asset.business_application_names Array A list of business applications associated with the asset. These applications help map the asset to business context or ownership.
Core.Coverage.Asset.is_scanned_by_cicd String Indicates whether the asset is scanned within CI/CD pipelines. Possible values: ENABLED, DISABLED, IRRELEVANT.
Core.Coverage.Asset.is_scanned_by_code_weakness String Indicates whether code weakness scanning is performed on the asset. Possible values: ENABLED, DISABLED, IRRELEVANT.
Core.Coverage.Asset.is_scanned_by_iac String Indicates whether infrastructure-as-code (IaC) scanning is enabled for the asset. Possible values: ENABLED, DISABLED, IRRELEVANT.
Core.Coverage.Asset.is_scanned_by_malware String Indicates whether malware scanning is enabled for the asset. Possible values: ENABLED, DISABLED, IRRELEVANT.
Core.Coverage.Asset.is_scanned_by_secrets String Indicates whether the asset is scanned for hardcoded secrets or credentials. Possible values: ENABLED, DISABLED, IRRELEVANT.
Core.Coverage.Asset.is_scanned_by_semgrep Boolean Boolean flag indicating whether the asset is analyzed using Semgrep for code issues or misconfigurations. Possible values: true, false.
Core.Coverage.Asset.is_scanned_by_sonarqube Boolean Boolean flag indicating whether the asset is analyzed using SonarQube for code quality and security issues. Possible values: true, false.
Core.Coverage.Asset.is_scanned_by_veracode Boolean Boolean flag indicating whether the asset is scanned using Veracode for security vulnerabilities. Possible values: true, false.
Core.Coverage.Asset.is_scanned_by_vulnerabilities String Indicates whether vulnerability scanning is enabled for the asset. Possible values: ENABLED, DISABLED, IRRELEVANT.
Core.Coverage.Asset.last_scan_status String The status of the most recent scan performed on the asset. Possible values: NOT_SCANNED_YET, ERROR, COMPLETED.
Core.Coverage.Asset.scanners_data Array An array containing detailed information from the scanners that evaluated the asset, including scan results, timestamps, and metadata.
Core.Coverage.Asset.status_coverage String The overall scan coverage of the asset. Possible values: FULLY SCANNED, PARTIALLY SCANNED, NOT SCANNED.
Core.Coverage.Asset.unified_provider String The unified provider name associated with the asset. Standardized across different vendor integrations. Possible values: AWS, AWS_CODE_BUILD, AWS_CODE_COMMIT, AZURE, AZURE_PIPELINES, AZURE_REPOS, BITBUCKET, CIRCLE_CI, DOCKER, GCP, GITHUB, GITHUB_ACTIONS, GITLAB, GITLAB_CI, HCP_TFC_RUN_TASKS, JENKINS, JFROG_ARTIFACTORY, OCI.

core-create-appsec-policy


Creates a new AppSec policy in Cortex Platform with defined conditions, scope, and triggers for application security governance.

Base Command

core-create-appsec-policy

Input

Argument Name Description Required
policy_name A unique name for the AppSec policy. Must be descriptive and follow organizational naming conventions. Required
description A detailed explanation of the policy’s objective, use case, and expected outcomes. Optional
asset_group_names Comma-separated list of Asset Group names to apply the policy to. Asset groups will be automatically resolved to their corresponding IDs. Optional
conditions_finding_type Filter by specific finding types to target policy enforcement. Supported values: Vulnerabilities, IaC Misconfiguration, Licenses, Operational Risk, Secrets, Code Weaknesses, CI/CD Risks. Optional
conditions_severity Filter findings by severity level to prioritize policy actions. Supported values: CRITICAL, HIGH, MEDIUM, LOW. Optional
conditions_respect_developer_suppression Controls whether a developer’s manual suppression should be honored. Set to ‘true’ to respect developer suppression (evaluate only non-suppressed findings). Set to ‘false’ to ignore suppression and always evaluate the finding. Possible values are: true, false. Optional
conditions_backlog_status Filter findings based on their backlog workflow status (NEW or BACKLOG). Possible values are: NEW, BACKLOG. Optional
conditions_package_name Target specific software packages by name for license or vulnerability policies. Optional
conditions_package_version Specify software package version constraints for precise policy targeting. Optional
conditions_package_operational_risk Filter packages by their operational risk assessment level. Supported values: HIGH, MEDIUM, LOW. Optional
conditions_appsec_rule_names Comma-separated list of AppSec rule names to include in policy evaluation. Rule names will be automatically resolved to their corresponding IDs. Optional
conditions_cvss CVSS base score threshold for vulnerability findings (0.0-10.0). Only vulnerabilities meeting or exceeding this score will trigger the policy. Optional
conditions_epss Exploit Prediction Scoring System score threshold (0-100). Targets vulnerabilities with higher exploitation probability. Optional
conditions_has_a_fix Filter findings based on whether a remediation fix or patch is available. Possible values are: true, false. Optional
conditions_is_kev Target findings listed in CISA’s Known Exploited Vulnerabilities (KEV) catalog for prioritized remediation. Possible values are: true, false. Optional
conditions_secret_validity Filter exposed secrets by their validity status. Supported values: VALID (active secrets), PRIVILEGED (high-access secrets), INVALID (expired/revoked), UNAVAILABLE (status unknown). Optional
conditions_license_type Target specific software license types for compliance and legal risk management. Optional
scope_category Define asset categories to include in policy scope. Supported values: Application, Repository, CI/CD Instance, CI/CD Pipeline, VCS Collaborator, VCS Organization. Optional
scope_business_application_names Target specific business applications by name for focused policy enforcement. Optional
scope_application_business_criticality Filter applications by business criticality level. Supported values: CRITICAL, HIGH, MEDIUM, LOW. Optional
scope_repository_name Target specific code repositories by name for repository-focused policies. Optional
scope_is_public_repository Filter repositories based on their visibility (public vs private) for exposure risk management. Possible values are: true, false. Optional
scope_has_deployed_assets Target repositories or applications that have associated deployed infrastructure or runtime assets. Possible values are: true, false. Optional
scope_has_internet_exposed_deployed_assets Filter assets based on whether the deployed components are exposed to internet traffic for external attack surface management. Possible values are: true, false. Optional
scope_has_sensitive_data_access Target deployed assets that have access to sensitive data stores, databases, or classified information. Possible values are: true, false. Optional
scope_has_privileged_capabilities Filter deployed assets with elevated privileges, admin access, or high-impact system capabilities. Possible values are: true, false. Optional
triggers_periodic_report_issue Enables detection during scheduled scans. When a violation is found in a periodic scan, an issue will be created (“Detect”). Possible values are: true, false. Optional
triggers_periodic_override_severity Override the default severity level for issues created by periodic scan detections. Possible values are: Critical, High, Medium, Low. Optional
triggers_pr_report_issue Enables detection during pull request scans. When a violation is found in a PR, an issue is created. Required for PR-based detection. Possible values are: true, false. Optional
triggers_pr_block_pr Blocks merging of pull requests that contain violations detected by the policy. Possible values are: true, false. Optional
triggers_pr_report_pr_comment Adds an automated comment to pull requests summarizing detected violations and guidance. Possible values are: true, false. Optional
triggers_pr_override_severity Override the default severity level for issues created by pull request detections. Possible values are: Critical, High, Medium, Low. Optional
triggers_cicd_report_issue Enables detection during CI/CD pipeline scans. When a violation is found in a pipeline run, an issue is created. Possible values are: true, false. Optional
triggers_cicd_block_cicd Blocks or fails CI/CD pipeline runs when violations occur. Possible values are: true, false. Optional
triggers_cicd_report_cicd Reports violation details back to the CI/CD system (pipeline logs, dashboards, status checks). Possible values are: true, false. Optional
triggers_cicd_override_severity Override the default severity level for issues created by CI/CD pipeline detections. Possible values are: Critical, High, Medium, Low. Optional

Context Output

There is no context output for this command.

core-update-issue


Updates the properties of an issue. This command does not provide an explicit indication of success.

Base Command

core-update-issue

Input

Argument Name Description Required
id Issue ID to update. If empty, updates the current issue ID. Optional
assigned_user_mail Email address of the user to assign the issue to. Optional
severity Change the severity of an issue. Possible values are: low, medium, high, critical. Optional
name Change the issue name. Optional
occurred Change the occurred time of an issue. Supports different time formats, for example: 3 days ago, 2017-09-27T10:00:00+03:00. Optional
phase Change the phase of an issue. Possible values are: Triage, Investigation, Containment, Response. Optional
type Change the type of an issue. Optional
description Change the description of an issue. Optional
status Change the status of an issue. Possible values are: New, In Progress, Resolved - Known Issue, Resolved - Duplicate Issue, Resolved - False Positive, Resolved - other, Resolved - True Positive, Resolved - Security Testing, Resolved - Dismissed, Resolved - Fixed, Resolved - Risk Accepted. Optional

Context Output

There is no context output for this command.

core-appsec-remediate-issue


Create automated pull requests to fix multiple security issues in a single bulk operation.

Base Command

core-appsec-remediate-issue

Input

Argument Name Description Required
issue_ids A comma-separated list of issue IDs to fix (maximum 10 per request). Required
title Custom title for the pull request. Optional

Context Output

Path Type Description
Core.TriggeredPRs.issueId String The issue identifier.
Core.TriggeredPRs.status String Either “triggered” or “automated_fix_not_available”.

core-get-appsec-issues


Retrieves application security issues based on specified filters.

Base Command

core-get-appsec-issues

Input

Argument Name Description Required
limit The maximum number of issues to return. Default is 50. Optional
sort_field The field by which to sort the results. Default is severity. Optional
sort_order The order in which to sort the results. Possible values are: DESC, ASC. Default is DESC. Optional
start_time The start time for filtering according to issue insert time. Supports free-text relative and absolute times. For example: 7 days ago, 2023-06-15T10:30:00Z, 13/8/2025. Optional
end_time The end time for filtering according to issue insert time. Supports free-text relative and absolute times. For example: 7 days ago, 2023-06-15T10:30:00Z, 13/8/2025. Optional
issue_id The issue ID. Accepts a comma-separated list. Optional
assignee The email of the user assigned to the issue. Accepts a comma-separated list.
Use ‘unassigned’ for all unassigned issues or ‘assigned’ for all assigned issues.
.
Optional
collaborator The collaborators of the issue. Accepts a comma-separated list. Optional
status The issue status. Accepts a comma-separated list. Possible values are: New, In Progress, Resolved. Optional
issue_name The issue name. Accepts a comma-separated list. Optional
asset_name The name of the affected asset for the issue. Accepts a comma-separated list. Optional
repository The repository of the issue. Accepts a comma-separated list. Optional
file_path The path of the relevant file for the issue. Accepts a comma-separated list. Optional
backlog_status The backlog status of the issue. Accepts a comma-separated list. Possible values are: BACKLOG, NEW. Optional
cvss_score_gte The minimum CVSS score. Optional
epss_score_gte The minimum EPSS score. Optional
has_kev Filter by vulnerabilities that have a Known Exploited Vulnerability (KEV). Possible values are: true, false. Optional
severity The severity of the issue. Accepts a comma-separated list. Possible values are: info, low, medium, high, critical. Optional
urgency The urgency of the issue. Accepts a comma-separated list. Possible values are: N/A, NOT_URGENT, URGENT, TOP_URGENT. Optional
automated_fix_available Is there an available automated fix. Possible values are: true, false. Optional
sla SLA status of the issue. Accepts a comma-separated list. Possible values are: Approaching, On Track, Overdue. Optional
validation Validation status of the issue. Accepts a comma-separated list. Possible values are: INVALID, NO_VALIDATION, PRIVILEGED, UNAVAILABLE, VALID. Optional

Context Output

Path Type Description
Core.AppsecIssue.internal_id String The unique identifier for the issue.
Core.AppsecIssue.asset_name String The names of the assets related to the issue.
Core.AppsecIssue.severity String The severity of the issue.
Core.AppsecIssue.epss_score Number The Exploit Prediction Scoring System (EPSS) score.
Core.AppsecIssue.cvss_score Number The Common Vulnerability Scoring System (CVSS) score.
Core.AppsecIssue.assignee String The full name of the user assigned to the issue.
Core.AppsecIssue.is_fixable Boolean Whether a fix is available for the issue.
Core.AppsecIssue.issue_name String The name of the issue.
Core.AppsecIssue.issue_source String The source of the issue.
Core.AppsecIssue.issue_category String The category of the issue.
Core.AppsecIssue.issue_domain String The domain of the issue.
Core.AppsecIssue.issue_description String The description of the issue.
Core.AppsecIssue.status String The status of the issue.
Core.AppsecIssue.time_added Number The timestamp when the issue was inserted.
Core.AppsecIssue.urgency String The urgency of the issue.
Core.AppsecIssue.sla_status String The SLA status of the issue.
Core.AppsecIssue.secret_validation String The secret validation status of the issue.
Core.AppsecIssue.repository_name String The name of the repository where the issue was found.
Core.AppsecIssue.repository_organization String The organization of the repository where the issue was found.
Core.AppsecIssue.file_path String The file path related to the issue.
Core.AppsecIssue.collaborator String The collaborator associated with the issue.
Core.AppsecIssue.has_kev Boolean Whether the issue is part of the Known Exploited Vulnerabilities catalog (KEV).
Core.AppsecIssue.backlog_status String The backlog status of the issue.

core-update-endpoint-version


Updates the version of the given endpoint to the target version supplied.

Base Command

core-update-endpoint-version

Input

Argument Name Description Required
endpoint_ids A comma-separated list of endpoint IDs. Required
platform The platform of the endpoints. Possible values are: windows, macos, linux. Required
version The target version for updating the endpoints. Required
start_time The start time for the update. Enter the time in a 24-hour format (HH:MM). Ensure that there are at least two hours between the start time and the end time. Optional
end_time The end time for the update. Enter the time in a 24-hour format (HH:MM). Optional
days A comma-separated list of days of the week the update may run. Possible values are: Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday. Optional

Context Output

Path Type Description
Core.EndpointUpdate.endpoint_ids String The IDs of the endpoints on which the update run.
Core.EndpointUpdate.action_id String The ID of the update action. 0 means that the action failed.

core-get-endpoint-update-version


Retrieves endpoint update versions for the provided endpoint IDs.

Base Command

core-get-endpoint-update-version

Input

Argument Name Description Required
endpoint_ids A comma-separated list of endpoint IDs. Required

Context Output

Path Type Description
Core.EndpointUpdateVersion.total_count String Total number of endpoints.
Core.EndpointUpdateVersion.platform_count String Number of endpoints per platform.
Core.EndpointUpdateVersion.distributions.platform String The platform of the endpoint update.
Core.EndpointUpdateVersion.distributions.version String The version of the endpoint update.
Core.EndpointUpdateVersion.distributions.endpoints_with_higher_version_count String The number of endpoints running a version later than the specified update.
Core.EndpointUpdateVersion.distributions.endpoints_with_same_version_count String The number of endpoints running the same version as the specified update.
Core.EndpointUpdateVersion.distributions.endpoints_with_lower_version_count String The number of endpoints running a version earlier than the specified update.

<~PLATFORM>

License Requirements

The following commands require one of these licenses: Cortex XSIAM, EP / EDR - Endpoint Protection, Cortex Cloud, Cloud Posture (Security), or Cloud Runtime Security:

  • core-list-endpoints

The following commands require one of these licenses: Cortex XSIAM, EP / EDR - Endpoint Protection, or Cloud Runtime Security:

  • core-get-endpoint-support-file
  • core-list-scripts
  • core-run-script-agentix
  • core-get-endpoint-update-version
  • core-update-endpoint-version

The following commands require one of these licenses: Cloud Posture (Security) or Cloud Runtime Security:

  • core-add-assessment-profile
  • core-list-compliance-standards

</~PLATFORM>

Configuration parameters

  • timeout — HTTP Timeout

Commands (40)

  • core-add-assessment-profile

    Create new assessment profile.

  • core-appsec-remediate-issue

    Create automated pull requests to fix multiple security issues in a single bulk operation.

  • core-create-appsec-policy

    Creates a new AppSec policy in Cortex Platform with defined conditions, scope, and triggers for application security governance.

  • core-create-endpoint-policy

    Creates a new endpoint policy and applies it to specified endpoints. Automatically handles priority conflicts by shifting existing policies when needed.

  • core-create-windows-exploit-profile

    Creates a new Windows exploit profile.

  • core-create-windows-malware-profile

    Creates a new windows malware profile.

  • core-delete-endpoint-policy

    Deletes one or more existing endpoint policies from the policy table.

  • core-delete-profile

    Deletes the provided profiles.

  • core-enable-scanners

    Enable or disable scanners with the specified configuration.

  • core-get-ai-model-activity

    Retrieves AI model activity information including usage statistics and inactive status.

  • core-get-appsec-issues

    Retrieves application security issues based on specified filters.

  • core-get-asset-coverage

    Retrieves a list of assets (e.g., Repositories, CI/CD Pipelines, Container Image Repositories) along with their scan coverage status.

  • core-get-asset-coverage-histogram

    Calculates the distribution of values (counts and percentages) for specified categorical fields.

  • core-get-asset-details

    Get asset information.

  • core-get-case-extra-data

    Get extra data fields of a specific case, including issues and key artifacts.

  • core-get-case-resolution-statuses

    Retrieves resolution status information for a specific case.

  • core-get-cases

    Get case information based on the specified filters.

  • core-get-endpoint-support-file

    Retrieves endpoint support files based on specified endpoint IDs.

  • core-get-endpoint-update-version

    Retrieves endpoint update versions for the provided endpoint IDs.

  • core-get-issue-recommendations

    Get comprehensive recommendations for an issue, including remediation steps, playbook suggestions, and recommended actions.

  • core-get-issues

    Returns a list of issues and their metadata, which you can filter by built-in arguments or use the custom_filter to input a JSON filter object. Multiple filter arguments will be concatenated using the AND operator, while arguments that support a comma-separated list of values will use an OR operator between each value.

  • core-get-vulnerabilities

    Retrieves vulnerabilities based on specified filters.

  • core-list-brokers

    Retrieves information about broker VMs configured on the system.

  • core-list-compliance-standards

    Create new assessment profile.

  • core-list-endpoints

    Retrieves endpoints based on the provided filters.

  • core-list-exception-rules

    Returns a list of exception rules and their metadata, which you can filter by built-in arguments. Multiple filter arguments will be concatenated using the AND operator, while arguments that support a comma-separated list of values will use an OR operator between each value.

  • core-list-findings

    Retrieves findings from the Cortex platform.

  • core-list-scripts

    Gets a list of scripts available in the scripts library.

  • core-list-system-users

    Returns a list users and their core properties within the system. Results are limited to 50.

  • core-run-playbook

    Runs a playbook on specific issue IDs.

  • core-run-script-agentix

    Run a script on endpoints.

  • core-search-asset-groups

    Searches for asset groups from the Cortex platform using one or more filter criteria.

  • core-search-assets

    Retrieves asset from the Cortex platform using optional filter criteria.

  • core-send-endpoint-heartbeat

    Sends endpoint heartbeat.

  • core-update-case

    Updates the properties of a case.

  • core-update-endpoint-version

    Updates the version of the given endpoint to the target version supplied.

  • core-update-issue

    Updates the properties of an issue. This command does not provide an explicit indication of success.

  • core-update-windows-exploit-profile

    Updates the specified Windows exploit profile.

  • core-update-windows-malware-profile

    Updates the specified Windows malware profile.

  • core-xql-generic-query-platform

    Execute an XQL query and retrieve a link to the query in the query center. If set to wait for results, the command will be executed every 10 seconds until results are retrieved or until the timeout is reached.

import traceback
from typing import Any

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


# Disable insecure warnings
urllib3.disable_warnings()

TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
INTEGRATION_CONTEXT_BRAND = "Core"
INTEGRATION_NAME = "Cortex Platform Core"
MAX_GET_INCIDENTS_LIMIT = 100
SEARCH_ASSETS_DEFAULT_LIMIT = 100
SEARCH_ASSETS_MAX_LIMIT = 5000
MAX_GET_CASES_LIMIT = 100
MAX_SCRIPTS_LIMIT = 100
MAX_GET_ENDPOINTS_LIMIT = 100
MAX_COMPLIANCE_STANDARDS = 100
AGENTS_TABLE = "AGENTS_TABLE"
BROKER_CLUSTER_TABLE = "BROKER_CLUSTER_TABLE"
AGENT_POLICY_TABLE = "AGENT_POLICY_TABLE"
AGENT_PROFILES_TABLE = "AGENT_PROFILES_TABLE"
SECONDS_IN_DAY = 86400  # Number of seconds in one day
MIN_DIFF_SECONDS = 2 * 3600  # Minimum allowed difference = 2 hours
MAX_GET_SYSTEM_USERS_LIMIT = 50
MAX_GET_EXCEPTION_RULES_LIMIT = 100
MALWARE_TYPE = "Malware"
EXPLOIT_TYPE = "Exploit"
WINDOWS_PLATFORM = "Windows"


ASSET_FIELDS = {
    "asset_names": "xdm__asset__name",
    "asset_types": "xdm__asset__type__name",
    "asset_tags": "xdm__asset__tags",
    "asset_ids": "xdm__asset__id",
    "asset_providers": "xdm__asset__provider",
    "asset_realms": "xdm__asset__realm",
    "asset_group_ids": "xdm__asset__group_ids",
    "asset_categories": "xdm__asset__type__category",
    "asset_classes": "xdm__asset__type__class",
    "software_package_versions": "xdm__software_package__version",
    "kubernetes_cluster_versions": "xdm__kubernetes__cluster__version",
}

APPSEC_SOURCES = [
    "CAS_CVE_SCANNER",
    "CAS_IAC_SCANNER",
    "CAS_SECRET_SCANNER",
    "CAS_LICENSE_SCANNER",
    "CAS_SAST_SCANNER",
    "CAS_OPERATIONAL_RISK_SCANNER",
    "CAS_CI_CD_RISK_SCANNER",
    "CAS_DRIFT_SCANNER",
]
REMEDIATION_TECHNIQUES_SOURCES = ["CIEM_SCANNER", "DATA_POLICY", "AISPM_RULE_ENGINE"]
WEBAPP_COMMANDS = [
    "core-get-vulnerabilities",
    "core-search-asset-groups",
    "core-get-issue-recommendations",
    "core-get-cases",
    "core-update-issue",
    "core-get-asset-coverage",
    "core-get-asset-coverage-histogram",
    "core-create-appsec-policy",
    "core-get-appsec-issues",
    "core-update-case",
    "core-list-scripts",
    "core-run-script-agentix",
    "core-list-endpoints",
    "core-get-issues",
    "core-list-exception-rules",
    "core-get-endpoint-update-version",
    "core-update-endpoint-version",
    "core-get-ai-model-activity",
    "core-update-windows-malware-profile",
    "core-update-windows-exploit-profile",
    "core-delete-profile",
    "core-list-findings",
    "core-list-brokers",
    "core-create-endpoint-policy",
    "core-delete-endpoint-policy",
    "core-search-assets",
]
DATA_PLATFORM_COMMANDS = ["core-get-asset-details"]
APPSEC_COMMANDS = ["core-enable-scanners", "core-appsec-remediate-issue"]
ENDPOINT_COMMANDS = ["core-get-endpoint-support-file", "core-send-endpoint-heartbeat"]
XSOAR_COMMANDS = ["core-run-playbook", "core-get-case-resolution-statuses"]

VULNERABLE_ISSUES_TABLE = "VULNERABLE_ISSUES_TABLE"
ASSET_GROUPS_TABLE = "UNIFIED_ASSET_MANAGEMENT_ASSET_GROUPS"
ASSETS_TABLE = "UNIFIED_ASSET_MANAGEMENT_AGGREGATED_ASSETS"
ASSET_COVERAGE_TABLE = "COVERAGE"
APPSEC_RULES_TABLE = "CAS_DETECTION_RULES"
CASES_TABLE = "CASE_MANAGER_TABLE"
SCRIPTS_TABLE = "SCRIPTS_TABLE"
AI_MODEL_ACTIVITY_TABLE = "AISPM_MODEL_ACTIVITY"
FINDINGS_TABLE = "FINDINGS"


class Profile:
    FIELDS = {
        "examinePortableExecutables": "portable_executables_and_dll_examination",
        "examineOfficeFiles": "office_files_with_macros_examination",
        "examineJScriptFiles": "jscript_file_examination",
        "aspFiles": "asp_aspx_files",
        "powerShellScriptFiles": "powershell_script_files",
        "scanEndpoints": "on_demand_file_examination",
        "endUserInitiatedLocalScan": "end_user_initiated_local_scan",
        "examineVBScriptFiles": "vb_scripts_examination",
        "dynamicSecurityEngine": "global_behavioral_threat_protection_rules",
        "passwordStealing": "credential_gathering_protection",
        "webshellDroppers": "anti_webshell_protection",
        "financialMalwareThreat": "financial_malware_threat_protection",
        "cryptominers": "cryptominers_protection",
        "inProcessShellcode": "in_process_shellcode_protection",
        "maliciousDevice": "malicious_device_prevention",
        "uacBypass": "uac_bypass_prevention",
        "antiTampering": "anti_tampering_protection",
        "iisProtection": "iis_protection",
        "uefiProtection": "uefi_protection",
        "ransomware": "ransomware_protection",
        "legitimateProcesses": "malicious_child_process_protection",
        "passwordTheftProtection": "password_theft_protection",
        "maliciousCausalityChainsResponse": "respond_to_malicious_causality_chains",
        "networkSignature": "network_packet_inspection_engine",
        "dynamicKernelProtection": "dynamic_kernel_protection",
        "dynamicDriverProtection": "dynamic_driver_protection",
        "securityMeasuresBypass": "security_measures_bypass",
        "basTools": "breach_attack_simulation_tools_settings",
        "browserExploitKits": "browser_exploits_protection",
        "logicalExploits": "logical_exploits_protection",
        "vulnerableApps": "known_vulnerable_processes_protection",
        "osKernelExploits": "operating_system_exploit_protection",
        "additionalProcesses": "exploit_protection_for_additional_processes",
        "manualScan": "end_user_initiated_local_scan",  # for update command
    }

    VALIDATION = {
        "asp_aspx_files": ["block", "disabled", "report"],
        "breach_attack_simulation_tools_settings": ["enabled", "disabled"],
        "uac_bypass_prevention": ["block", "disabled", "report"],
        "on_demand_file_examination": ["disabled", "enabled"],
        "end_user_initiated_local_scan": ["disabled", "enabled"],
        "ransomware_protection": ["block", "disabled", "report"],
        "cryptominers_protection": ["block", "disabled", "report"],
        "anti_tampering_protection": ["block", "disabled", "report"],
        "iis_protection": ["block", "disabled", "report"],
        "uefi_protection": ["block", "disabled", "report"],
        "malicious_device_prevention": ["block", "disabled", "report"],
        "network_packet_inspection_engine": ["terminateSession", "disabled", "report"],
        "credential_gathering_protection": ["block", "disabled", "report"],
        "anti_webshell_protection": ["block", "disabled", "report"],
        "office_files_with_macros_examination": ["block", "disabled", "report"],
        "in_process_shellcode_protection": ["block", "disabled", "report"],
        "jscript_file_examination": ["block", "disabled", "report"],
        "malicious_child_process_protection": ["block", "disabled", "report"],
        "vb_scripts_examination": ["block", "disabled", "report"],
        "global_behavioral_threat_protection_rules": ["block", "disabled", "report"],
        "powershell_script_files": ["block", "disabled", "report"],
        "financial_malware_threat_protection": ["block", "disabled", "report"],
        "security_measures_bypass": ["block", "disabled", "report"],
        "dynamic_driver_protection": ["block", "disabled", "report"],
        "dynamic_kernel_protection": ["block", "disabled", "report"],
        "password_theft_protection": ["disabled", "enabled"],
        "portable_executables_and_dll_examination": ["block", "disabled", "report"],
        "respond_to_malicious_causality_chains": ["disabled", "enabled"],
        "browser_exploits_protection": ["block", "disabled", "report"],
        "logical_exploits_protection": ["block", "disabled", "report"],
        "known_vulnerable_processes_protection": ["block", "disabled", "report"],
        "operating_system_exploit_protection": ["block", "disabled", "report"],
        "exploit_protection_for_additional_processes": ["block", "disabled", "report"],
    }


class ScriptManagement:
    FIELDS = {
        "script_name": "NAME",
        "supported_platforms": "PLATFORM",
    }

    PLATFORMS = {
        "windows": "AGENT_OS_WINDOWS",
        "linux": "AGENT_OS_LINUX",
        "macos": "AGENT_OS_MAC",
    }


DISABLE_PREVENTION_RULES_TABLE = "AGENT_EXCEPTION_RULES_TABLE_ADVANCED"
LEGACY_AGENT_EXCEPTIONS_TABLE = "AGENT_EXCEPTION_RULES_TABLE_LEGACY"


CUSTOM_FIELDS_TABLE = "CUSTOM_FIELDS_CASE_TABLE"


class CaseManagement:
    STATUS_RESOLVED_REASON = {
        "known_issue": "STATUS_040_RESOLVED_KNOWN_ISSUE",
        "duplicate": "STATUS_050_RESOLVED_DUPLICATE",
        "false_positive": "STATUS_060_RESOLVED_FALSE_POSITIVE",
        "other": "STATUS_070_RESOLVED_OTHER",
        "true_positive": "STATUS_090_TRUE_POSITIVE",
        "security_testing": "STATUS_100_SECURITY_TESTING",
    }

    STATUS_RESOLVED_REASON_OUTPUT = {v: k for k, v in STATUS_RESOLVED_REASON.items()}

    FIELDS = {
        "case_id_list": "CASE_ID",
        "case_domain": "INCIDENT_DOMAIN",
        "case_name": "NAME",
        "case_description": "DESCRIPTION",
        "status": "STATUS_PROGRESS",
        "severity": "SEVERITY",
        "creation_time": "CREATION_TIME",
        "asset_ids": "UAI_ASSET_IDS",
        "asset_groups": "UAI_ASSET_GROUP_IDS",
        "assignee": "ASSIGNED_USER_PRETTY",
        "assignee_email": "ASSIGNED_USER",
        "name": "CONTAINS",
        "description": "DESCRIPTION",
        "last_updated": "LAST_UPDATE_TIME",
        "hosts": "HOSTS",
        "starred": "CASE_STARRED",
        "tags": "CURRENT_TAGS",
    }

    STATUS = {
        "new": "STATUS_010_NEW",
        "under_investigation": "STATUS_020_UNDER_INVESTIGATION",
        "in_progress": "STATUS_020_UNDER_INVESTIGATION",
        "resolved": "STATUS_025_RESOLVED",
    }

    SEVERITY = {
        "low": "SEV_020_LOW",
        "medium": "SEV_030_MEDIUM",
        "high": "SEV_040_HIGH",
        "critical": "SEV_050_CRITICAL",
    }

    TAGS = {
        "DOM:Security": "DOM:1",
        "DOM:Posture": "DOM:5",
    }


class Endpoints:
    ENDPOINT_TYPE = {
        "mobile": "AGENT_TYPE_MOBILE",
        "server": "AGENT_TYPE_SERVER",
        "workstation": "AGENT_TYPE_WORKSTATION",
        "containerized": "AGENT_TYPE_CONTAINERIZED",
        "serverless": "AGENT_TYPE_SERVERLESS",
    }
    ENDPOINT_STATUS = {
        "connected": "STATUS_010_CONNECTED",
        "lost": "STATUS_020_LOST",
        "disconnected": "STATUS_040_DISCONNECTED",
        "uninstalled": "STATUS_050_UNINSTALLED",
        "vdi pending login": "STATUS_060_VDI_PENDING_LOG_ON",
        "forensics offline": "STATUS_070_FORENSICS_OFFLINE",
    }
    ENDPOINT_PLATFORM = {
        "windows": "AGENT_OS_WINDOWS",
        "mac": "AGENT_OS_MAC",
        "linux": "AGENT_OS_LINUX",
        "android": "AGENT_OS_ANDROID",
        "ios": "AGENT_OS_IOS",
        "serverless": "AGENT_OS_SERVERLESS",
    }
    ENDPOINT_OPERATIONAL_STATUS = {
        "protected": "PROTECTED",
        "partially protected": "PARTIALLY_PROTECTED",
        "unprotected": "UNPROTECTED",
    }
    ASSIGNED_PREVENTION_POLICY = {
        "pcastro": "0a80deae95e84a90a26e0586a7a6faef",
        "Caas Default": "236a259c803d491484fc5f6d0c198676",
        "kris": "31987a7fb890406ca70287c1fc582cbf",
        "democloud": "44fa048803db4a8f989125a3887baf68",
        "Linux Default": "705e7aae722f45c5ab2926e2639b295f",
        "Android Default": "874e0fb9979c44459ca8f2dfdb3f03d9",
        "Serverless Function Default": "c68bb058bbf94bbcb78d748191978d3b",
        "macOS Default": "c9fd93fcee42486fb270ae0acbb7e0fb",
        "iOS Default": "dc2e804c147f4549a6118c96a5b0d710",
        "Windows Default": "e1f6b443a1e24b27955af39b4c425556",
        "bcpolicy": "f32766a625db4cc29b5dddbfb721fe58",
    }
    ENDPOINT_FIELDS = {
        "endpoint_name": "HOST_NAME",
        "endpoint_type": "AGENT_TYPE",
        "endpoint_status": "AGENT_STATUS",
        "platform": "OS_TYPE",
        "operating_system": "OS_DESC",
        "agent_version": "AGENT_VERSION",
        "agent_eol": "SUPPORTED_VERSION",
        "os_version": "OS_VERSION",
        "ip_address": "IP",
        "domain": "DOMAIN",
        "assigned_prevention_policy": "ACTIVE_POLICY",
        "tags": "TAGS",
        "endpoint_id": "AGENT_ID",
        "operational_status": "OPERATIONAL_STATUS",
        "cloud_provider": "CLOUD_PROVIDER",
        "cloud_region": "CLOUD_REGION",
    }


class AppsecIssues:
    class AppsecIssueType:
        def __init__(self, table_name: str, filters: set[str]):
            self.table_name: str = table_name
            self.filters: set = filters or set()

    ISSUE_TYPES = [
        AppsecIssueType(
            "ISSUES_IAC",
            {"urgency", "repository", "file_path", "automated_fix_available", "sla"},
        ),
        AppsecIssueType(
            "ISSUES_CVES",
            {
                "urgency",
                "repository",
                "file_path",
                "automated_fix_available",
                "sla",
                "cvss_score_gte",
                "epss_score_gte",
                "has_kev",
            },
        ),
        AppsecIssueType(
            "ISSUES_SECRETS",
            {"urgency", "repository", "file_path", "sla", "validation"},
        ),
        AppsecIssueType("ISSUES_WEAKNESSES", {"urgency", "repository", "file_path", "sla"}),
        AppsecIssueType("ISSUES_OPERATIONAL_RISK", {"repository", "file_path", "sla"}),
        AppsecIssueType("ISSUES_LICENSES", {"repository", "file_path", "sla"}),
        AppsecIssueType("ISSUES_CI_CD", {"sla"}),
    ]

    SPECIAL_FILTERS = {
        # List of filters that aren't a part of every Appsec table
        "urgency",
        "repository",
        "file_path",
        "automated_fix_available",
        "sla",
        "epss_score_gte",
        "cvss_score_gte",
        "has_kev",
        "validation",
    }

    SEVERITY_MAPPINGS = {
        "info": "SEV_010_INFO",
        "low": "SEV_020_LOW",
        "medium": "SEV_030_MEDIUM",
        "high": "SEV_040_HIGH",
        "critical": "SEV_050_CRITICAL",
        "unknown": "SEV_090_UNKNOWN",
    }

    SEVERITY_OUTPUT_MAPPINGS = {
        "SEV_010_INFO": "info",
        "SEV_020_LOW": "low",
        "SEV_030_MEDIUM": "medium",
        "SEV_040_HIGH": "high",
        "SEV_050_CRITICAL": "critical",
        "SEV_090_UNKNOWN": "unknown",
    }

    STATUS_MAPPINGS = {
        "New": "STATUS_010_NEW",
        "In Progress": "STATUS_020_UNDER_INVESTIGATION",
        "Resolved": "STATUS_025_RESOLVED",
    }

    STATUS_OUTPUT_MAPPINGS = {
        "STATUS_010_NEW": "New",
        "STATUS_020_UNDER_INVESTIGATION": "In Progress",
        "STATUS_025_RESOLVED": "Resolved",
    }

    SLA_MAPPING = {
        "Approaching": "APPROACHING",
        "On Track": "IN_SLA",
        "Overdue": "OVERDUE",
    }

    SLA_OUTPUT_MAPPING = {
        "APPROACHING": "Approaching",
        "IN_SLA": "On Track",
        "OVERDUE": "Overdue",
    }

    URGENCY_OUTPUT_MAPPING = {
        "NOT_URGENT": "Not Urgent",
        "N/A": "N/A",
        "TOP_URGENT": "Top Urgent",
        "URGENT": "Urgent",
    }


ASSET_GROUP_FIELDS = {
    "asset_group_name": "XDM__ASSET_GROUP__NAME",
    "asset_group_type": "XDM__ASSET_GROUP__TYPE",
    "asset_group_description": "XDM__ASSET_GROUP__DESCRIPTION",
    "asset_group_id": "XDM__ASSET_GROUP__ID",
}

VULNERABILITIES_SEVERITY_MAPPING = {
    "info": "SEV_030_INFO",
    "low": "SEV_040_LOW",
    "medium": "SEV_050_MEDIUM",
    "high": "SEV_060_HIGH",
    "critical": "SEV_070_CRITICAL",
}

ALLOWED_SCANNERS = [
    "SCA",
    "IAC",
    "SECRETS",
]

EXCEPTION_RULES_TYPE_TO_TABLE_MAPPING = {
    "legacy_agent_exceptions": LEGACY_AGENT_EXCEPTIONS_TABLE,
    "disable_prevention_rules": DISABLE_PREVENTION_RULES_TABLE,
}
# Policy finding type mapping
POLICY_FINDING_TYPE_MAPPING = {
    "CI/CD Risk": "CAS_CI_CD_RISK_SCANNER",
    "Vulnerabilities": "CAS_CVE_SCANNER",
    "IaC Misconfiguration": "CAS_IAC_SCANNER",
    "Licenses": "CAS_LICENSE_SCANNER",
    "Operational Risk": "CAS_OPERATIONAL_RISK_SCANNER",
    "Secrets": "CAS_SECRET_SCANNER",
    "Weaknesses": "CAS_SAST_SCANNER",
}


# Policy category mapping
POLICY_CATEGORY_MAPPING = {
    "Application": "APPLICATION",
    "Repository": "REPOSITORY",
    "CI/CD Instance": "CICD_INSTANCE",
    "CI/CD Pipeline": "CICD_PIPELINE",
    "VCS Collaborator": "VCS_COLLABORATOR",
    "VCS Organization": "VCS_ORGANIZATION",
}

EXCEPTION_RULES_OUTPUT_FIELDS_TO_MAP = {"MODULES", "PROFILE_IDS"}


DAYS_MAPPING = {
    "sunday": 1,
    "monday": 2,
    "tuesday": 3,
    "wednesday": 4,
    "thursday": 5,
    "friday": 6,
    "saturday": 7,
}

# Mapping from human-readable issue type names to table names.
ISSUE_TYPE_TABLE_MAPPING: dict[str, str] = {
    "vulnerabilities": "ISSUES_CVES",
    "secrets": "ISSUES_SECRETS",
    "iac": "ISSUES_IAC",
    "weaknesses": "ISSUES_WEAKNESSES",
    "operational_risk": "ISSUES_OPERATIONAL_RISK",
    "licenses": "ISSUES_LICENSES",
    "cicd": "ISSUES_CI_CD",
}


def replace_substring(data: dict | str, original: str, new: str) -> str | dict:
    """
    Replace all occurrences of a substring in the keys of a dictionary with a new substring or in a string.

    Args:
        data (dict | str): The dictionary to replace keys in.
        original (str): The substring to be replaced.
        new (str): The substring to replace with.

    Returns:
        dict: The dictionary with all occurrences of `original` replaced by `new` in its keys.
    """

    if isinstance(data, str):
        return data.replace(original, new)
    if isinstance(data, dict):
        for key in list(data.keys()):
            if isinstance(key, str) and original in key:
                new_key = key.replace(original, new)
                data[new_key] = data.pop(key)
    return data


def determine_assignee_filter_field(assignee_list: list) -> str:
    """
    Determine whether the assignee should be filtered by email or pretty name.

    Args:
        assignee (list): The assignee values to filter on.

    Returns:
        str: The appropriate field to filter on based on the input.
    """
    if not assignee_list:
        return CaseManagement.FIELDS["assignee"]

    assignee = assignee_list[0]

    if "@" in assignee:
        # If the assignee contains '@', use the email field
        return CaseManagement.FIELDS["assignee_email"]
    else:
        # Otherwise, use the pretty name field
        return CaseManagement.FIELDS["assignee"]


def process_case_response(resp):
    """
    Process case response by removing unnecessary fields.

    Args:
        resp (dict): Response dictionary to be processed

    Returns:
        dict: Cleaned response dictionary
    """
    fields_to_remove = ["layoutId", "layoutRuleName", "sourcesList"]

    reply = resp.get("reply", {})

    for field in fields_to_remove:
        reply.pop(field, None)

    # Remove nested score values
    if "score" in reply and isinstance(reply["score"], dict):
        reply["score"].pop("previous_score_source", None)
        reply["score"].pop("previous_score", None)

    if "incidentDomain" in reply:
        reply["caseDomain"] = reply.pop("incidentDomain")

    return reply


def issue_to_alert(args: dict | str) -> dict | str:
    return replace_substring(args, "issue", "alert")


def alert_to_issue(output: dict | str) -> dict | str:
    return replace_substring(output, "alert", "issue")


def incident_to_case(output: dict | str) -> dict | str:
    return replace_substring(output, "incident", "case")


def case_to_incident(args: dict | str) -> dict | str:
    return replace_substring(args, "case", "incident")


def arg_to_float(arg: Optional[str]):
    """
    Converts an XSOAR argument to a Python float
    """

    if arg is None or arg == "":
        return None

    arg = encode_string_results(arg)

    if isinstance(arg, str):
        try:
            return float(arg)
        except Exception:
            raise ValueError(f'"{arg}" is not a valid number')

    if isinstance(arg, int | float):
        return arg

    raise ValueError(f'"{arg}" is not a valid number')


def preprocess_get_cases_args(args: dict):
    demisto.debug(f"original args: {args}")
    args["limit"] = min(int(args.get("limit", MAX_GET_INCIDENTS_LIMIT)), MAX_GET_INCIDENTS_LIMIT)
    args = issue_to_alert(case_to_incident(args))
    demisto.debug(f"after preprocess_get_cases_args args: {args}")
    return args


def preprocess_get_cases_outputs(outputs: list | dict):
    def process(output: dict | str):
        return alert_to_issue(incident_to_case(output))

    if isinstance(outputs, list):
        return [process(o) for o in outputs]
    return process(outputs)


def preprocess_get_case_extra_data_outputs(outputs: list | dict):
    def process(output: dict | str):
        if isinstance(output, dict):
            if "incident" in output:
                output["incident"] = alert_to_issue(incident_to_case(output.get("incident", {})))
            alerts_data = output.get("alerts", {}).get("data", {})
            modified_alerts_data = [alert_to_issue(incident_to_case(alert)) for alert in alerts_data]
            if "alerts" in output and isinstance(output["alerts"], dict):
                output["alerts"]["data"] = modified_alerts_data
        return alert_to_issue(incident_to_case(output))

    if isinstance(outputs, list):
        return [process(o) for o in outputs]
    return process(outputs)


def filter_context_fields(output_keys: list, context: list) -> list:
    """
    Filters only specific keys from the context dictionary where values are not None.
    """
    return [{k: v for k in output_keys if (v := alert.get(k)) is not None} for alert in context]


class Client(CoreClient):
    def platform_http_request(
        self,
        method,
        url_suffix="",
        json_data=None,
        params=None,
        data=None,
        timeout=None,
        ok_codes=None,
        error_handler=None,
        with_metrics=False,
    ):
        """A wrapper for the platformAPICall method to better handle requests and responses.

        Args:
            method (str): The HTTP method, for example: GET, POST, and so on.
            url_suffix (str): The API endpoint suffix to append to the base URL.
            json_data (dict, optional): Dictionary to send in the request body as JSON.
                Will be automatically serialized to JSON string.
            params (dict, optional): URL parameters to specify the query string.
            data (str, optional): Raw data to send in the request body.
                Used when json_data is not provided.
            timeout (float or tuple, optional): The amount of time (in seconds) that a request
                will wait for a client to establish a connection to a remote machine before
                a timeout occurs. Can be only float (Connection Timeout) or a tuple
                (Connection Timeout, Read Timeout).
            ok_codes (list, optional): List of HTTP status codes that are considered successful.
                If the response status is not in this list, an error will be raised.
            error_handler (callable, optional): Custom error handler function to process errors.
            with_metrics (bool): Whether to include metrics in error handling.

        Returns:
            dict or str: The parsed JSON response as a dictionary, or the raw response data
                if JSON parsing fails.

        Raises:
            DemistoException: If FORWARD_USER_RUN_RBAC is not enabled, indicating the integration
                is cloned or the server version is too low.
        """
        data = json.dumps(json_data) if json_data is not None else data

        response = demisto._platformAPICall(path=url_suffix, method=method, params=params, data=data, timeout=timeout)

        if ok_codes and response.get("status") not in ok_codes:
            self._handle_error(error_handler, response, with_metrics)
        try:
            return json.loads(response["data"])
        except json.JSONDecodeError:
            demisto.debug(f"Converting data to json was failed. Return it as is. The data's type is {type(response['data'])}")
            return response["data"]

    def test_module(self):
        """
        Performs basic get request to get health_check samples
        """
        try:
            self.get_health_check()
        except Exception as err:
            if "API request Unauthorized" in str(err):
                # this error is received from the Core server when the client clock is not in sync to the server
                raise DemistoException(f"{err!s} please validate that your both XSOAR and Core server clocks are in sync")
            else:
                raise

    def get_asset_details(self, asset_id):
        reply = self._http_request(
            method="POST",
            json_data={"asset_id": asset_id},
            headers=self._headers,
            url_suffix="/unified-asset-inventory/get_asset/",
        )

        return reply

    def update_issue(self, filter_data):
        return self._http_request(method="POST", json_data=filter_data, url_suffix="/alerts/update_alerts")

    def link_issue_to_cases(self, issue_id, case_ids: list) -> dict:
        """Link an issue to one or more cases.

        Args:
            issue_id: The issue ID to link
            case_ids: List of case IDs to link the issue to

        Returns:
            dict: API response
        """
        return self._http_request(
            method="POST", json_data={"issue_ids": [issue_id], "case_ids": case_ids}, url_suffix="/cases/link_issues"
        )

    def unlink_issue_from_cases(self, issue_id, case_ids: list) -> dict:
        """Unlink an issue from one or more cases.

        Args:
            issue_id: The issue ID to unlink
            case_ids: List of case IDs to unlink the issue from

        Returns:
            dict: API response
        """
        return self._http_request(
            method="POST", json_data={"issue_id": issue_id, "case_ids": case_ids}, url_suffix="/cases/unlink_issue"
        )

    def search_assets(self, filter, page_number, page_size, on_demand_fields):
        reply = self._http_request(
            method="POST",
            headers=self._headers,
            json_data={
                "request_data": {
                    "filters": filter,
                    "search_from": page_number * page_size,
                    "search_to": (page_number + 1) * page_size,
                    "on_demand_fields": on_demand_fields,
                },
            },
            url_suffix="/assets",
        )

        return reply

    def search_asset_groups(self, filter):
        reply = self._http_request(
            method="POST",
            headers=self._headers,
            json_data={"request_data": {"filters": filter}},
            full_url="/api/webapp/public_api/v1/asset-groups",
        )

        return reply

    def get_webapp_data(self, request_data: dict) -> dict:
        return self._http_request(
            method="POST",
            url_suffix="/get_data",
            json_data=request_data,
        )

    def get_webapp_view_def(self, request_data: dict) -> dict:
        return self._http_request(
            method="GET",
            url_suffix="/get_view_def",
            json_data=request_data,
        )

    def get_webapp_histograms(self, request_data: dict) -> dict:
        return self._http_request(
            method="POST",
            url_suffix="/get_histograms",
            json_data=request_data,
        )

    def enable_scanners(self, payload: dict, repository_id: str) -> dict:
        return self._http_request(
            method="PUT",
            url_suffix=f"/v1/repositories/{repository_id}/scan-configuration",
            json_data=payload,
            headers={
                **self._headers,
                "Content-Type": "application/json",
            },
        )

    def get_playbook_suggestion_by_issue(self, issue_id):
        """
        Get playbook suggestions for a specific issue.
        Args:
            issue_id (str): The ID of the issue to get playbook suggestions for.
        Returns:
            dict: The response containing playbook suggestions.
        """
        return self._http_request(
            method="POST",
            json_data={"alert_internal_id": issue_id},
            headers=self._headers,
            url_suffix="/incident/get_playbook_suggestion_by_alert/",
        )

    def get_playbooks_metadata(self):
        return self._http_request(
            method="GET",
            headers=self._headers,
            full_url="/xsoar/playbooks/metadata",
        )

    def get_quick_actions_metadata(self):
        return self._http_request(
            method="GET",
            headers=self._headers,
            full_url="/xsoar/quickactions",
        )

    def appsec_remediate_issue(self, request_body):
        return self._http_request(
            method="POST",
            data=request_body,
            headers=self._headers,
            url_suffix="/v1/issues/fix/trigger_fix_pull_request",
        )

    def get_appsec_suggested_fix(self, issue_id: str) -> dict | None:
        reply = self._http_request(
            method="GET",
            headers=self._headers,
            full_url=f"/api/webapp/public_api/appsec/v1/issues/fix/{issue_id}/fix_suggestion",
        )
        return reply

    def create_policy(self, policy_payload: str) -> dict:
        """
        Creates a new policy in Cortex XDR.
        Args:
            policy_payload (str): The policy definition payload.
        Returns:
            dict: The response from the API.
        """
        demisto.debug(f"Policy creation payload: {policy_payload}")
        return self._http_request(
            method="POST",
            data=policy_payload,
            headers=self._headers,
            url_suffix="/public_api/appsec/v1/policies",
        )

    def get_endpoint_support_file(self, request_data: dict[str, Any]) -> dict:
        """
        Retrieve endpoint support file from Cortex XDR.
        Args:
            request_data (dict[str, Any]): The request data containing endpoint information.
        Returns:
            dict: The response containing the endpoint support file data.
        """
        demisto.debug(f"Endpoint support file request payload: {request_data}")
        return self._http_request(
            method="POST",
            data=request_data,
            headers=self._headers,
            url_suffix="/retrieve_endpoint_tsf",
        )

    def send_endpoint_heartbeat(self, json_data: dict) -> dict:
        """
        Perform endpoint heartbeat.
        Args:
            json_data (dict[str, Any]): The json data containing endpoint information.
        Returns:
            dict: The response from the API.
        """
        return self._http_request(
            method="POST",
            url_suffix="/call_home/",
            json_data=json_data,
        )

    def get_endpoint_update_version(self, request_data):
        reply = self._http_request(
            method="POST",
            json_data={"request_data": request_data},
            url_suffix="/agents/upgrade/details",
        )
        return reply

    def update_endpoint_version(self, request_data):
        reply = self._http_request(
            method="POST",
            json_data={"request_data": request_data},
            url_suffix="/agents/upgrade",
        )
        return reply

    def update_case(self, case_update_payload, case_id):
        """
        Update a case with the provided data.

        Args:
            case_update_payload (dict): The data to update in the case.
            case_id (str): Case ID to update.

        Returns:
            dict: Response from the API for the case update.
        """
        request_data = {"request_data": {"newIncidentInterface": True, "case_id": case_id, **case_update_payload}}
        return self._http_request(
            method="POST",
            url_suffix="/case/set_data",
            json_data=request_data,
        )

    def bulk_update_case(self, case_update_payload, case_ids):
        request_data = {
            "request_data": {
                "filter_data": {"filter": {"OR": [{"SEARCH_FIELD": "CASE_ID", "SEARCH_TYPE": "IN", "SEARCH_VALUE": case_ids}]}},
                "update_attrs": case_update_payload,
            }
        }
        return self._http_request(
            method="POST",
            url_suffix="/case/bulk_update_cases",
            json_data=request_data,
        )

    def run_playbook(self, issue_ids: list, playbook_id: str) -> dict:
        """
        Runs a specific playbook for a given investigation.

        Args:
            issue_ids: The IDs of the issues.
            playbook_id: The ID of the playbook to run.

        Returns:
            dict: The response from running the playbook.
        """
        return self._http_request(
            method="POST",
            url_suffix="/inv-playbook/new",
            headers={
                **self._headers,
                "Content-Type": "application/json",
            },
            json_data={"alertIds": issue_ids, "playbookId": playbook_id},
        )

    def unassign_case(self, case_id: str) -> dict:
        """
        Unassign a case by updating it with default unassignment data.

        Args:
            case_id (str): Case ID to unassign.

        Returns:
            dict: Response from the API for the case update.
        """
        request_data = {"request_data": {"newIncidentInterface": True, "case_id": case_id}}

        return self._http_request(
            method="POST",
            url_suffix="/case/un_assign_user",
            headers={
                **self._headers,
                "Content-Type": "application/json",
            },
            json_data=request_data,
        )

    def add_assessment_profile(self, profile_payload: dict) -> dict:
        """
        Add a new assessment profile to Cortex XDR.

        Args:
            profile_payload (dict): The assessment profile configuration payload.

        Returns:
            dict: The response from the API for adding the assessment profile.
        """
        return self._http_request(
            method="POST",
            url_suffix="/compliance/add_assessment_profile",
            json_data=profile_payload,
        )

    def list_compliance_standards_command(self, payload: dict) -> dict:
        """
        List compliance standards from Cortex XDR.

        Args:
            payload (dict): The request payload for listing compliance standards.

        Returns:
            dict: The response from the API containing compliance standards data.
        """
        return self._http_request(
            method="POST",
            url_suffix="/compliance/get_standards",
            json_data=payload,
        )

    def get_users(self):
        reply = self._http_request(
            method="POST",
            json_data={},
            headers=self._headers,
            url_suffix="/rbac/get_users",
        )

        return reply

    def get_case_resolution_statuses(self, case_id: str) -> dict:
        reply = self._http_request(
            method="GET",
            json_data={},
            headers={
                **self._headers,
                "Content-Type": "application/json",
            },
            url_suffix=f"case/{case_id}/resolution-plan/tasks",
        )
        return reply

    def get_custom_fields_metadata(self) -> dict[str, Any]:
        """
        Retrieve custom fields metadata from the CUSTOM_FIELDS_CASE_TABLE.

        Returns comprehensive metadata for all custom fields including:
        - CUSTOM_FIELD_NAME: Internal field identifier (display name)
        - CUSTOM_FIELD_PRETTY_NAME: User-friendly display name
        - CUSTOM_FIELD_CLI_NAME: Machine/CLI name used in commands and search
        - CUSTOM_FIELD_IS_SYSTEM: Boolean flag (true = system field, false = custom field)
        - CUSTOM_FIELD_TYPE: Field data type

        Returns:
            dict: Response containing custom fields metadata in reply.DATA
        """
        request_data = {
            "type": "grid",
            "table_name": CUSTOM_FIELDS_TABLE,
            "filter_data": {
                "sort": [],
                "filter": {},
                "free_text": "",
                "visible_columns": None,
                "locked": None,
                "paging": {"from": 0, "to": 1000},
            },
            "jsons": [],
            "on_demand_fields": ["CUSTOM_FIELD_CLI_NAME"],
        }

        return self.get_webapp_data(request_data)

    def get_case_ai_summary(self, case_id: int) -> dict:
        """
        Retrieves AI-generated summary for a specific case ID.

        Args:
            case_id (int): The ID of the case to retrieve AI summary for.

        Returns:
            dict: API response containing case AI summary.
        """
        return self._http_request(
            method="POST",
            url_suffix="/cases/get_ai_case_details",
            json_data={"case_id": case_id},
        )

    def create_profile(self, profile_data: dict) -> dict:
        return self._http_request(
            method="POST",
            url_suffix="/profiles/prevention/add",
            json_data=profile_data,
        )

    def get_profile(self, profile_id: str) -> dict:
        return self._http_request(
            method="POST",
            url_suffix="/profiles/get_profile_view_by_id",
            json_data={"profile_id": profile_id},
        )

    def update_profile(self, update_data: dict) -> dict:
        return self._http_request(
            method="POST",
            url_suffix="/profiles/edit_profile",
            json_data=update_data,
        )

    def delete_profile(self, profile_ids: list) -> dict:
        return self._http_request(
            method="POST",
            url_suffix="/profiles/delete_profiles",
            json_data={"profile_ids": profile_ids},
        )

    def get_agent_policy_table(self) -> dict:
        """
        Retrieves the current agent policy table with policy hash.

        Returns:
            dict: API response containing policy table data and hash.
        """
        request_data = {
            "type": "grid",
            "table_name": AGENT_POLICY_TABLE,
            "filter_data": {"filter": {}},
        }
        return self.get_webapp_data(request_data)

    def update_agent_policy(self, update_data: dict) -> dict:
        """
        Updates the agent policy table with new or modified policies.

        Args:
            update_data (dict): The update payload containing policy data and hash.

        Returns:
            dict: API response from the policy update.
        """

        demisto.debug({"update_data": update_data})
        return self._http_request(
            method="POST",
            url_suffix="/agent/policy/update",
            json_data={"update_data": update_data},
        )

    def get_multiple_cases_extra_data(self, case_ids: list[str]) -> dict:
        """
        Retrieve extra data for multiple cases in a single bulk API call.

        Uses the /public_api/v1/incidents/get_multiple_incidents_extra_data/ endpoint
        to fetch enriched data (alerts, network/file artifacts) for all provided case IDs at once.

        Args:
            case_ids: List of case ID strings to retrieve extra data for.

        Returns:
            dict: The raw API response containing enriched data for all requested cases.
        """
        payload = {
            "request_data": {
                "filters": [
                    {
                        "field": "incident_id_list",
                        "operator": "in",
                        "value": case_ids,
                    },
                ],
                "full_alert_fields": True,
            }
        }
        demisto.debug(f"Calling get_multiple_incidents_extra_data with case_ids={case_ids}")
        response = self._http_request(
            method="POST",
            url_suffix="/public_api/v1/incidents/get_multiple_incidents_extra_data/",
            json_data=payload,
            headers=self._headers,
            timeout=self.timeout,
        )
        demisto.debug(f"get_multiple_incidents_extra_data response received for {len(case_ids)} cases")
        return response


def get_appsec_suggestion(client: Client, issue: dict, issue_id: str) -> dict:
    """
    Append Application Security - related suggestions to the recommendation data.

    Args:
        client (Client): Client instance used to send the request.
        headers (list): Headers for the readable output.
        issue (dict): Details of the issue.
        recommendation (dict): The base remediation recommendation.
        issue_id (str): The issue ID.

    Returns:
        tuple[list, dict]: Updated headers and recommendation including AppSec additions.
    """
    alert_source = issue.get("alert_source")
    if alert_source not in APPSEC_SOURCES:
        return {}

    recommendation = {}
    manual_fix = issue.get("extended_fields", {}).get("action")
    if manual_fix:
        recommendation["remediation"] = manual_fix

    fix_suggestion = client.get_appsec_suggested_fix(issue_id)
    demisto.debug(f"AppSec fix suggestion: {fix_suggestion}")

    if fix_suggestion and isinstance(fix_suggestion, dict) and fix_suggestion.get("suggestedCodeBlock"):
        recommendation.update(
            {
                "existing_code_block": fix_suggestion.get("existingCodeBlock", ""),
                "suggested_code_block": fix_suggestion.get("suggestedCodeBlock", ""),
            }
        )
    demisto.debug(f"{recommendation=} for {issue=}")

    return recommendation


def get_remediation_techniques_suggestion(issue: dict, current_issue_id: str) -> list:
    """
    Get remediation techniques suggestions based on asset types.

    Args:
        issue (dict): The issue data.
        current_issue_id (str): The current issue ID.

    Returns:
        list: A list of filtered remediation techniques.
    """
    asset_types: list = issue.get("asset_types", [])
    normalized_asset_types = {t.upper().replace(" ", "_") for t in asset_types if t}
    remediation_techniques_response = issue.get("extended_fields", {}).get("remediationTechniques") or []
    filtered_techniques = [
        t
        for t in remediation_techniques_response
        if t.get("techniqueAssetType") and t.get("techniqueAssetType").upper() in normalized_asset_types
    ]
    demisto.debug(f"Remediation recommendation of {current_issue_id=}: {filtered_techniques}")
    return filtered_techniques


def populate_playbook_and_quick_action_suggestions(
    client: Client, issue_id: str, pb_id_to_data: dict, qa_name_to_data: dict
) -> dict:
    """
    Fetches playbook and quick-action suggestions for a given issue
    and updates the recommendation dictionary accordingly.

    Returns:
        recommendation
    """
    recommendation = {}

    response = client.get_playbook_suggestion_by_issue(issue_id)
    suggestions = response.get("reply", {})
    demisto.debug(f"Playbooks and quick action {suggestions=} for {issue_id=}")

    if not suggestions:
        return {}

    # Playbook suggestion
    playbook_id = suggestions.get("playbook_id")
    suggestion_rule_id = suggestions.get("suggestion_rule_id")

    if playbook_id:
        recommendation["playbook_suggestions"] = {
            "playbook_id": playbook_id,
            "suggestion_rule_id": suggestion_rule_id,
        }
        pb_data = pb_id_to_data.get(playbook_id)
        if pb_data:
            recommendation["playbook_suggestions"].update(pb_data)

    # Quick action suggestion
    quick_action_id = suggestions.get("quick_action_id", None)
    quick_action_suggestion_rule_id = suggestions.get("quick_action_suggestion_rule_id", None)

    if quick_action_id:
        recommendation["quick_action_suggestions"] = {
            "name": quick_action_id,
            "suggestion_rule_id": quick_action_suggestion_rule_id,
        }
        qa_data = qa_name_to_data.get(quick_action_id)
        if qa_data:
            recommendation["quick_action_suggestions"].update(qa_data)

    return recommendation


def map_qa_name_to_data(qas_metadata) -> dict:
    """
    Maps each quick-action command name to its metadata, filtering hidden arguments
    and removing empty fields.

    Returns:
        dict: command_name → metadata.
    """
    if not isinstance(qas_metadata, list):
        return {}

    qa_name_to_data = {}

    for item in qas_metadata:
        brand = item.get("brand")
        category = item.get("category")

        for cmd in item.get("commands", []):
            cmd_name = cmd.get("name")
            arguments = cmd.get("arguments", [])
            filtered_args = [arg for arg in arguments if not arg.get("hidden", False)]
            qa_name_to_data[cmd_name] = remove_empty_elements(
                {
                    "brand": brand,
                    "category": category,
                    "description": cmd.get("description"),
                    "pretty_name": cmd.get("prettyName"),
                    "arguments": filtered_args,
                }
            )

    return qa_name_to_data


def map_pb_id_to_data(pbs_metadata) -> dict:
    """
    Maps each playbook ID to its corresponding data to enable fast lookups.

    Args:
        pbs_metadata: List of playbook metadata dictionaries.

    Returns:
        dict: Mapping of playbook ID to its data from the metadata list.
    """
    if not isinstance(pbs_metadata, list):
        return {}

    pb_id_to_data = {}
    for pb_metadata in pbs_metadata:
        pb_id = pb_metadata.get("id")
        if pb_id:
            pb_id_to_data[pb_id] = remove_empty_elements({"name": pb_metadata.get("name"), "comment": pb_metadata.get("comment")})

    return pb_id_to_data


def create_issue_recommendations_readable_output(issue_ids: list[str], all_recommendations: list[dict]) -> str:
    """
    Create readable output for issue recommendations with dynamic headers based on content.

    Args:
        issue_ids: List of issue IDs being processed
        all_recommendations: Complete recommendation data used to determine headers and create readable output

    Returns:
        str: Formatted markdown table string for readable output
    """
    # Base headers that are always present
    headers = [
        "issue_id",
        "issue_name",
        "severity",
        "description",
        "remediation",
        "network_reachability",
    ]

    # Flags to track what headers we need to append
    append_appsec_headers = False
    append_playbook_suggestions_header = False
    append_quick_action_suggestions_header = False

    readable_recommendations = []

    # Single loop to both check for headers and create readable recommendations
    for recommendation in all_recommendations:
        # Check what headers we need to append
        if not append_appsec_headers and ("existing_code_block" in recommendation or "suggested_code_block" in recommendation):
            append_appsec_headers = True
        if not append_playbook_suggestions_header and "playbook_suggestions" in recommendation:
            append_playbook_suggestions_header = True
        if not append_quick_action_suggestions_header and "quick_action_suggestions" in recommendation:
            append_quick_action_suggestions_header = True

        # Create readable recommendation
        readable_rec = recommendation.copy()

        # Simplify playbook suggestions for readable output (show only name)
        if "playbook_suggestions" in readable_rec and isinstance(readable_rec["playbook_suggestions"], dict):
            pb_suggestions = readable_rec["playbook_suggestions"]
            readable_rec["playbook_suggestions"] = {
                "name": pb_suggestions.get("name", ""),
                "playbook_id": pb_suggestions.get("playbook_id", ""),
            }

        # Simplify quick action suggestions for readable output (show only pretty_name)
        if "quick_action_suggestions" in readable_rec and isinstance(readable_rec["quick_action_suggestions"], dict):
            qa_suggestions = readable_rec["quick_action_suggestions"]
            readable_rec["quick_action_suggestions"] = {
                "name": qa_suggestions.get("name", ""),
                "pretty_name": qa_suggestions.get("pretty_name", ""),
            }

        readable_recommendations.append(readable_rec)

    # Add conditional headers based on what we found
    if append_appsec_headers:
        headers.extend(["existing_code_block", "suggested_code_block"])

    if append_playbook_suggestions_header:
        headers.append("playbook_suggestions")

    if append_quick_action_suggestions_header:
        headers.append("quick_action_suggestions")

    # Create the readable output table
    issue_readable_output = tableToMarkdown(
        f"Issue Recommendations for {issue_ids}",
        readable_recommendations,
        headerTransform=string_to_table_header,
        headers=headers,
    )

    return issue_readable_output


def get_issue_recommendations_command(client: Client, args: dict) -> CommandResults:
    """
    Get comprehensive recommendations for an issue, including remediation steps and playbook suggestions.
    Retrieves issue data with remediation field using the generic /api/webapp/get_data endpoint.
    """
    issue_ids = argToList(args.get("issue_ids"))
    if len(issue_ids) > 10:
        raise DemistoException("Please provide a maximum of 10 issue IDs per request.")

    filter_builder = FilterBuilder()
    filter_builder.add_field("internal_id", FilterType.EQ, issue_ids)

    request_data = build_webapp_request_data(
        table_name="ALERTS_VIEW_TABLE",
        filter_dict=filter_builder.to_dict(),
        limit=10,
        sort_field="source_insert_ts",
        sort_order="DESC",
        on_demand_fields=[],
    )

    # Get issue data with remediation field
    response = client.get_webapp_data(request_data)
    reply = response.get("reply", {})
    issue_data = reply.get("DATA", [])

    if not issue_data:
        raise DemistoException(f"No issues found with IDs: {issue_ids}")

    # Call the endpoint here to avoid calling it for each issue.
    pbs_metadata = client.get_playbooks_metadata() or []
    qas_metadata = client.get_quick_actions_metadata() or []
    pb_id_to_data = map_pb_id_to_data(pbs_metadata)
    qa_name_to_data = map_qa_name_to_data(qas_metadata)
    all_recommendations = []

    for issue in issue_data:
        current_issue_id = issue.get("internal_id")
        alert_source = issue.get("alert_source")

        # Base recommendation
        recommendation = {
            "issue_id": current_issue_id,
            "issue_name": issue.get("alert_name"),
            "severity": issue.get("severity"),
            "description": issue.get("alert_description"),
            "remediation": issue.get("remediation"),
            "network_reachability": issue.get("extended_fields", {}).get("network_reachability") or {},
        }

        # --- Playbook and Quick Action Suggestions ---
        recommendation_pb_qa = populate_playbook_and_quick_action_suggestions(
            client, current_issue_id, pb_id_to_data, qa_name_to_data
        )
        recommendation.update(recommendation_pb_qa)

        # --- AppSec ---
        appsec_recommendation = get_appsec_suggestion(client, issue, current_issue_id)
        if appsec_recommendation:
            recommendation.update(appsec_recommendation)

        # --- Remediation Techniques ---
        elif alert_source in REMEDIATION_TECHNIQUES_SOURCES:
            filtered_techniques = get_remediation_techniques_suggestion(issue, current_issue_id)
            recommendation["remediation"] = filtered_techniques or recommendation.get("remediation")

        all_recommendations.append(recommendation)

    # Final header adjustments
    issue_readable_output = create_issue_recommendations_readable_output(
        issue_ids=issue_ids, all_recommendations=all_recommendations
    )

    return CommandResults(
        readable_output=issue_readable_output,
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.IssueRecommendations",
        outputs_key_field="issue_id",
        outputs=all_recommendations,
        raw_response=response,
    )


def search_asset_groups_command(client: Client, args: dict) -> CommandResults:
    """
    Retrieves asset groups from the Cortex platform based on provided filters.

    Args:
        client (Client): The client instance used to send the request.
        args (dict): Dictionary containing the arguments for the command.
                     Expected to include:
                         - name (str, optional): Filter by asset group names
                         - type (str, optional): Filter by asset group type
                         - description (str, optional): Filter by description
                         - id (str, optional): Filter by asset group ids

    Returns:
        CommandResults: Object containing the formatted asset groups,
                        raw response, and outputs for integration context.
    """
    limit = arg_to_number(args.get("limit")) or 50
    filter_builder = FilterBuilder()
    filter_builder.add_field(
        ASSET_GROUP_FIELDS["asset_group_name"],
        FilterType.CONTAINS,
        argToList(args.get("name")),
    )
    filter_builder.add_field(ASSET_GROUP_FIELDS["asset_group_type"], FilterType.EQ, args.get("type"))
    filter_builder.add_field(
        ASSET_GROUP_FIELDS["asset_group_description"],
        FilterType.CONTAINS,
        argToList(args.get("description")),
    )
    filter_builder.add_field(ASSET_GROUP_FIELDS["asset_group_id"], FilterType.EQ, argToList(args.get("id")))

    request_data = build_webapp_request_data(
        table_name=ASSET_GROUPS_TABLE,
        filter_dict=filter_builder.to_dict(),
        limit=limit,
        sort_field="XDM__ASSET_GROUP__LAST_UPDATE_TIME",
    )

    response = client.get_webapp_data(request_data)
    reply = response.get("reply", {})
    data = reply.get("DATA", [])

    data = [
        {(k.replace("XDM__ASSET_GROUP__", "") if k.startswith("XDM__ASSET_GROUP__") else k).lower(): v for k, v in item.items()}
        for item in data
    ]

    return CommandResults(
        readable_output=tableToMarkdown("AssetGroups", data, headerTransform=string_to_table_header),
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.AssetGroups",
        outputs_key_field="id",
        outputs=data,
        raw_response=response,
    )


def build_histogram_request_data(table_name: str, filter_dict: dict, max_values_per_column: int, columns: list) -> dict:
    """
    Builds the request data for the generic /api/webapp//get_histograms endpoint.
    """
    filter_data = {
        "filter": filter_dict,
    }
    demisto.debug(f"{filter_data=}")

    return {
        "table_name": table_name,
        "filter_data": filter_data,
        "max_values_per_column": max_values_per_column,
        "columns": columns,
    }


def get_vulnerabilities_command(client: Client, args: dict) -> CommandResults:
    """
    Retrieves vulnerabilities using the generic /api/webapp/get_data endpoint.
    """
    limit = arg_to_number(args.get("limit")) or 50
    sort_field = args.get("sort_field", "LAST_OBSERVED")
    sort_order = args.get("sort_order", "DESC")

    filter_builder = FilterBuilder()
    filter_builder.add_field("CVE_ID", FilterType.CONTAINS, argToList(args.get("cve_id")))
    filter_builder.add_field("CVSS_SCORE", FilterType.GTE, arg_to_number(args.get("cvss_score_gte")))
    filter_builder.add_field("EPSS_SCORE", FilterType.GTE, arg_to_number(args.get("epss_score_gte")))
    filter_builder.add_field(
        "INTERNET_EXPOSED",
        FilterType.EQ,
        arg_to_bool_or_none(args.get("internet_exposed")),
    )
    filter_builder.add_field("EXPLOITABLE", FilterType.EQ, arg_to_bool_or_none(args.get("exploitable")))
    filter_builder.add_field("HAS_KEV", FilterType.EQ, arg_to_bool_or_none(args.get("has_kev")))
    filter_builder.add_field(
        "AFFECTED_SOFTWARE",
        FilterType.CONTAINS,
        argToList(args.get("affected_software")),
    )
    filter_builder.add_field(
        "PLATFORM_SEVERITY",
        FilterType.EQ,
        argToList(args.get("severity")),
        VULNERABILITIES_SEVERITY_MAPPING,
    )
    filter_builder.add_field("FINDING_SOURCES", FilterType.CONTAINS_IN_LIST, argToList(args.get("finding_sources")))
    filter_builder.add_field("ISSUE_ID", FilterType.CONTAINS, argToList(args.get("issue_id")))
    filter_builder.add_time_range_field("LAST_OBSERVED", args.get("start_time"), args.get("end_time"))
    filter_builder.add_field_with_mappings(
        "ASSIGNED_TO",
        FilterType.CONTAINS,
        argToList(args.get("assignee")),
        {
            "unassigned": FilterType.IS_EMPTY,
            "assigned": FilterType.NIS_EMPTY,
        },
    )
    filter_builder.add_field("CORTEX_VULNERABILITY_RISK_SCORE", FilterType.GTE, arg_to_number(args.get("cvrs_gte")))
    filter_builder.add_field(
        "COMPENSATING_CONTROLS_DETECTED_COVERAGE", FilterType.EQ, argToList(args.get("compensating_controls_effective_coverage"))
    )

    request_data = build_webapp_request_data(
        table_name=VULNERABLE_ISSUES_TABLE,
        filter_dict=filter_builder.to_dict(),
        limit=limit,
        sort_field=sort_field,
        sort_order=sort_order,
        on_demand_fields=argToList(args.get("on_demand_fields")),
    )
    response = client.get_webapp_data(request_data)
    reply = response.get("reply", {})
    data = reply.get("DATA", [])

    output_keys = [
        "ISSUE_ID",
        "CVE_ID",
        "CVE_DESCRIPTION",
        "ASSET_NAME",
        "PLATFORM_SEVERITY",
        "EPSS_SCORE",
        "CVSS_SCORE",
        "ASSIGNED_TO",
        "ASSIGNED_TO_PRETTY",
        "AFFECTED_SOFTWARE",
        "FIX_AVAILABLE",
        "INTERNET_EXPOSED",
        "HAS_KEV",
        "EXPLOITABLE",
        "ASSET_IDS",
        "FINDING_SOURCES",
        "COMPENSATING_CONTROLS_DETECTED_COVERAGE",
        "CORTEX_VULNERABILITY_RISK_SCORE",
        "FIX_VERSIONS",
        "ASSET_TYPES",
        "COMPENSATING_CONTROLS_DETECTED_CONTROLS",
        "EXPLOIT_LEVEL",
        "ISSUE_NAME",
        "PACKAGE_IN_USE",
        "PROVIDERS",
        "OS_FAMILY",
        "IMAGE",
    ]
    filtered_data = [{k: v for k, v in item.items() if k in output_keys} for item in data]

    readable_output = tableToMarkdown(
        "Vulnerabilities",
        filtered_data,
        headerTransform=string_to_table_header,
        sort_headers=False,
    )
    return CommandResults(
        readable_output=readable_output,
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.VulnerabilityIssue",
        outputs_key_field="ISSUE_ID",
        outputs=filtered_data,
        raw_response=response,
    )


def get_asset_details_command(client: Client, args: dict) -> CommandResults:
    """
    Retrieves details of a specific asset by its ID and formats the response.

    Args:
        client (Client): The client instance used to send the request.
        args (dict): Dictionary containing the arguments for the command.
                     Expected to include:
                         - asset_id (str): The ID of the asset to retrieve.

    Returns:
        CommandResults: Object containing the formatted asset details,
                        raw response, and outputs for integration context.
    """
    asset_id = args.get("asset_id")
    response = client.get_asset_details(asset_id)
    if not response:
        raise DemistoException(f"Failed to fetch asset details for {asset_id}. Ensure the asset ID is valid.")

    reply = response.get("reply")
    return CommandResults(
        readable_output=tableToMarkdown("Asset Details", reply, headerTransform=string_to_table_header),
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CoreAsset",
        outputs=reply,
        raw_response=reply,
    )


def extract_ids(case_extra_data: dict, data_key: str = "issues", field_name: str = "issue_id") -> list:
    """
    Extract a list of IDs from a command result.

    Args:
        case_extra_data: The extra data dictionary containing the nested data.
        data_key: The top-level key containing the data list (default: "issues").
        field_name: The name of the field that contains the ID (default: "issue_id").

    Returns:
        A list of the IDs extracted from the command result.
    """
    if not case_extra_data:
        return []

    container = case_extra_data.get(data_key, {})
    data = container.get("data", []) if container else []
    ids = [str(item[field_name]) for item in data if isinstance(item, dict) and item.get(field_name) is not None]
    demisto.debug(f"Extracted {field_name}s: {ids}")
    return ids


def parse_single_case_extra_data(case_incident_data: dict) -> dict:
    """
    Parse a single case's extra data from the bulk API response into the CaseExtraData format.

    The bulk endpoint returns each case as:
        {"incident": {...}, "alerts": {"total_count": N, "data": [...]}, "network_artifacts": ..., "file_artifacts": ...}

    Extracts fields from the incident object that are only available via the extra-data API
    (not returned by the initial get_cases call), plus issue_ids from alerts and artifacts.

    Args:
        case_incident_data: A single case entry from the bulk response's 'incidents' list.

    Returns:
        dict: The CaseExtraData dict with issue_ids, extra incident fields, and network/file artifacts.
    """
    incident_data = case_incident_data.get("incident", {})
    issue_ids = extract_ids(case_incident_data, data_key="alerts", field_name="alert_id")

    return {
        "issue_ids": issue_ids,
        "network_artifacts": case_incident_data.get("network_artifacts"),
        "file_artifacts": case_incident_data.get("file_artifacts"),
        "notes": incident_data.get("notes"),
        "detection_time": incident_data.get("detection_time"),
        "xdr_url": incident_data.get("xdr_url"),
        "starred_manually": incident_data.get("starred_manually"),
        "manual_description": incident_data.get("manual_description"),
    }


def add_cases_extra_data(client: Client, cases_list: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """
    Enrich a list of cases with extra data using a single bulk API call.

    Calls the /public_api/v1/incidents/get_multiple_incidents_extra_data/ endpoint once
    for all case IDs, then maps the response back to each case.

    Args:
        client: The Cortex platform client instance.
        cases_list: List of case dictionaries, each containing a 'case_id' key.

    Returns:
        list: The same cases_list with 'CaseExtraData' added to each case.
    """
    case_ids = [str(case["case_id"]) for case in cases_list if case.get("case_id") is not None]
    demisto.debug(f"Fetching bulk extra data for case_ids={case_ids}")

    if not case_ids:
        demisto.debug("No valid case IDs to fetch extra data for, skipping API call.")
        for case in cases_list:
            case["CaseExtraData"] = {}
        return cases_list

    try:
        response = client.get_multiple_cases_extra_data(case_ids)
    except Exception as e:
        demisto.debug(f"Failed to retrieve bulk extra data for case IDs {case_ids}: {e}\n{traceback.format_exc()}")
        for case in cases_list:
            case["CaseExtraData"] = {}
        return cases_list

    reply = response.get("reply", {})
    incidents_data = reply.get("incidents", [])

    # Build a lookup from case_id (incident_id) to its extra data
    extra_data_by_case_id: dict[str, dict] = {}
    for incident_entry in incidents_data:
        case_id = str(incident_entry.get("incident", {}).get("incident_id", ""))
        if case_id:
            extra_data_by_case_id[case_id] = parse_single_case_extra_data(incident_entry)

    demisto.debug(f"Bulk extra data parsed for {len(extra_data_by_case_id)} cases")

    for case in cases_list:
        case_id = str(case.get("case_id", ""))
        case["CaseExtraData"] = extra_data_by_case_id.get(case_id, {})

    return cases_list


def map_case_format(case_list):
    """
    Maps a list of case data from the API response format to a standardized internal format.

    Args:
        case_list (list): List of case dictionaries from the API response.
                         Each case should contain fields like CASE_ID, NAME, STATUS, etc.

    Returns:
        dict or list: Returns an empty dict if case_list is invalid or empty,
                     otherwise returns a list of mapped case dictionaries with
                     standardized field names and processed values.
    """
    if not case_list or not isinstance(case_list, list):
        return {}

    mapped_cases = []
    for case_data in case_list:
        demisto.debug(f"Processing case data: {case_data}")
        mapped_case = {
            "case_id": str(case_data.get("CASE_ID")),
            "case_name": case_data.get("NAME"),
            "description": case_data.get("DESCRIPTION"),
            "creation_time": case_data.get("CREATION_TIME"),
            "modification_time": case_data.get("LAST_UPDATE_TIME"),
            "resolved_timestamp": case_data.get("RESOLVED_TIMESTAMP"),
            "status": str(case_data.get("STATUS", case_data.get("STATUS_PROGRESS"))).split("_")[-1].lower(),
            "resolve_comment": case_data.get("RESOLVED_COMMENT"),
            "resolve_reason": CaseManagement.STATUS_RESOLVED_REASON_OUTPUT.get(
                case_data.get("RESOLVE_REASON"), case_data.get("RESOLVE_REASON")
            ),
            "severity": str(case_data.get("SEVERITY")).split("_")[-1].lower(),
            "case_domain": case_data.get("INCIDENT_DOMAIN"),
            "original_tags": [tag.get("tag_name") for tag in (case_data.get("ORIGINAL_TAGS") or [])],
            "tags": [tag.get("tag_name") for tag in (case_data.get("CURRENT_TAGS") or [])],
            "issue_count": case_data.get("ACC_ALERT_COUNT"),
            "critical_severity_issue_count": case_data.get("CRITICAL_SEVERITY_ALERTS"),
            "high_severity_issue_count": case_data.get("HIGH_SEVERITY_ALERTS"),
            "med_severity_issue_count": case_data.get("MEDIUM_SEVERITY_ALERTS"),
            "low_severity_issue_count": case_data.get("LOW_SEVERITY_ALERTS"),
            "rule_based_score": case_data.get("CALCULATED_SCORE"),
            "aggregated_score": case_data.get("SCORE"),
            "manual_score": case_data.get("MANUAL_SCORE"),
            "predicted_score": case_data.get("SCORTEX"),
            "wildfire_hits": case_data.get("WF_HITS"),
            "assigned_user_pretty_name": case_data.get("ASSIGNED_USER_PRETTY"),
            "assigned_user_mail": case_data.get("ASSIGNED_USER"),
            "issues_grouping_status": str(case_data.get("CASE_GROUPING_STATUS")).split("_")[-1],
            "starred": case_data.get("CASE_STARRED"),
            "case_sources": case_data.get("INCIDENT_SOURCES"),
            "custom_fields": {k.removeprefix("CUSTOM_").lower(): v for k, v in case_data.items() if k.startswith("CUSTOM_")},
            "hosts": case_data.get("HOSTS") or [],
            "users": case_data.get("USERS") or [],
            "host_count": len(case_data.get("HOSTS", []) or []),
            "user_count": len(case_data.get("USERS", []) or []),
            "issue_categories": case_data.get("ALERT_CATEGORIES"),
            "mitre_techniques_ids_and_names": case_data.get("MITRE_TECHNIQUES"),
            "mitre_tactics_ids_and_names": case_data.get("MITRE_TACTICS"),
            "manual_severity": case_data.get("USER_SEVERITY"),
            "asset_accounts": case_data.get("UAI_ASSET_ACCOUNTS", []),
            "asset_categories": case_data.get("UAI_ASSET_CATEGORIES", []),
            "asset_classes": case_data.get("UAI_ASSET_CLASSES", []),
            "asset_group_ids": case_data.get("UAI_ASSET_GROUP_IDS", []),
            "asset_ids": case_data.get("UAI_ASSET_IDS", []),
            "asset_names": case_data.get("UAI_ASSET_NAMES", []),
            "asset_providers": case_data.get("UAI_ASSET_PROVIDERS", []),
            "asset_regions": case_data.get("UAI_ASSET_REGIONS", []),
            "asset_types": case_data.get("UAI_ASSET_TYPES", []),
        }

        mapped_cases.append(mapped_case)

    return mapped_cases


def build_get_cases_filter(args: dict) -> FilterBuilder:
    since_creation_start_time = args.get("since_creation_time")
    since_creation_end_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") if since_creation_start_time else None
    since_modification_start_time = args.get("since_modification_time")
    since_modification_end_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") if since_modification_start_time else None
    gte_creation_time = args.get("gte_creation_time")
    lte_creation_time = args.get("lte_creation_time")
    gte_modification_time = args.get("gte_modification_time")
    lte_modification_time = args.get("lte_modification_time")

    not_status_values = [CaseManagement.STATUS.get(status) for status in argToList(args.get("not_status"))]
    status_values = [CaseManagement.STATUS.get(status) for status in argToList(args.get("status"))]
    severity_values = [CaseManagement.SEVERITY.get(severity) for severity in argToList(args.get("severity"))]
    tag_values = [CaseManagement.TAGS.get(tag, tag) for tag in argToList(args.get("tag"))]
    filter_builder = FilterBuilder()
    filter_builder.add_time_range_field(CaseManagement.FIELDS["creation_time"], gte_creation_time, lte_creation_time)
    filter_builder.add_time_range_field(
        CaseManagement.FIELDS["last_updated"],
        gte_modification_time,
        lte_modification_time,
    )
    filter_builder.add_time_range_field(
        CaseManagement.FIELDS["creation_time"],
        since_creation_start_time,
        since_creation_end_time,
    )
    filter_builder.add_time_range_field(
        CaseManagement.FIELDS["last_updated"],
        since_modification_start_time,
        since_modification_end_time,
    )
    filter_builder.add_field(CaseManagement.FIELDS["status"], FilterType.EQ, status_values)
    filter_builder.add_field(CaseManagement.FIELDS["status"], FilterType.NEQ, not_status_values)
    filter_builder.add_field(CaseManagement.FIELDS["severity"], FilterType.EQ, severity_values)
    filter_builder.add_field(
        CaseManagement.FIELDS["case_id_list"],
        FilterType.EQ,
        argToList(args.get("case_id_list")),
    )
    filter_builder.add_field(
        CaseManagement.FIELDS["case_domain"],
        FilterType.EQ,
        argToList(args.get("case_domain")),
    )
    filter_builder.add_field(
        CaseManagement.FIELDS["case_name"],
        FilterType.CONTAINS,
        argToList(args.get("case_name")),
    )
    filter_builder.add_field(
        CaseManagement.FIELDS["case_description"],
        FilterType.CONTAINS,
        argToList(args.get("case_description")),
    )
    filter_builder.add_field(
        CaseManagement.FIELDS["starred"],
        FilterType.EQ,
        [argToBoolean(x) for x in argToList(args.get("starred"))],
    )
    filter_builder.add_field(
        CaseManagement.FIELDS["asset_ids"],
        FilterType.CONTAINS_IN_LIST,
        argToList(args.get("asset_ids")),
    )
    filter_builder.add_field(
        CaseManagement.FIELDS["asset_groups"],
        FilterType.CONTAINS_IN_LIST,
        argToList(args.get("asset_groups")),
    )
    filter_builder.add_field(
        CaseManagement.FIELDS["hosts"],
        FilterType.CASE_HOST_EQ,
        argToList(args.get("hosts")),
    )
    filter_builder.add_field(CaseManagement.FIELDS["tags"], FilterType.ARRAY_CONTAINS, tag_values)
    filter_builder.add_field_with_mappings(
        determine_assignee_filter_field(argToList(args.get("assignee"))),
        FilterType.CONTAINS,
        argToList(args.get("assignee")),
        {
            "unassigned": FilterType.IS_EMPTY,
            "assigned": FilterType.NIS_EMPTY,
        },
    )

    return filter_builder


def get_cases_command(client: Client, args: dict[str, Any]):
    """
    Retrieves cases from Cortex platform based on provided filtering criteria.

    Args:
        client: The Cortex platform client instance for making API requests.
        args (dict): Dictionary containing filter parameters including page number,
                    limits, time ranges, status, severity, and other case attributes.

    Returns:
        List of mapped case objects containing case details and metadata.
    """

    page = arg_to_number(args.get("page")) or 0
    limit = arg_to_number(args.get("limit")) or MAX_GET_CASES_LIMIT

    limit = page * MAX_GET_CASES_LIMIT + limit
    page = page * MAX_GET_CASES_LIMIT

    sort_field, sort_order = get_cases_sort_order(args.get("sort_by_creation_time"), args.get("sort_by_modification_time"))
    request_data = build_webapp_request_data(
        table_name=CASES_TABLE,
        filter_dict=build_get_cases_filter(args).to_dict(),
        limit=limit,
        sort_field=sort_field,
        sort_order=sort_order,
        start_page=page,
    )
    demisto.info(f"{request_data=}")
    response = client.get_webapp_data(request_data)
    reply = response.get("reply", {})
    data = reply.get("DATA", [])
    demisto.debug(f"Raw case data retrieved from API: {data}")
    data = map_case_format(data)
    demisto.debug(f"Case data after mapping and formatting: {data}")

    filter_count = int(reply.get("FILTER_COUNT", "0"))
    returned_count = len(data)

    command_results = [
        CommandResults(
            outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.CasesMetadata",
            outputs={"filtered_count": filter_count, "returned_count": returned_count},
        )
    ]

    if (
        returned_count == 1 and int(data[0].get("issue_count") or 0) > 1
    ):  # AI summary supported in cases of a single case query with more than one issue
        case_id = data[0].get("case_id")
        try:  # if functionality isn't supported exception is raised and should be handled
            response = client.get_case_ai_summary(int(case_id))
            if response:
                reply = response.get("reply", {})
                if case_description := reply.get("case_description"):
                    data[0]["description"] = case_description
                if case_name := reply.get("case_name"):
                    data[0]["case_name"] = case_name
        except Exception as e:
            demisto.debug(f"Failed to retrieve case AI summary for case ID {case_id}: {str(e)}")

    get_enriched_case_data = argToBoolean(args.get("get_enriched_case_data", "false"))
    if isinstance(data, dict):
        data = [data] if data else []

    if get_enriched_case_data and data:
        data = add_cases_extra_data(client, data)

    command_results.append(
        CommandResults(
            readable_output=tableToMarkdown("Cases", data, headerTransform=string_to_table_header),
            outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.Case",
            outputs_key_field="case_id",
            outputs=data,
            raw_response=data,
        )
    )

    return command_results


def get_cases_sort_order(sort_by_creation_time, sort_by_modification_time):
    if sort_by_creation_time and sort_by_modification_time:
        raise CortexConflictingArgsError(
            override_message="Should be provide either sort_by_creation_time or sort_by_modification_time. Can't provide both",
            arguments=["sort_by_creation_time", "sort_by_modification_time"],
            reason="Only one sort field can be specified at a time.",
            resolution="Provide either sort_by_creation_time or sort_by_modification_time, not both.",
            mutually_exclusive=True,
        )

    if sort_by_creation_time:
        sort_field = "CREATION_TIME"
        sort_order = sort_by_creation_time
    elif sort_by_modification_time:
        sort_field = "LAST_UPDATE_TIME"
        sort_order = sort_by_modification_time
    else:
        sort_field = "LAST_UPDATE_TIME"
        sort_order = "DESC"
    return sort_field, sort_order


def get_issue_id(args) -> str:
    """Retrieve the issue ID from either provided arguments or calling context.

    Args:
        args (dict): Arguments passed in the command, containing optional issue_id

    Returns:
        str: The extracted issue ID
    """
    issue_id = args.get("id", "")
    if not issue_id:
        issues = demisto.callingContext.get("context", {}).get("Incidents")
        if issues:
            issue = issues[0]
            issue_id = issue.get("id")

    return issue_id


def create_filter_data(issue_id: str, update_args: dict) -> dict:
    """Creates filter data for updating an issue with specified parameters.

    Args:
        issue_id (bool): Issue ID from args or context
        update_args (dict): Dictionary of fields to update

    Returns:
        dict: Object representing updated issue details
    """
    filter_builder = FilterBuilder()
    filter_builder.add_field("internal_id", FilterType.EQ, issue_id)

    filter_data = {
        "filter_data": {"filter": filter_builder.to_dict()},
        "filter_type": "static",
        "update_data": update_args,
    }
    return filter_data


def update_issue_command(client: Client, args: dict):
    """Updates an issue with specified parameters.

    Args:
        client (Client): Client instance to execute the request
        args (dict): Command arguments for updating an issue
    """
    issue_id = get_issue_id(args)
    if not issue_id:
        raise CortexMissingArgError("id", override_message="Issue ID is required for updating an issue.")

    status_map = {
        "New": "STATUS_010_NEW",
        "In Progress": "STATUS_020_UNDER_INVESTIGATION",
        "Resolved - Known Issue": "STATUS_040_RESOLVED_KNOWN_ISSUE",
        "Resolved - Duplicate Issue": "STATUS_050_RESOLVED_DUPLICATE",
        "Resolved - False Positive": "STATUS_060_RESOLVED_FALSE_POSITIVE",
        "Resolved - other": "STATUS_070_RESOLVED_OTHER",
        "Resolved - True Positive": "STATUS_090_RESOLVED_TRUE_POSITIVE",
        "Resolved - Security Testing": "STATUS_100_RESOLVED_SECURITY_TESTING",
        "Resolved - Dismissed": "STATUS_240_RESOLVED_DISMISSED",
        "Resolved - Fixed": "STATUS_250_RESOLVED_FIXED",
        "Resolved - Risk Accepted": "STATUS_130_RESOLVED_RISK_ACCEPTED",
    }
    severity_map = {
        "low": "SEV_020_LOW",
        "medium": "SEV_030_MEDIUM",
        "high": "SEV_040_HIGH",
        "critical": "SEV_050_CRITICAL",
    }
    severity_value = args.get("severity")
    status = args.get("status")
    link_cases = [int(case_id) for case_id in argToList(args.get("link_cases"))] if args.get("link_cases") else []
    unlink_cases = [int(case_id) for case_id in argToList(args.get("unlink_cases"))] if args.get("unlink_cases") else []

    update_args = {
        "assigned_user": args.get("assigned_user_mail"),
        "severity": severity_map.get(severity_value) if severity_value else None,
        "name": args.get("name"),
        "occurred": arg_to_timestamp(args.get("occurred"), ""),
        "phase": args.get("phase"),
        "type": args.get("type"),
        "description": args.get("description"),
        "resolution_status": status_map.get(status) if status else None,
    }

    # Remove None values before sending to API
    filtered_update_args = {k: v for k, v in update_args.items() if v is not None}

    if not filtered_update_args and not link_cases and not unlink_cases:
        raise CortexMissingArgError(
            [
                "assigned_user_mail",
                "severity",
                "name",
                "occurred",
                "phase",
                "type",
                "description",
                "status",
                "link_cases",
                "unlink_cases",
            ],
            require_one=True,
            override_message="Please provide arguments to update the issue.",
        )

    if link_cases:
        client.link_issue_to_cases(int(issue_id), link_cases)
        demisto.debug(f"Linked issue {issue_id} to cases {link_cases}")

    if unlink_cases:
        client.unlink_issue_from_cases(int(issue_id), unlink_cases)
        demisto.debug(f"Unlinked issue {issue_id} from cases {unlink_cases}")

    if filtered_update_args:
        filter_data = create_filter_data(issue_id, filtered_update_args)
        demisto.debug(filter_data)
        client.update_issue(filter_data)

    return "done"


def get_extra_data_for_case_id_command(client: CoreClient, args):
    """
    Retrieves extra data for a specific case ID.

    Args:
        client (Client): The client instance used to send the request.
        args (dict): Dictionary containing the arguments for the command.
                     Expected to include:
                         - case_id (str): The ID of the case to retrieve extra data for.
                         - issues_limit (int): The maximum number of issues to return per case. Default is 1000.

    Returns:
        CommandResults: Object containing the formatted extra data,
                        raw response, and outputs for integration context.
    """
    case_id = args.get("case_id")
    if not case_id:
        raise DemistoException("case_id is required. Please provide a valid numeric case ID.")
    case_id = str(case_id).strip()
    if not case_id.isdigit():
        raise DemistoException(
            f"Invalid case_id '{case_id}'. The case_id must be a valid numeric identifier. "
            "Use the core-get-cases command to retrieve valid case IDs."
        )
    issues_limit = min(int(args.get("issues_limit", 1000)), 1000)
    response = client.get_incident_data(case_id, issues_limit, full_alert_fields=True)
    mapped_response = preprocess_get_case_extra_data_outputs(response)
    case = mapped_response.get("case")
    if int(case.get("issue_count") or 0) > 1:
        try:  # if functionality isn't supported exception is raised and should be handled
            web_app_client = init_client("webapp")
            ai_response = web_app_client.get_case_ai_summary(int(case_id))
            if ai_response:
                reply = ai_response.get("reply", {})
                if case_description := reply.get("case_description"):
                    case["description"] = case_description
                if case_name := reply.get("case_name"):
                    case["case_name"] = case_name
        except Exception as e:
            demisto.debug(f"Failed to retrieve case AI summary for case ID {case_id}: {str(e)}")

    return CommandResults(
        readable_output=tableToMarkdown("Case", mapped_response, headerTransform=string_to_table_header),
        outputs_prefix="Core.CaseExtraData",
        outputs=mapped_response,
        raw_response=mapped_response,
    )


def normalize_key(key: str) -> str:
    """
    Strips the prefixes 'xdm__asset__' or 'xdm__' from the
    beginning of the key, if present, and returns the remaining key unchanged otherwise.

    Args:
        key (str): The original output key.

    Returns:
        str: The normalized key without XDM prefixes.
    """
    if key.startswith("xdm__asset__"):
        return key.removeprefix("xdm__asset__")

    if key.startswith("xdm__"):
        return key.removeprefix("xdm__")

    return key


def search_assets_command(client: Client, args):
    """
    Search for assets in XDR based on the provided filters.
    Args:
        client (Client): The client instance used to send the request.
        args (dict): Dictionary containing the arguments for the command.
                     Expected to include:
                         - asset_names (list[str]): List of asset names to search for.
                         - asset_types (list[str]): List of asset types to search for.
                         - asset_tags (list[str]): List of asset tags to search for.
                         - asset_ids (list[str]): List of asset IDs to search for.
                         - asset_providers (list[str]): List of asset providers to search for.
                         - asset_realms (list[str]): List of asset realms to search for.
                         - asset_group_names (list[str]): List of asset group names to search for.
    """
    asset_group_ids = get_asset_group_ids_from_names(client, argToList(args.get("asset_groups", "")))
    software_package_versions = args.get("software_package_versions", "")
    kubernetes_cluster_versions = args.get("kubernetes_cluster_versions", "")
    filter = FilterBuilder()
    filter.add_field(
        ASSET_FIELDS["asset_names"],
        FilterType.CONTAINS,
        argToList(args.get("asset_names", "")),
    )
    filter.add_field(
        ASSET_FIELDS["asset_tags"],
        FilterType.JSON_WILDCARD,
        safe_load_json(args.get("asset_tags", [])),
    )
    filter.add_field(ASSET_FIELDS["asset_ids"], FilterType.EQ, argToList(args.get("asset_ids", "")))
    filter.add_field(
        ASSET_FIELDS["asset_providers"],
        FilterType.EQ,
        argToList(args.get("asset_providers", "")),
    )
    filter.add_field(
        ASSET_FIELDS["asset_realms"],
        FilterType.EQ,
        argToList(args.get("asset_realms", "")),
    )
    filter.add_field(ASSET_FIELDS["asset_group_ids"], FilterType.ARRAY_CONTAINS, asset_group_ids)
    filter.add_field(
        ASSET_FIELDS["asset_categories"],
        FilterType.EQ,
        argToList(args.get("asset_categories", "")),
    )
    filter.add_field(ASSET_FIELDS["asset_classes"], FilterType.EQ, argToList(args.get("asset_classes", "")))
    filter.add_field(ASSET_FIELDS["software_package_versions"], FilterType.EQ, argToList(software_package_versions))
    filter.add_field(ASSET_FIELDS["kubernetes_cluster_versions"], FilterType.EQ, argToList(kubernetes_cluster_versions))

    asset_types = argToList(args.get("asset_types", ""))
    filter.add_field(ASSET_FIELDS["asset_types"], FilterType.CONTAINS, asset_types)

    page_size = arg_to_number(args.get("page_size", SEARCH_ASSETS_DEFAULT_LIMIT))
    if page_size is None:
        page_size = SEARCH_ASSETS_DEFAULT_LIMIT
    if page_size > SEARCH_ASSETS_MAX_LIMIT:
        raise CortexInvalidArgError(
            "page_size",
            value=page_size,
            reason=f"must not exceed {SEARCH_ASSETS_MAX_LIMIT}",
            override_message=f"page_size cannot exceed {SEARCH_ASSETS_MAX_LIMIT}",
        )

    if page_size == 0:  # 0 Maps to max in the API, we will maintain this behavior with our max value instead
        page_size = SEARCH_ASSETS_MAX_LIMIT

    page_number: int = arg_to_number(args.get("page_number", 0))  # type: ignore[assignment]
    on_demand_fields = ["xdm__asset__tags"]
    version_fields = [
        ("xdm__software_package__version", software_package_versions),
        ("xdm__kubernetes__cluster__version", kubernetes_cluster_versions),
    ]
    on_demand_fields.extend([field for field, condition in version_fields if condition])

    request_data = build_webapp_request_data(
        table_name=ASSETS_TABLE,
        filter_dict=filter.to_dict(),
        limit=page_number * page_size + page_size,
        sort_field="xdm__asset__name",
        sort_order="DESC",
        start_page=page_number * page_size,
        on_demand_fields=on_demand_fields,
    )
    demisto.debug(f"Search Assets Request: {request_data}")
    raw_response = client.get_webapp_data(request_data).get("reply", {}).get("DATA", [])
    # Remove "xdm__asset__" and "xdm__" prefix from all keys in the response
    response = [{normalize_key(k): v for k, v in item.items()} for item in raw_response]

    # Preserve BC after migrating to private API - add related issues/cases as in old API response
    for asset in response:
        asset["related_issues.critical_issues"] = asset.get("issues_critical", [])
        asset["related_issues.issues_breakdown"] = asset.get("issues_breakdown", [])
        asset["related_cases.critical_cases"] = asset.get("cases_critical", [])
        asset["related_cases.cases_breakdown"] = asset.get("cases_breakdown", [])

    return CommandResults(
        readable_output=tableToMarkdown("Assets", response, headerTransform=string_to_table_header),
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.Asset",
        outputs_key_field="id",
        outputs=response,
        raw_response=raw_response,
    )


def validate_scanner_name(scanner_name: str):
    """
    Validate that a scanner name is allowed.

    Args:
        scanner_name (str): The name of the scanner to validate.

    Returns:
        bool: True if the scanner name is valid.

    Raises:
        ValueError: If the scanner name is not in the list of allowed scanners.
    """
    if scanner_name.upper() not in ALLOWED_SCANNERS:
        raise ValueError(f"Invalid scanner '{scanner_name}'. Allowed scanners are: {', '.join(sorted(ALLOWED_SCANNERS))}")


def build_scanner_config_payload(args: dict) -> dict:
    """
    Build a scanner configuration payload for repository scanning.

    Args:
        args (dict): Dictionary containing configuration arguments.
                    Expected to include:
                        - enable_scanners (list): List of scanners to enable.
                        - disable_scanners (list): List of scanners to disable.
                        - pr_scanning (bool): Whether to enable PR scanning.
                        - block_on_error (bool): Whether to block on scanning errors.
                        - tag_resource_blocks (bool): Whether to tag resource blocks.
                        - tag_module_blocks (bool): Whether to tag module blocks.
                        - exclude_paths (list): List of paths to exclude from scanning.

    Returns:
        dict: Scanner configuration payload.

    Raises:
        ValueError: If the same scanner is specified in both enable and disabled lists.
    """
    enabled_scanners = argToList(args.get("enable_scanners", []))
    disabled_scanners = argToList(args.get("disable_scanners", []))
    secret_validation = argToBoolean(args.get("secret_validation", "False"))
    enable_pr_scanning = arg_to_bool_or_none(args.get("pr_scanning"))
    block_on_error = arg_to_bool_or_none(args.get("block_on_error"))
    tag_resource_blocks = arg_to_bool_or_none(args.get("tag_resource_blocks"))
    tag_module_blocks = arg_to_bool_or_none(args.get("tag_module_blocks"))
    exclude_paths = argToList(args.get("exclude_paths", []))

    overlap = set(enabled_scanners) & set(disabled_scanners)
    if overlap:
        raise ValueError(f"Cannot enable and disable the same scanner(s) simultaneously: {', '.join(overlap)}")

    # Build scanners configuration
    scanners = {}
    for scanner in enabled_scanners:
        validate_scanner_name(scanner)
        if scanner.upper() == "SECRETS":
            scanners["SECRETS"] = {
                "isEnabled": True,
                "scanOptions": {"secretValidation": secret_validation},
            }
        else:
            scanners[scanner.upper()] = {"isEnabled": True}

    for scanner in disabled_scanners:
        validate_scanner_name(scanner)
        scanners[scanner.upper()] = {"isEnabled": False}

    # Build scan configuration payload with only relevant arguments
    scan_configuration = {}

    if scanners:
        scan_configuration["scanners"] = scanners

    if args.get("pr_scanning") is not None:
        scan_configuration["prScanning"] = {
            "isEnabled": enable_pr_scanning,
            **({"blockOnError": block_on_error} if block_on_error is not None else {}),
        }

    if args.get("tag_resource_blocks") is not None or args.get("tag_module_blocks") is not None:
        scan_configuration["taggingBot"] = {
            **({"tagResourceBlocks": tag_resource_blocks} if tag_resource_blocks is not None else {}),
            **({"tagModuleBlocks": tag_module_blocks} if tag_module_blocks is not None else {}),
        }

    if exclude_paths:
        scan_configuration["excludedPaths"] = exclude_paths

    demisto.debug(f"{scan_configuration=}")

    return scan_configuration


def enable_scanners_command(client: Client, args: dict):
    """
    Updates repository scan configuration by enabling/disabling scanners and setting scan options.

    Args:
        client (Client): The client instance used to send the request.
        args (dict): Dictionary containing configuration arguments including repository_ids,
                    enabled_scanners, disabled_scanners, and other scan settings.

    Returns:
        CommandResults: Command results with readable output showing update status and raw response.
    """
    repository_ids = argToList(args.get("repository_ids"))
    payload = build_scanner_config_payload(args)

    # Send request to update repository scan configuration
    responses = []
    for repository_id in repository_ids:
        responses.append(client.enable_scanners(payload, repository_id))

    readable_output = f"Successfully updated repositories: {', '.join(repository_ids)}"

    return CommandResults(
        readable_output=readable_output,
        raw_response=responses,
    )


def get_asset_group_ids_from_names(client: Client, group_names: list[str]) -> list[str]:
    """
    Retrieves the IDs of asset groups based on their names.

    Args:
        client (Client): The client instance used to send the request.
        group_names (list[str]): List of asset group names to retrieve IDs for.

    Returns:
        list[str]: List of asset group IDs.
    """
    if not group_names:
        return []

    filter = FilterBuilder()
    filter.add_field("XDM.ASSET_GROUP.NAME", FilterType.EQ, group_names)
    filter_str = filter.to_dict()

    groups = client.search_asset_groups(filter_str).get("reply", {}).get("data", [])

    group_ids = [group.get("XDM.ASSET_GROUP.ID") for group in groups if group.get("XDM.ASSET_GROUP.ID")]

    if len(group_ids) != len(group_names):
        found_groups = [group.get("XDM.ASSET_GROUP.NAME") for group in groups if group.get("XDM.ASSET_GROUP.ID")]
        missing_groups = [name for name in group_names if name not in found_groups]
        raise DemistoException(f"Failed to fetch asset group IDs for {missing_groups}. Ensure the asset group names are valid.")

    return group_ids


def appsec_remediate_issue_command(client: Client, args: dict) -> CommandResults:
    """
    Create automated pull requests to fix multiple security issues in a single bulk operation.

    Args:
        client (Client): The client instance used to send the request.
        args (dict): Dictionary containing the arguments for the command.
                     Expected to include:
                         - issueIds (str): List of issue IDs to fix.
                         - title (str): Title of the PR triggered.

    Returns:
        CommandResults: Object containing the formatted extra data,
                        raw response, and outputs for integration context.
    """
    args = demisto.args()
    issue_ids = argToList(args.get("issue_ids"))
    if len(issue_ids) > 10:
        raise DemistoException("Please provide a maximum of 10 issue IDs per request.")

    triggered_prs = []
    for issue_id in issue_ids:
        request_body = {"issueIds": [issue_id], "title": args.get("title")}
        request_body = remove_empty_elements(request_body)
        current_response = client.appsec_remediate_issue(request_body)
        if current_response and isinstance(current_response, dict):
            current_triggered_prs = current_response.get("triggeredPrs")
            if isinstance(current_triggered_prs, list) and len(current_triggered_prs) > 0:
                triggered_prs.append(current_triggered_prs[0])

    return CommandResults(
        readable_output=tableToMarkdown(name="Triggered PRs", t=triggered_prs),
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.TriggeredPRs",
        outputs=triggered_prs,
        outputs_key_field="issueId",
        raw_response=triggered_prs,
    )


def build_asset_coverage_filter(args: dict) -> FilterBuilder:
    filter_builder = FilterBuilder()
    filter_builder.add_field("asset_id", FilterType.CONTAINS, argToList(args.get("asset_id")))
    filter_builder.add_field("asset_name", FilterType.CONTAINS, argToList(args.get("asset_name")))
    filter_builder.add_field(
        "business_application_names",
        FilterType.ARRAY_CONTAINS,
        argToList(args.get("business_application_names")),
    )
    filter_builder.add_field("status_coverage", FilterType.EQ, argToList(args.get("status_coverage")))
    filter_builder.add_field(
        "is_scanned_by_vulnerabilities",
        FilterType.EQ,
        argToList(args.get("is_scanned_by_vulnerabilities")),
    )
    filter_builder.add_field(
        "is_scanned_by_code_weakness",
        FilterType.EQ,
        argToList(args.get("is_scanned_by_code_weakness")),
    )
    filter_builder.add_field(
        "is_scanned_by_secrets",
        FilterType.EQ,
        argToList(args.get("is_scanned_by_secrets")),
    )
    filter_builder.add_field("is_scanned_by_iac", FilterType.EQ, argToList(args.get("is_scanned_by_iac")))
    filter_builder.add_field(
        "is_scanned_by_malware",
        FilterType.EQ,
        argToList(args.get("is_scanned_by_malware")),
    )
    filter_builder.add_field("is_scanned_by_cicd", FilterType.EQ, argToList(args.get("is_scanned_by_cicd")))
    filter_builder.add_field("last_scan_status", FilterType.EQ, argToList(args.get("last_scan_status")))
    filter_builder.add_field("asset_type", FilterType.EQ, argToList(args.get("asset_type")))
    filter_builder.add_field("unified_provider", FilterType.EQ, argToList(args.get("asset_provider")))
    filter_builder.add_field("asset_provider", FilterType.EQ, argToList(args.get("vendor_name")))

    return filter_builder


def build_exception_rules_filter(args: dict) -> FilterBuilder:
    filter_builder = FilterBuilder()
    filter_builder.add_field("ID", FilterType.CONTAINS, argToList(args.get("id")))
    filter_builder.add_field("NAME", FilterType.CONTAINS, argToList(args.get("rule_name")))
    filter_builder.add_field("PLATFORM", FilterType.EQ, argToList(args.get("platform")))
    filter_builder.add_field("CONDITIONS_PRETTY", FilterType.CONTAINS, argToList(args.get("conditions")))
    filter_builder.add_field("CREATED_BY", FilterType.CONTAINS, argToList(args.get("created_by")))
    filter_builder.add_field("USER_EMAIL", FilterType.CONTAINS, argToList(args.get("user_email")))
    start_modification_time_str, end_modification_time_str = (
        args.get("start_modification_time"),
        args.get("end_modification_time"),
    )
    if end_modification_time_str and not start_modification_time_str:
        start_modification_time_str = (
            "1970-01-01"  # The standard "beginning of time" for most systems - beginning_of_unix = datetime.fromtimestamp(0)
        )
    filter_builder.add_time_range_field("MODIFICATION_TIME", start_modification_time_str, end_modification_time_str)
    filter_builder.add_field("STATUS", FilterType.EQ, argToList(args.get("status")))
    filter_builder.add_field("SUBTYPE", FilterType.EQ, argToList(args.get("rule_type")))
    return filter_builder


def get_asset_coverage_command(client: Client, args: dict):
    """
    Retrieves ASPM assets coverage using the generic /api/webapp/get_data endpoint.
    """

    request_data = build_webapp_request_data(
        table_name=ASSET_COVERAGE_TABLE,
        filter_dict=build_asset_coverage_filter(args).to_dict(),
        limit=arg_to_number(args.get("limit")) or 100,
        sort_field=args.get("sort_field"),
        sort_order=args.get("sort_order"),
    )
    response = client.get_webapp_data(request_data)
    reply = response.get("reply", {})
    data = reply.get("DATA", [])

    readable_output = tableToMarkdown(
        "ASPM Coverage",
        data,
        headerTransform=string_to_table_header,
        sort_headers=False,
    )
    return CommandResults(
        readable_output=readable_output,
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.Coverage.Asset",
        outputs_key_field="asset_id",
        outputs=data,
        raw_response=response,
    )


def get_asset_coverage_histogram_command(client: Client, args: dict):
    """
    Retrieves ASPM assets coverage histogrm using the generic /api/webapp/get_histograms endpoint.
    """
    columns = argToList(args.get("columns"))
    columns = [COVERAGE_API_FIELDS_MAPPING.get(col, col) for col in columns]
    if not columns:
        raise ValueError("Please provide column value to create the histogram.")
    request_data = build_histogram_request_data(
        table_name=ASSET_COVERAGE_TABLE,
        filter_dict=build_asset_coverage_filter(args).to_dict(),
        columns=columns,
        max_values_per_column=arg_to_number(args.get("max_values_per_column")) or 100,
    )

    response = client.get_webapp_histograms(request_data)
    reply = response.get("reply", {})
    outputs = [{"column_name": column_name, "data": data} for column_name, data in reply.items()]

    readable_output = "\n".join(
        tableToMarkdown(
            f"ASPM Coverage {output['column_name']} Histogram",
            output["data"],
            headerTransform=string_to_table_header,
            sort_headers=False,
        )
        for output in outputs
    )

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.Coverage.Histogram",
        outputs=outputs,
        raw_response=response,
    )


def get_ai_model_activity_command(client: Client, args: dict) -> CommandResults:
    """
    Retrieves AI model activity information using the generic /api/webapp/get_data endpoint.

    Args:
        client (Client): The client instance used to send the request.
        args (dict): Dictionary containing the arguments for the command.
                     Expected to include:
                         - asset_id (str): Comma-separated list of asset IDs to query.

    Returns:
        CommandResults: Object containing the formatted AI model activity data,
                        raw response, and outputs for integration context.
    """
    demisto.debug(f"get_ai_model_activity_command called with args: {args}")
    asset_ids = argToList(args.get("asset_id"))
    filter_builder = FilterBuilder()
    filter_builder.add_field("asset_id", FilterType.EQ, asset_ids)
    filter_dict = filter_builder.to_dict()
    demisto.debug(f"Built filter_dict: {filter_dict}")

    request_data = build_webapp_request_data(
        table_name=AI_MODEL_ACTIVITY_TABLE,
        filter_dict=filter_dict,
        limit=len(asset_ids),
        sort_field=None,
    )
    demisto.debug(f"Built request_data: {request_data}")

    response = client.get_webapp_data(request_data)
    reply = response.get("reply", {})
    data = reply.get("DATA", [])

    readable_output = tableToMarkdown(
        "AI Model Activity",
        data,
        headerTransform=string_to_table_header,
        sort_headers=False,
    )

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.AIModelActivity",
        outputs_key_field="asset_id",
        outputs=data,
        raw_response=response,
    )


def get_appsec_rule_ids_from_names(client, rule_names: list[str]) -> list[str]:
    """
    Retrieves the IDs of AppSec rules based on their names using exact and partial matching.

    Args:
        client (Client): The client instance used to send the request.
        rule_names (list[str]): List of AppSec rule names to retrieve IDs for.

    Returns:
        list[str]: List of AppSec rule IDs.

    Raises:
        DemistoException: If any rule names cannot be found in the system.
    """
    if not rule_names:
        return []

    fb = FilterBuilder()
    fb.add_field("ruleName", FilterType.EQ, rule_names)
    data = (
        client.get_webapp_data(build_webapp_request_data(APPSEC_RULES_TABLE, fb.to_dict(), limit=200, sort_field="ruleName"))
        .get("reply", {})
        .get("DATA", [])
        or []
    )

    lookup = {r["ruleName"].lower(): r["ruleId"] for r in data if r.get("ruleId")}
    ids, found = [], set()

    for name in rule_names:
        n = name.lower()
        rid = lookup.get(n) or next((v for k, v in lookup.items() if n in k), None)
        if rid:
            ids.append(rid)
            found.add(name)

    missing = set(rule_names) - found
    if missing:
        raise DemistoException(f"Missing AppSec rules: {', '.join(missing)}")

    return ids


def create_policy_command(client: Client, args: dict) -> CommandResults:
    """
    Creates a new policy in Cortex Platform with defined conditions, scope, and triggers.
    Args:
        client: The Cortex Platform client instance.
        args: Dictionary containing policy configuration parameters including:
            - policy_name: Required name for the new policy
            - description: Optional policy description
            - asset_group_names: Asset groups to apply the policy to
            - conditions_*: Various condition parameters (finding type, severity, etc.)
            - scope_*: Policy scope configuration parameters
            - trigger_*: Policy trigger configuration (periodic, PR, CI/CD)
            - suggestion_id: Optional ID of an AI-generated policy suggestion to link to

    Returns:
        CommandResults: Results object containing the created policy information with
        readable output, outputs prefix, and raw response data.

    Raises:
        DemistoException: If policy name is missing or no triggers are enabled.
    """
    policy_name = args.get("policy_name")
    if not policy_name:
        raise DemistoException("Policy name is required.")

    description = args.get("description", "")
    group_names = argToList(args.get("asset_group_names"))
    asset_group_ids = get_asset_group_ids_from_names(client, group_names)

    conditions = create_policy_build_conditions(client, args)
    scope = create_policy_build_scope(args)
    triggers = create_policy_build_triggers(args)

    # Ensure at least one trigger is enabled
    if not any(trigger.get("isEnabled") for trigger in triggers.values()):
        raise DemistoException("At least one trigger (periodic, PR, or CI/CD) must be enabled for the policy.")

    payload = {
        "name": policy_name,
        "description": description,
        "conditions": conditions,
        "scope": scope,
        "assetGroupIds": asset_group_ids,
        "triggers": triggers,
    }

    # Optional: link to an AI-generated policy suggestion
    if suggestion_id := args.get("suggestion_id"):
        payload["suggestionId"] = suggestion_id

    payload = json.dumps(payload)

    client.create_policy(payload)

    return CommandResults(readable_output=f"AppSec policy '{policy_name}' created successfully.")


def create_policy_build_conditions(client: Client, args: dict) -> dict:
    """
    Build conditions for create-policy command based on provided arguments.

    Creates a filter structure for policy conditions including finding types, severity,
    developer suppression, backlog status, package information, AppSec rules, CVSS/EPSS scores,
    and various boolean conditions. If no finding types are specified, defaults to all types
    except "CI/CD Risk".

    Args:
        client: The Cortex Platform client instance
        args: Dictionary containing condition arguments from the command

    Returns:
        dict: Filter dictionary containing all specified conditions
    """
    builder = FilterBuilder()

    finding_types = argToList(args.get("conditions_finding_type"))
    if not finding_types:
        # Default to all finding types if none specified
        finding_types = [ft for ft in POLICY_FINDING_TYPE_MAPPING if ft != "CI/CD Risk"]

    # Support both human-readable names ("Vulnerabilities") and raw API values ("CAS_CVE_SCANNER").
    api_values = set(POLICY_FINDING_TYPE_MAPPING.values())
    resolved_finding_types = []
    for ft in finding_types:
        if ft in api_values:
            resolved_finding_types.append(ft)
        elif ft in POLICY_FINDING_TYPE_MAPPING:
            resolved_finding_types.append(POLICY_FINDING_TYPE_MAPPING[ft])

    builder.add_field("Finding Type", FilterType.EQ, resolved_finding_types)

    # Severity
    if severities := argToList(args.get("conditions_severity")):
        builder.add_field("Severity", FilterType.EQ, severities)

    # Developer Suppression
    if dev_supp := arg_to_bool_or_none(args.get("conditions_respect_developer_suppression")):
        builder.add_field("Respect Developer Suppression", FilterType.EQ, dev_supp)

    # Packages
    package_field_map = {
        "package_name": ("PackageName", FilterType.EQ),
        "package_version": ("PackageVersion", FilterType.EQ),
        "package_operational_risk": ("Package Operational Risk", FilterType.EQ),
    }
    for field, (api_field_name, op) in package_field_map.items():
        if val := args.get(f"conditions_{field}"):
            builder.add_field(api_field_name, op, val)

    # AppSec Rules
    if rule_names := argToList(args.get("conditions_appsec_rule_names")):
        rule_ids = get_appsec_rule_ids_from_names(client, rule_names)
        builder.add_field("AppSec Rule", FilterType.EQ, rule_ids)

    # HasAFix
    if has_a_fix := arg_to_bool_or_none(args.get("conditions_has_a_fix")):
        builder.add_field("HasAFix", FilterType.EQ, has_a_fix)

    # Backlog
    if backlog := args.get("conditions_backlog_status"):
        builder.add_field("Backlog Status", FilterType.EQ, backlog)

    # IsKev
    if is_kev := arg_to_bool_or_none(args.get("conditions_is_kev")):
        builder.add_field("IsKev", FilterType.EQ, is_kev)

    # CVSS / EPSS
    for f, n in [("cvss", "CVSS"), ("epss", "EPSS")]:
        if val := arg_to_number(args.get(f"conditions_{f}")):
            builder.add_field(n, FilterType.GTE, val)

    # Secret Validity, License Type
    for key, label in {
        "secret_validity": "SecretValidity",
        "license_type": "LicenseType",
    }.items():
        if vals := argToList(args.get(f"conditions_{key}", [])):
            builder.add_field(label, FilterType.EQ, vals)

    return builder.to_dict()


def parse_custom_fields(custom_fields: str) -> dict:
    """
    Parse and sanitize custom fields from JSON string input.

    Accepts two formats:
    - Dict: ``{"field1": "value1", "field2": ["a", "b"]}``
    - List of single-key objects (legacy): ``[{"field1": "value1"}, {"field2": ["a", "b"]}]``

    Args:
        custom_fields: JSON string in either dict or list-of-objects format.

    Returns:
        dict: Dictionary with sanitized alphanumeric keys and native values.
              Values are passed as-is (no stringification) so that multiselect
              list values, booleans, and numbers reach the API in the correct type.
              Duplicate keys are ignored (first occurrence wins).
    """
    parsed = safe_load_json(custom_fields)

    parsed_fields: dict = {}

    if isinstance(parsed, dict):
        # New preferred format: {"field1": "value1", "field2": ["a", "b"]}
        raw_items = list(parsed.items())
    elif isinstance(parsed, list):
        # Legacy format: [{"field1": "value1"}, {"field2": ["a", "b"]}]
        raw_items = [(k, v) for obj in parsed if isinstance(obj, dict) for k, v in obj.items()]
    else:
        return {}

    for key, value in raw_items:
        # Sanitize key: remove non-alphanumeric characters
        sanitized_key = "".join(char for char in key if char.isalnum())
        if sanitized_key and sanitized_key not in parsed_fields:
            parsed_fields[sanitized_key] = value

    return parsed_fields


def create_policy_build_scope(args: dict) -> dict:
    """
    Build scope filters for create-policy.
    Processes various scope parameters including categories, business applications,
    repository settings, and boolean filters like public repository status and
    security characteristics.

    Args:
        args: Dictionary containing scope filter parameters with keys like:
            - scope_category: List of categories to filter by
            - scope_business_application_names: Business application names
            - scope_application_business_criticality: Application criticality level
            - scope_repository_name: Repository name to filter
            - scope_is_public_repository: Boolean for public repository filter
            - scope_has_deployed_assets: Boolean for deployed assets filter
            - scope_has_internet_exposed_deployed_assets: Boolean for internet exposure filter
            - scope_has_sensitive_data_access: Boolean for sensitive data access filter
            - scope_has_privileged_capabilities: Boolean for privileged capabilities filter

    Returns:
        dict: Filter dictionary structure for policy scope, can be empty if no scope filters set
    """
    builder = FilterBuilder()

    # Category
    if categories := argToList(args.get("scope_category", [])):
        resolved_categories = [POLICY_CATEGORY_MAPPING.get(c.title(), POLICY_CATEGORY_MAPPING.get(c, c)) for c in categories]
        builder.add_field("category", FilterType.EQ, resolved_categories)

    # Business application names — always use ARRAY_CONTAINS since the field is an array type.
    if business_app_names := argToList(args.get("scope_business_application_names")):
        builder.add_field("business_application_names", FilterType.ARRAY_CONTAINS, business_app_names)

    # Application business criticality
    if app_criticality := argToList(args.get("scope_application_business_criticality")):
        builder.add_field("application_business_criticality", FilterType.EQ, app_criticality)

    # Repository name
    if repo_name := args.get("scope_repository_name"):
        builder.add_field("repository_name", FilterType.CONTAINS, repo_name)

    # Boolean scope filters
    for key, label in {
        "scope_is_public_repository": "is_public_repository",
        "scope_has_deployed_assets": "has_deployed_assets",
        "scope_has_internet_exposed_deployed_assets": "has_internet_exposed",
        "scope_has_sensitive_data_access": "has_sensitive_data_access",
        "scope_has_privileged_capabilities": "has_leverage_privileged_capabilities",
    }.items():
        if val := arg_to_bool_or_none(args.get(key)):
            builder.add_field(label, FilterType.EQ, val)

    # Always return the filter dict (can be empty for scope)
    return builder.to_dict()


def create_policy_build_triggers(args: dict) -> dict:
    """
    Build triggers configuration for policy creation.

    Creates a complete triggers structure with periodic, PR, and CI/CD trigger types.
    Each trigger includes enabled status, actions, and optional severity overrides.

    Args:
        args (dict): Command arguments containing trigger configuration parameters:
            - triggers_periodic_report_issue: Enable periodic issue reporting
            - triggers_periodic_override_severity: Override severity for periodic triggers
            - triggers_pr_report_issue: Enable PR issue reporting
            - triggers_pr_block_pr: Enable PR blocking
            - triggers_pr_report_pr_comment: Enable PR comment reporting
            - triggers_pr_override_severity: Override severity for PR triggers
            - triggers_cicd_report_issue: Enable CI/CD issue reporting
            - triggers_cicd_block_cicd: Enable CI/CD blocking
            - triggers_cicd_report_cicd: Enable CI/CD reporting
            - triggers_cicd_override_severity: Override severity for CI/CD triggers

    Returns:
        dict: Triggers configuration with periodic, PR, and CI/CD sections.
              Each section contains isEnabled flag, actions dict, and overrideIssueSeverity.

    Raises:
        DemistoException: When no triggers are enabled (at least one must be set).

    Note:
        When an override severity is specified, reportIssue is automatically enabled
        for that trigger type.
    """
    # Periodic trigger
    periodic_report_issue = argToBoolean(args.get("triggers_periodic_report_issue", False))
    periodic_override = args.get("triggers_periodic_override_severity")

    # If override is set, reportIssue must be True
    if periodic_override:
        periodic_report_issue = True

    periodic_enabled = periodic_report_issue or bool(periodic_override)

    # PR trigger
    pr_report_issue = argToBoolean(args.get("triggers_pr_report_issue", False))
    pr_block_pr = argToBoolean(args.get("triggers_pr_block_pr", False))
    pr_report_comment = argToBoolean(args.get("triggers_pr_report_pr_comment", False))
    pr_override = args.get("triggers_pr_override_severity")

    # If override is set, reportIssue must be True
    if pr_override:
        pr_report_issue = True

    pr_enabled = pr_report_issue or pr_block_pr or pr_report_comment or bool(pr_override)

    # CI/CD trigger
    cicd_report_issue = argToBoolean(args.get("triggers_cicd_report_issue", False))
    cicd_block_cicd = argToBoolean(args.get("triggers_cicd_block_cicd", False))
    cicd_report_cicd = argToBoolean(args.get("triggers_cicd_report_cicd", False))
    cicd_override = args.get("triggers_cicd_override_severity")

    # If override is set, reportIssue must be True
    if cicd_override:
        cicd_report_issue = True

    cicd_enabled = cicd_report_issue or cicd_block_cicd or cicd_report_cicd or bool(cicd_override)

    triggers = {
        "periodic": {
            "isEnabled": periodic_enabled,
            "actions": {"reportIssue": periodic_report_issue},
        },
        "pr": {
            "isEnabled": pr_enabled,
            "actions": {
                "reportIssue": pr_report_issue,
                "blockPr": pr_block_pr,
                "reportPrComment": pr_report_comment,
            },
        },
        "cicd": {
            "isEnabled": cicd_enabled,
            "actions": {
                "reportIssue": cicd_report_issue,
                "blockCicd": cicd_block_cicd,
                "reportCicd": cicd_report_cicd,
            },
        },
    }

    # Add override severity if specified (and set to null if not specified)
    triggers["periodic"]["overrideIssueSeverity"] = periodic_override if periodic_override else None
    triggers["pr"]["overrideIssueSeverity"] = pr_override if pr_override else None
    triggers["cicd"]["overrideIssueSeverity"] = cicd_override if cicd_override else None

    # Ensure at least one trigger is enabled
    if not any(t["isEnabled"] for t in triggers.values()):
        raise DemistoException("At least one trigger (periodic, PR, or CI/CD) must be set.")

    return triggers


def create_appsec_issues_filter_and_tables(args: dict) -> dict[str, FilterBuilder]:
    """
    Generate a filter and determine applicable tables for fetching AppSec issues based on input filter arguments.

    Args:
        args (dict): Command input args for core-appsec-get-issues.

    Returns:
        tuple[list, FilterBuilder]: A tuple containing:
            - A list of applicable issue type table names
            - A FilterBuilder instance with configured filters
    """
    special_filter_args = {filter for filter in args if filter in AppsecIssues.SPECIAL_FILTERS}
    tables_filters = {}
    filter_builder = FilterBuilder()

    # If issue_category is specified, restrict to those specific tables
    requested_types = argToList(args.get("issue_category"))
    if requested_types:
        valid_table_names = {it.table_name for it in AppsecIssues.ISSUE_TYPES}
        for rt in requested_types:
            rt_lower = rt.lower().strip()
            table_name = ISSUE_TYPE_TABLE_MAPPING.get(rt_lower) or rt.upper().strip()
            if table_name in valid_table_names:
                tables_filters[table_name] = filter_builder
            else:
                raise DemistoException(
                    f"Unknown issue type '{rt}'. " f"Supported values: {', '.join(ISSUE_TYPE_TABLE_MAPPING.keys())}."
                )
    else:
        for issue_type in AppsecIssues.ISSUE_TYPES:
            if special_filter_args.issubset(issue_type.filters):
                tables_filters[issue_type.table_name] = filter_builder

    if not tables_filters:
        raise DemistoException(f"No matching issue type found for the given filter combination: {special_filter_args}")

    filter_builder.add_field(
        "cas_issues_cvss_score",
        FilterType.GTE,
        arg_to_float(args.get("cvss_score_gte")),
    )
    filter_builder.add_field(
        "cas_issues_epss_score",
        FilterType.GTE,
        arg_to_float(args.get("epss_score_gte")),
    )
    filter_builder.add_field("cas_issues_is_kev", FilterType.EQ, arg_to_bool_or_none(args.get("has_kev")))
    filter_builder.add_field(
        "cas_sla_status",
        FilterType.EQ,
        argToList(args.get("sla")),
        AppsecIssues.SLA_MAPPING,
    )
    filter_builder.add_field(
        "cas_issues_is_fixable",
        FilterType.EQ,
        arg_to_bool_or_none(args.get("automated_fix_available")),
    )
    filter_builder.add_field("cas_issues_validation", FilterType.EQ, argToList(args.get("validation")))
    filter_builder.add_field("urgency", FilterType.EQ, argToList(args.get("urgency")))
    filter_builder.add_field(
        "severity",
        FilterType.EQ,
        argToList(args.get("severity")),
        AppsecIssues.SEVERITY_MAPPINGS,
    )
    filter_builder.add_field("internal_id", FilterType.CONTAINS, argToList(args.get("issue_id")))
    filter_builder.add_field("alert_name", FilterType.CONTAINS, argToList(args.get("issue_name")))
    filter_builder.add_field("cas_issues_asset_name", FilterType.CONTAINS, argToList(args.get("asset_name")))
    filter_builder.add_field("cas_issues_repository", FilterType.CONTAINS, argToList(args.get("repository")))
    filter_builder.add_field("cas_issues_file_path", FilterType.CONTAINS, argToList(args.get("file_path")))
    filter_builder.add_field("cas_issues_git_user", FilterType.CONTAINS, argToList(args.get("collaborator")))
    filter_builder.add_field("status_progress", FilterType.EQ, argToList(args.get("status")))
    filter_builder.add_time_range_field("local_insert_ts", args.get("start_time"), args.get("end_time"))
    filter_builder.add_field_with_mappings(
        "assigned_to_pretty",
        FilterType.CONTAINS,
        argToList(args.get("assignee")),
        {
            "unassigned": FilterType.IS_EMPTY,
            "assigned": FilterType.NIS_EMPTY,
        },
    )

    if "backlog_status" in args and "ISSUES_CI_CD" in tables_filters:
        # backlog filter is different for the CI/CD issue table
        cicd_filter_builder = copy.deepcopy(filter_builder)
        cicd_filter_builder.add_field("issue_backlog_status", FilterType.EQ, argToList(args.get("backlog_status")))
        tables_filters["ISSUES_CI_CD"] = cicd_filter_builder

    filter_builder.add_field("backlog_status", FilterType.EQ, argToList(args.get("backlog_status")))

    return tables_filters


def normalize_and_filter_appsec_issue(issue: dict) -> dict:
    """
    Transforms raw issue data from the main issue table into the AppSec issues format.

    Args:
        raw_issue (dict): Raw issue data retrieved from the alerts view table.

    Returns:
        dict: issue with standard Appsec fields.
    """
    issue_all_fields = cast(dict, alert_to_issue(issue))

    filtered_output_keys: dict[str, dict] = {
        "internal_id": {"path": ["internal_id"]},
        "severity": {
            "path": ["severity"],
            "mapper": AppsecIssues.SEVERITY_OUTPUT_MAPPINGS,
        },
        "issue_name": {"path": ["issue_name"]},
        "issue_source": {"path": ["issue_source"]},
        "issue_category": {"path": ["issue_category"]},
        "issue_domain": {"path": ["issue_domain"]},
        "issue_description": {"path": ["issue_description"]},
        "status": {
            "path": ["status_progress"],
            "mapper": AppsecIssues.STATUS_OUTPUT_MAPPINGS,
        },
        "asset_name": {"path": ["cas_issues_asset_name"]},
        "assignee": {"path": ["assigned_to_pretty"]},
        "time_added": {"path": ["source_insert_ts"]},
        "epss_score": {"path": ["cas_issues_extended_fields", "epss_score"]},
        "cvss_score": {"path": ["cas_issues_normalized_fields", "xdm.vulnerability.cvss_score"]},
        "has_kev": {"path": ["cas_issues_is_kev"]},
        "urgency": {"path": ["urgency"], "mapper": AppsecIssues.URGENCY_OUTPUT_MAPPING},
        "sla_status": {
            "path": ["cas_sla_status"],
            "mapper": AppsecIssues.SLA_OUTPUT_MAPPING,
        },
        "secret_validation": {"path": ["secret_validation"]},
        "is_fixable": {"path": ["cas_issues_is_fixable"]},
        "repository_name": {"path": ["cas_issues_normalized_fields", "xdm.repository.name"]},
        "package_version": {"path": ["cas_issues_extended_fields", "package_version"]},
        "fix_versions": {"path": ["cas_issues_normalized_fields", "xdm.vulnerability.fix_versions"]},
        "repository_organization": {"path": ["cas_issues_normalized_fields", "xdm.repository.organization"]},
        "file_path": {"path": ["cas_issues_normalized_fields", "xdm.file.path"]},
        "collaborator": {"path": ["cas_issues_normalized_fields", "xdm.code.git.commit.author.name"]},
        "is_deployed": {"path": ["cas_issues_extended_fields", "urgency", "metric", "is_deployed"]},
        "repository_is_public": {"path": ["cas_issues_extended_fields", "repository_is_public"]},
        "backlog_status": {"path": ["backlog_status"]},
    }
    appsec_issue = {}
    for output_key, output_info in filtered_output_keys.items():
        current_value = issue_all_fields
        path = output_info.get("path", {})
        for key in path:
            current_value = current_value.get(key, {})

        if current_value:
            value = current_value if "mapper" not in output_info else output_info.get("mapper", {}).get(current_value)
            appsec_issue[output_key] = value

    return appsec_issue


def get_endpoint_support_file_command(client: Client, args: dict) -> CommandResults:
    endpoint_ids = argToList(args.get("endpoint_ids"))

    filter_builder = FilterBuilder()
    filter_builder.add_field("AGENT_ID", FilterType.EQ, endpoint_ids)
    request_data = {
        "request_data": {
            "filter_data": {"filter": filter_builder.to_dict()},
            "filter_type": "static",
        }
    }

    response = client.get_endpoint_support_file(request_data)

    reply = response.get("reply", {})
    group_action_id = reply.get("group_action_id")

    if not group_action_id:
        raise DemistoException("No group_action_id found. Please ensure that valid endpoint IDs are provided.")

    readable_output = f"Endpoint support file request submitted successfully. Group Action ID: {group_action_id}"

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.EndpointSupportFile",
        outputs_key_field="group_action_id",
        outputs=reply,
        raw_response=response,
    )


def send_endpoint_heartbeat_command(client: Client, args: dict) -> CommandResults:
    """
    Perform endpoint heartbeat.
    Args:
        client (Client): The client instance used to send the request.
        args (dict): Dictionary containing the arguments for the command.
                     Expected to include:
                         - endpoint_id (str): The ID of the endpoint.
    Returns:
        CommandResults: Object containing the formatted output.
    """
    call_home_type_heartbeat = 6
    endpoint_id = args.get("endpoint_id")
    if not endpoint_id:
        raise ValueError("endpoint_id is required")

    json_data = {
        "request_data": {
            "endpoint_id": endpoint_id,
            "call_home_type": call_home_type_heartbeat,
        }
    }

    client.send_endpoint_heartbeat(json_data)

    return CommandResults(
        readable_output=f"Heartbeat sent successfully for endpoint {endpoint_id}",
    )


def get_appsec_issues_command(client: Client, args: dict) -> CommandResults:
    """
    Retrieves application security issues based on specified filters across multiple issue types.
    """
    limit = arg_to_number(args.get("limit")) or 50
    sort_field = args.get("sort_field", "severity")
    sort_order = args.get("sort_order", "DESC")

    tables_filters: dict[str, FilterBuilder] = create_appsec_issues_filter_and_tables(args)

    all_appsec_issues: list[dict] = []
    for table_name, filter_builder in tables_filters.items():
        request_data = build_webapp_request_data(
            table_name=table_name,
            filter_dict=filter_builder.to_dict(),
            limit=limit,
            sort_field=sort_field,
            sort_order=sort_order,
        )
        try:
            demisto.debug(f"Fetching issues from table {table_name}")
            response = client.get_webapp_data(request_data)
            reply = response.get("reply", {})
            data = reply.get("DATA", [])
            all_appsec_issues.extend(data)
        except Exception as e:
            raise DemistoException(f"Failed to retrieve issues from the {table_name} table: {e}")

    sorted_issues = sorted(
        all_appsec_issues,
        key=lambda issue: issue.get(sort_field, ""),
        reverse=(sort_order == "DESC"),
    )
    sorted_issues = sorted_issues[:limit]
    filtered_appsec_issues = [normalize_and_filter_appsec_issue(issue) for issue in sorted_issues]

    readable_output = tableToMarkdown(
        "Application Security Issues",
        filtered_appsec_issues,
        headerTransform=string_to_table_header,
        sort_headers=False,
    )

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.AppsecIssue",
        outputs_key_field="internal_id",
        outputs=filtered_appsec_issues,
        raw_response=all_appsec_issues,
    )


def update_case_command(client: Client, args: dict) -> CommandResults:
    """
    Updates one or more cases with the specified parameters such as name, description, assignee, status, and custom fields.

    Handles case status changes including resolution with proper validation, and supports bulk updates across multiple cases.
    Validates input parameters and returns appropriate error messages for invalid values.
    """
    case_ids = argToList(args.get("case_id"))
    case_name = args.get("case_name", "")
    description = args.get("description", "")
    assignee = args.get("assignee", "").lower()
    status = args.get("status", "")
    notes = args.get("notes", "")
    starred = args.get("starred", "")
    user_defined_severity = args.get("user_defined_severity", "")
    resolve_reason = args.get("resolve_reason", "")
    resolved_comment = args.get("resolved_comment", "")
    resolve_all_alerts = args.get("resolve_all_alerts", "")
    custom_fields = parse_custom_fields(args.get("custom_fields", []))

    if status == "resolved" and (not resolve_reason or not CaseManagement.STATUS_RESOLVED_REASON.get(resolve_reason, False)):
        raise CortexMissingArgError(
            "resolve_reason",
            override_message="In order to set the case to resolved, you must provide a resolve reason.",
        )

    if (resolve_reason or resolve_all_alerts or resolved_comment) and status != "resolved":
        conflicting = [arg for arg in ("resolve_reason", "resolve_all_alerts", "resolved_comment") if args.get(arg)]
        raise CortexConflictingArgsError(
            override_message=(
                "In order to use resolve_reason, resolve_all_alerts, or resolved_comment, the case status must be set to "
                "'resolved'."
            ),
            arguments=conflicting + ["status"],
            reason="resolve_reason, resolve_all_alerts, and resolved_comment can only be used when status is 'resolved'.",
            resolution="Set status to 'resolved' or remove the resolution-specific arguments.",
        )

    if status and not CaseManagement.STATUS.get(status):
        raise CortexInvalidArgError(
            "status",
            value=status,
            allowed_values=list(CaseManagement.STATUS.keys()),
            override_message=f"Invalid status '{status}'. Valid statuses are: {list(CaseManagement.STATUS.keys())}",
        )

    if user_defined_severity and not CaseManagement.SEVERITY.get(user_defined_severity, False):
        raise CortexInvalidArgError(
            "user_defined_severity",
            value=user_defined_severity,
            allowed_values=list(CaseManagement.SEVERITY.keys()),
            override_message=(
                f"Invalid user_defined_severity '{user_defined_severity}'. Valid severities are: "
                f"{list(CaseManagement.SEVERITY.keys())}"
            ),
        )

    valid_fields_to_update, error_messages = validate_custom_fields(custom_fields, client)

    # Build request_data with mapped and filtered values
    case_update_payload = {
        "caseName": case_name if case_name else None,
        "description": description if description else None,
        "assignedUser": assignee if assignee else None,
        "notes": notes if notes else None,
        "starred": argToBoolean(starred) if starred else None,
        "status": CaseManagement.STATUS.get(status) if status else None,
        "userSeverity": CaseManagement.SEVERITY.get(user_defined_severity) if user_defined_severity else None,
        "resolve_reason": CaseManagement.STATUS_RESOLVED_REASON.get(resolve_reason) if resolve_reason else None,
        "caseResolvedComment": resolved_comment if resolved_comment else None,
        "resolve_all_alerts": resolve_all_alerts if resolve_all_alerts else None,
        "CustomFields": valid_fields_to_update if valid_fields_to_update else None,
    }
    remove_nulls_from_dictionary(case_update_payload)

    if not case_update_payload:
        raise CortexMissingArgError(
            [
                "case_name",
                "description",
                "assignee",
                "status",
                "notes",
                "starred",
                "user_defined_severity",
                "resolve_reason",
                "resolved_comment",
                "resolve_all_alerts",
                "custom_fields",
            ],
            require_one=True,
            override_message=f"No valid update parameters provided.\n{error_messages}",
        )

    def is_bulk_update_allowed(case_update_payload: dict) -> bool:
        # Bulk update supports only those fields
        allowed_bulk_fields = {"userSeverity", "status", "starred", "assignedUser"}

        for field_name, field_value in case_update_payload.items():
            if (
                field_name == "status"
                and field_value == CaseManagement.STATUS["resolved"]
                or field_name not in allowed_bulk_fields
            ):
                return False
        return True

    def repackage_to_update_case_format(case_list):
        """
        Maps raw API case data to the Update Case Format,
        """
        if not case_list or not isinstance(case_list, list):
            return []

        reverse_tags = {v: k for k, v in CaseManagement.TAGS.items()}
        grouping_status_map = {"enabled": "GROUPING_STATUS_010_ENABLED", "disabled": "GROUPING_STATUS_020_DISABLED"}

        target = []
        for raw_case in case_list:
            raw_status = str(raw_case.get("STATUS", raw_case.get("STATUS_PROGRESS", ""))).split("_")[-1].lower()
            status_key = raw_status.replace("investigation", "under_investigation")
            raw_severity = str(raw_case.get("SEVERITY", "")).split("_")[-1].lower()
            raw_grouping = str(raw_case.get("CASE_GROUPING_STATUS", "")).split("_")[-1].lower()

            target.append(
                {
                    "id": str(raw_case.get("CASE_ID")),
                    "name": {"isUser": True, "value": raw_case.get("NAME")},
                    "score": {
                        "manual_score": raw_case.get("MANUAL_SCORE"),
                        "score": raw_case.get("SCORE"),
                        "score_source": raw_case.get("SCORE_SOURCE"),
                        "scoring_rules": raw_case.get("CALCULATED_SCORE"),
                        "scortex": raw_case.get("SCORTEX"),
                    },
                    "notes": None,
                    "description": {"isUser": True, "value": raw_case.get("DESCRIPTION")},
                    "caseDomain": raw_case.get("INCIDENT_DOMAIN"),
                    "creationTime": raw_case.get("CREATION_TIME"),
                    "lastUpdateTime": raw_case.get("LAST_UPDATE_TIME"),
                    "modifiedBy": None,
                    "starred": raw_case.get("CASE_STARRED"),
                    "status": {
                        "value": CaseManagement.STATUS.get(status_key),
                        "resolveComment": raw_case.get("RESOLVED_COMMENT"),
                        "resolve_reason": raw_case.get("RESOLVED_REASON"),
                    },
                    "severity": CaseManagement.SEVERITY.get(raw_severity),
                    "userSeverity": raw_case.get("USER_SEVERITY"),
                    "assigned": {"mail": raw_case.get("ASSIGNED_USER"), "pretty": raw_case.get("ASSIGNED_USER_PRETTY")},
                    "severityCounters": {
                        "SEV_020_LOW": raw_case.get("LOW_SEVERITY_ALERTS", 0),
                        "SEV_030_MEDIUM": raw_case.get("MEDIUM_SEVERITY_ALERTS", 0),
                        "SEV_040_HIGH": raw_case.get("HIGH_SEVERITY_ALERTS", 0),
                        "SEV_050_CRITICAL": raw_case.get("CRITICAL_SEVERITY_ALERTS", 0),
                    },
                    "topCounters": {
                        "HOSTS": len(raw_case.get("HOSTS", []) or []),
                        "MAL_ARTIFACTS": raw_case.get("WF_HITS", 0),
                        "USERS": len(raw_case.get("USERS", []) or []),
                    },
                    "tags": [
                        {"tag_id": reverse_tags.get(tag.get("tag_name")), "tag_name": tag.get("tag_name")}
                        for tag in (raw_case.get("CURRENT_TAGS", []) or [])
                    ],
                    "groupingStatus": {
                        "pretty": raw_grouping.capitalize(),
                        "raw": grouping_status_map.get(raw_grouping),
                        "reason": None,
                    },
                    "hasAttachment": raw_case.get("HAS_ATTACHMENT", False),
                    "internalStatus": raw_case.get("INTERNAL_STATUS", "STATUS_010_NONE"),
                }
            )

        return target

    demisto.info(f"Executing case update for cases {case_ids} with request data: {case_update_payload}")
    replies = []
    if is_bulk_update_allowed(case_update_payload):
        demisto.debug("Performing bulk case update")
        if case_update_payload.get("userSeverity"):
            case_update_payload["severity"] = case_update_payload.pop("userSeverity")
        if case_update_payload.get("assignedUser") == "unassigned":
            case_update_payload["assignedUser"] = None

        client.bulk_update_case(case_update_payload, case_ids)
        filter_builder = FilterBuilder()
        filter_builder.add_field(
            CaseManagement.FIELDS["case_id_list"],
            FilterType.EQ,
            case_ids,
        )
        request_data = build_webapp_request_data(
            table_name=CASES_TABLE,
            filter_dict=filter_builder.to_dict(),
            limit=len(case_ids),
            sort_field="CREATION_TIME",
        )
        demisto.debug(f"request_data to retrieve cases that were updated via bulk: {request_data}")
        response = client.get_webapp_data(request_data)
        reply = response.get("reply", {})
        data = reply.get("DATA", [])
        replies = repackage_to_update_case_format(data)

    else:
        demisto.debug("Performing iterative case update")
        if assignee == "unassigned":
            for case_id in case_ids:
                client.unassign_case(case_id)
        responses = [client.update_case(case_update_payload, case_id) for case_id in case_ids]
        replies = []
        for resp in responses:
            replies.append(process_case_response(resp))

    command_results = CommandResults(
        readable_output=tableToMarkdown("Cases", replies, headerTransform=string_to_table_header),
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.Case",
        outputs_key_field="case_id",
        outputs=replies,
        raw_response=replies,
    )

    if error_messages:
        return_results(command_results)
        # The fields failed validation (unknown field, invalid value/type, system field),
        # so surface a standardized INVALID_ARGUMENT error_code while keeping the
        # original human-readable message for backward compatibility.
        error = CortexInvalidArgError(
            "custom_fields",
            reason=error_messages,
            override_message=f"The following fields could not be updated:\n{error_messages}",
        )
        return_error(error.build_message(), error=error)

    return command_results


def validate_custom_fields(fields_to_validate: dict, client: Client) -> tuple[dict, str]:
    """
    Validates custom fields against system metadata.

    Users must pass the CLI/machine name (e.g., ``servicenowticketid``) which
    is the identifier shown in Object Setup and used in XQL queries.  The
    metadata API returns this value in ``CUSTOM_FIELD_CLI_NAME``.

    Args:
        fields_to_validate: Dict of field CLI names and values to validate.
        client: Client instance for API calls.

    Returns:
        Tuple of (valid_fields_dict, error_messages_str).
    """
    if not fields_to_validate:
        return {}, ""

    fields_data = client.get_custom_fields_metadata().get("reply", {}).get("DATA", [])

    if not fields_data:
        return {}, "No Fields are defined in the system."

    system_fields = {
        f["CUSTOM_FIELD_CLI_NAME"]: f.get("CUSTOM_FIELD_PRETTY_NAME", f["CUSTOM_FIELD_CLI_NAME"])
        for f in fields_data
        if f.get("CUSTOM_FIELD_CLI_NAME") and f.get("CUSTOM_FIELD_IS_SYSTEM")
    }
    custom_fields = {
        f["CUSTOM_FIELD_CLI_NAME"]: {
            "pretty_name": f.get("CUSTOM_FIELD_PRETTY_NAME", f["CUSTOM_FIELD_CLI_NAME"]),
            "field_type": f.get("CUSTOM_FIELD_TYPE", ""),
            "select_values": (f.get("CUSTOM_FIELD_FIELD_DATA") or {}).get("selectValues") or [],
        }
        for f in fields_data
        if f.get("CUSTOM_FIELD_CLI_NAME") and not f.get("CUSTOM_FIELD_IS_SYSTEM")
    }

    if not custom_fields:
        return {}, "No custom fields are defined in the system."

    demisto.debug(f"Available custom fields: {custom_fields=}")
    valid_fields, error_messages = {}, []
    for field_name, field_value in fields_to_validate.items():
        if field_name in system_fields:
            error_messages.append(
                f"Field '{field_name}' ({system_fields[field_name]}) is a system field and cannot"
                f" be set with custom_fields argument."
            )
        elif field_name in custom_fields:
            field_type = custom_fields[field_name]["field_type"]
            select_values = custom_fields[field_name]["select_values"]

            if field_type == "multiSelect":
                # Auto-coerce plain string → single-element list for multiSelect fields
                if not isinstance(field_value, list):
                    demisto.debug(
                        f"Field '{field_name}' is of type multiSelect but received a non-list value {field_value!r}. "
                        f"Auto-converting to list: [{field_value!r}]"
                    )
                    field_value = [field_value]
                if select_values:
                    invalid_values = [v for v in field_value if v not in select_values]
                    if invalid_values:
                        error_messages.append(
                            f"Field '{field_name}' contains invalid value(s): {invalid_values}."
                            f" Allowed values are: {select_values}"
                        )
                        continue
                valid_fields[field_name] = field_value
            elif field_type == "shortText" and isinstance(field_value, list):
                error_messages.append(
                    f"Field '{field_name}' is of type shortText and does not accept a list value."
                    f" Provide a single string value instead."
                )
            else:
                valid_fields[field_name] = field_value
        else:
            error_messages.append(f"Field '{field_name}' does not exist. Use the CLI/machine name as shown in Object Setup.")

    return valid_fields, "\n".join(f"- {e}" for e in error_messages)


def resolve_playbook_id(client: Client, playbook: str) -> str:
    """
    Resolves a playbook name or ID to a playbook ID.

    Fetches all playbooks metadata and builds a name→id mapping.
    If the provided value matches a known playbook name, the corresponding ID is returned.
    If no name match is found, the value is checked against known IDs.
    If it matches a known ID, it is returned as-is.
    If it matches neither a name nor a known ID, a DemistoException is raised.

    Args:
        client (Client): The client instance for making API requests.
        playbook (str): A playbook name or playbook ID.

    Returns:
        str: The resolved playbook ID.

    Raises:
        DemistoException: If the value does not match any known playbook name or ID.
    """
    pbs_metadata: list = client.get_playbooks_metadata() or []
    name_to_id: dict[str, str] = {}
    known_ids: set[str] = set()
    for pb in pbs_metadata:
        pb_name = pb.get("name", "")
        pb_id = pb.get("id", "")
        known_ids.add(pb_id)
        name_to_id[pb_name] = pb_id

    if playbook in known_ids:
        demisto.debug(f"Playbook '{playbook}' matched a known ID directly.")
        return playbook

    if playbook in name_to_id:
        resolved_id = name_to_id[playbook]
        demisto.debug(f"Resolved playbook name '{playbook}' to ID '{resolved_id}'.")
        return resolved_id

    raise DemistoException(
        f"Playbook '{playbook}' was not found. "
        f"Verify that the playbook name or ID is correct and that the playbook exists in the system."
    )


def run_playbook_command(client: Client, args: dict) -> CommandResults:
    """
    Executes a playbook command with specified arguments.

    Reads the ``playbook`` argument, which can be either a playbook name or a playbook ID,
    and resolves it to a playbook ID via :func:`resolve_playbook_id`.

    All outcomes — success, resolution errors (unknown name/ID), and API-level per-issue
    failures — are reported via the ``result`` output field rather than raising exceptions.
    The ``result`` field contains the full status message string in all cases.

    Args:
        client (Client): The client instance for making API requests.
        args (dict): Arguments for running the playbook. Supported keys:
            - ``playbook`` (str): Playbook name or ID to execute.
            - ``issue_ids`` (str | list): Issue IDs to run the playbook against.

    Returns:
        CommandResults: Results of the playbook execution with the following output fields:
            - ``playbook``: The playbook name or ID as provided by the caller.
            - ``result``: A status message string. On success:
              ``"Playbook '<name>' executed successfully for all issue IDs: <ids>"``.
              On resolution failure or per-issue API errors: a descriptive error string.
    """
    playbook_input = args.get("playbook", "")
    issue_ids = argToList(args.get("issue_ids", ""))

    try:
        playbook_id = resolve_playbook_id(client, playbook_input)
    except DemistoException as e:
        demisto.debug(f"Playbook resolution error: {str(e)}")
        return CommandResults(
            outputs_prefix="Core.RunPlaybook",
            outputs_key_field="playbook",
            outputs={"playbook": playbook_input, "result": str(e)},
        )

    response = client.run_playbook(issue_ids, playbook_id)

    if response:
        pb_failed_on_issue = [f"Issue ID {issue_id}: {msg.replace('alert', 'issue')}" for issue_id, msg in response.items()]
        result = f"Playbook '{playbook_input}' failed for following issues:\n" + "\n".join(pb_failed_on_issue)
        demisto.debug(f"Playbook run errors: {pb_failed_on_issue}")
    else:
        result = f"Playbook '{playbook_input}' executed successfully for all issue IDs: {', '.join(issue_ids)}"

    return CommandResults(
        outputs_prefix="Core.RunPlaybook",
        outputs_key_field="playbook",
        outputs={"playbook": playbook_input, "result": result},
    )


def list_scripts_command(client: Client, args: dict) -> List[CommandResults]:
    """
    Retrieves a list of scripts from the platform with optional filtering.
    """
    page_number = arg_to_number(args.get("page_number")) or 0
    page_size = arg_to_number(args.get("page_size")) or MAX_SCRIPTS_LIMIT
    start_index = page_number * page_size
    end_index = start_index + page_size

    filter_builder = FilterBuilder()
    filter_builder.add_field(
        ScriptManagement.FIELDS["script_name"],
        FilterType.CONTAINS,
        argToList(args.get("script_name")),
    )

    platforms = [ScriptManagement.PLATFORMS[platform] for platform in argToList(args.get("supported_platforms"))]
    filter_builder.add_field(ScriptManagement.FIELDS["supported_platforms"], FilterType.CONTAINS, platforms)

    request_data = build_webapp_request_data(
        table_name=SCRIPTS_TABLE,
        filter_dict=filter_builder.to_dict(),
        limit=end_index,
        sort_field="MODIFICATION_TIME",
        start_page=start_index,
    )

    response = client.get_webapp_data(request_data)
    reply = response.get("reply", {})
    data = reply.get("DATA", [])

    mapped_scripts = []
    for script in data:
        mapped_script = {
            "name": script.get("NAME"),
            "description": script.get("DESCRIPTION"),
            "windows_supported": "AGENT_OS_WINDOWS" in str(script.get("PLATFORM", "")),
            "linux_supported": "AGENT_OS_LINUX" in str(script.get("PLATFORM", "")),
            "macos_supported": "AGENT_OS_MAC" in str(script.get("PLATFORM", "")),
            "script_uid": script.get("GUID"),
            "script_id": script.get("ID"),
            "script_inputs": script.get("ENTRY_POINT_DEFINITION", {}).get("input_params", []),
        }
        mapped_scripts.append(mapped_script)

    metadata = {
        "filtered_count": reply.get("FILTER_COUNT", 0),
        "returned_count": len(mapped_scripts),
    }

    command_results = []
    command_results.append(
        CommandResults(
            readable_output=tableToMarkdown("Scripts", mapped_scripts, headerTransform=string_to_table_header),
            outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.Scripts",
            outputs=mapped_scripts,
            outputs_key_field="script_id",
            raw_response=response,
        )
    )
    command_results.append(
        CommandResults(
            outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.ScriptsMetadata",
            outputs=metadata,
        )
    )

    return command_results


def run_script_agentix_command(client: Client, args: dict) -> PollResult:
    """
    Executes a script on agents with specified parameters.

    Args:
        client (Client): The client instance for making API requests.
        args (dict): Arguments for running the script.

    Returns:
        CommandResults: Results of the script execution.
    """
    script_uid = args.get("script_uid", "")
    script_name = args.get("script_name", "")
    endpoint_ids = argToList(args.get("endpoint_ids", ""))
    endpoint_names = argToList(args.get("endpoint_names", ""))
    parameters = args.get("parameters", "")
    if script_uid and script_name:
        raise ValueError("Please provide either script_uid or script_name, not both.")

    if not script_uid and not script_name:
        raise ValueError("You must specify either script_uid or script_name.")

    if endpoint_ids and endpoint_names:
        raise ValueError("Please provide either endpoint_ids or endpoint_names, not both.")

    if not endpoint_ids and not endpoint_names:
        raise ValueError("You must specify either endpoint_ids or endpoint_names.")

    if script_name:
        scripts_results = list_scripts_command(client, {"script_name": script_name})
        scripts: list = scripts_results[0].outputs or []  # type: ignore
        number_of_returned_scripts = len(scripts)
        demisto.debug(f"Scripts results: {scripts}")
        if number_of_returned_scripts > 1:
            error_message = (
                "Multiple scripts found. Please specify the exact script by providing one of the following script_uid:\n\n"
            )
            for script in scripts:
                error_message += (
                    f"Script UID: {script['script_uid']}\n"
                    f"Description: {script['description']}\n"
                    f"Name: {script['name']}\n"
                    f"Supported Platforms: Windows: {script['windows_supported']}, "
                    f"Linux: {script['linux_supported']}, "
                    f"MacOS: {script['macos_supported']}\n"
                    f"Script Inputs: {script['script_inputs']}\n\n"
                )
            raise ValueError(error_message)

        # If exactly one script is found, use its script_uid
        elif number_of_returned_scripts == 1:
            script = scripts[0]
            script_uid = script["script_uid"]
            script_inputs = script["script_inputs"]
            script_inputs_names = [input_param.get("name") for input_param in script_inputs]
            if script["script_inputs"] and not parameters:
                raise ValueError(
                    f"Script '{script_name}' requires the following input parameters: {', '.join(script_inputs_names)}, "
                    "but none were provided."
                )

        # If no scripts found, raise an error
        else:
            raise ValueError(f"No scripts found with the name: {script_name}")

    if endpoint_names:
        endpoint_results = core_list_endpoints_command(client, {"endpoint_name": endpoint_names})
        endpoints = endpoint_results.outputs or []
        demisto.debug(f"Endpoint results: {endpoints}")
        endpoint_ids = [endpoint["endpoint_id"] for endpoint in endpoints]  # type: ignore

    if not endpoint_ids:
        raise ValueError(f"No endpoints found with the specified names: {', '.join(endpoint_names)}")

    client._base_url = "/api/webapp/public_api/v1"
    return script_run_polling_command(
        {"endpoint_ids": endpoint_ids, "script_uid": script_uid, "parameters": parameters, "is_core": True}, client
    )


def map_endpoint_format(endpoint_list: list) -> list:
    """
    Maps and prepares endpoints data for consistent output formatting.

    Args:
        endpoint_list (list): Raw endpoint list from client response.

    Returns:
        dict: Formatted endpoint results with markdown table and outputs.
    """
    map_output_endpoint_fields = {v: k for k, v in Endpoints.ENDPOINT_FIELDS.items()}

    map_output_endpoint_type = {v: k for k, v in Endpoints.ENDPOINT_TYPE.items()}

    map_output_endpoint_status = {v: k for k, v in Endpoints.ENDPOINT_STATUS.items()}

    map_output_endpoint_platform = {v: k for k, v in Endpoints.ENDPOINT_PLATFORM.items()}

    map_output_endpoint_operational_status = {v: k for k, v in Endpoints.ENDPOINT_OPERATIONAL_STATUS.items()}

    map_output_assigned_prevention_policy = {v: k for k, v in Endpoints.ASSIGNED_PREVENTION_POLICY.items()}

    # A dispatcher for easy lookup:
    nested_mappers = {
        "endpoint_type": map_output_endpoint_type,
        "endpoint_status": map_output_endpoint_status,
        "platform": map_output_endpoint_platform,
        "operational_status": map_output_endpoint_operational_status,
        "assigned_prevention_policy": map_output_assigned_prevention_policy,
    }
    mapped_list = []

    for outputs in endpoint_list:
        mapped_item = {}

        for raw_key, raw_value in outputs.items():
            # Step 1: map backend key → prettified_output_key
            if raw_key not in map_output_endpoint_fields:
                continue

            prettified_output_key = map_output_endpoint_fields[raw_key]

            # Step 2: map nested values (policy ID, status, etc.)
            if prettified_output_key in nested_mappers:
                mapper = nested_mappers[prettified_output_key]
                friendly_value = mapper.get(raw_value, raw_value)
            else:
                friendly_value = raw_value

            mapped_item[prettified_output_key] = friendly_value

        mapped_list.append(mapped_item)

    return mapped_list


def build_endpoint_filters(args: dict):
    """
    Build a FilterBuilder for endpoint queries from provided arguments.

    Args:
        args (dict): Command arguments.

    Returns:
        FilterBuilder: Object with filters applied.
    """
    operational_status = [
        Endpoints.ENDPOINT_OPERATIONAL_STATUS[operational_status]
        for operational_status in argToList(args.get("operational_status"))
    ]
    endpoint_type = [Endpoints.ENDPOINT_TYPE[endpoint_type] for endpoint_type in argToList(args.get("endpoint_type"))]
    endpoint_status = [Endpoints.ENDPOINT_STATUS[status] for status in argToList(args.get("endpoint_status"))]
    platform = [Endpoints.ENDPOINT_PLATFORM[platform] for platform in argToList(args.get("platform"))]
    assigned_prevention_policy = [
        Endpoints.ASSIGNED_PREVENTION_POLICY[assigned] for assigned in argToList(args.get("assigned_prevention_policy"))
    ]
    agent_eol = args.get("agent_eol")
    supported_version = arg_to_bool_or_none(agent_eol) if agent_eol else None

    filter_builder = FilterBuilder()
    filter_builder.add_field(Endpoints.ENDPOINT_FIELDS["endpoint_status"], FilterType.EQ, endpoint_status)
    filter_builder.add_field(Endpoints.ENDPOINT_FIELDS["operational_status"], FilterType.EQ, operational_status)
    filter_builder.add_field(Endpoints.ENDPOINT_FIELDS["endpoint_type"], FilterType.EQ, endpoint_type)
    filter_builder.add_field(Endpoints.ENDPOINT_FIELDS["platform"], FilterType.EQ, platform)
    filter_builder.add_field(Endpoints.ENDPOINT_FIELDS["assigned_prevention_policy"], FilterType.EQ, assigned_prevention_policy)
    filter_builder.add_field(Endpoints.ENDPOINT_FIELDS["endpoint_name"], FilterType.EQ, argToList(args.get("endpoint_name")))
    filter_builder.add_field(
        Endpoints.ENDPOINT_FIELDS["operating_system"], FilterType.CONTAINS, argToList(args.get("operating_system"))
    )
    filter_builder.add_field(Endpoints.ENDPOINT_FIELDS["agent_version"], FilterType.EQ, argToList(args.get("agent_version")))
    filter_builder.add_field(Endpoints.ENDPOINT_FIELDS["os_version"], FilterType.EQ, argToList(args.get("os_version")))
    filter_builder.add_field(
        Endpoints.ENDPOINT_FIELDS["ip_address"], FilterType.ADVANCED_IP_MATCH_EXACT, argToList(args.get("ip_address"))
    )
    filter_builder.add_field(Endpoints.ENDPOINT_FIELDS["domain"], FilterType.EQ, argToList(args.get("domain")))
    filter_builder.add_field(Endpoints.ENDPOINT_FIELDS["tags"], FilterType.EQ, argToList(args.get("tags")))
    filter_builder.add_field(Endpoints.ENDPOINT_FIELDS["endpoint_id"], FilterType.EQ, argToList(args.get("endpoint_id")))
    filter_builder.add_field(Endpoints.ENDPOINT_FIELDS["cloud_provider"], FilterType.EQ, argToList(args.get("cloud_provider")))
    filter_builder.add_field(Endpoints.ENDPOINT_FIELDS["cloud_region"], FilterType.EQ, argToList(args.get("cloud_region")))
    filter_builder.add_field(Endpoints.ENDPOINT_FIELDS["agent_eol"], FilterType.EQ, supported_version)
    filter_dict = filter_builder.to_dict()

    return filter_dict


def core_list_endpoints_command(client: Client, args: dict) -> CommandResults:
    """
    Retrieves a list of endpoints from the server, applies filters, maps the data, and returns
    it as CommandResults for Cortex XSOAR.

    Args:
        client (Client): The integration client used to fetch data.
        args (dict): Command arguments.

    Returns:
        CommandResults: Contains the formatted table, raw response, and outputs.
    """
    page = arg_to_number(args.get("page")) or 0
    limit = arg_to_number(args.get("page_size")) or MAX_GET_ENDPOINTS_LIMIT
    limit = min(limit, MAX_GET_ENDPOINTS_LIMIT)
    page_from = page * limit
    page_to = page * limit + limit
    filter_dict = build_endpoint_filters(args)

    request_data = build_webapp_request_data(
        table_name=AGENTS_TABLE,
        filter_dict=filter_dict,
        limit=page_to,
        sort_field="AGENT_NAME",
        sort_order="ASC",
        start_page=page_from,
    )
    demisto.info(f"{request_data=}")
    response = client.get_webapp_data(request_data)
    reply = response.get("reply", {})
    data = reply.get("DATA", [])
    data = map_endpoint_format(data)
    demisto.debug(f"Endpoint data after mapping and formatting: {data}")

    return CommandResults(
        readable_output=tableToMarkdown("Endpoints", data, headerTransform=string_to_table_header),
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.Endpoint",
        outputs_key_field="endpoint_id",
        outputs=data,
        raw_response=data,
    )


def get_issues_command(client: Client, args: dict) -> list[CommandResults]:
    response: list[CommandResults] = get_issues_by_filter_command(client, args)
    output_keys = argToList(args.pop("output_keys", []))
    if isinstance(response[0].outputs, list) and response[0].outputs:
        response[0].outputs = [alert_to_issue(output) for output in response[0].outputs]

        if output_keys:
            response[0].outputs = filter_context_fields(output_keys, response[0].outputs)

    return response


def parse_frequency(day: str | None, time: str | None) -> str:
    """
    Convert day and time to cron-style frequency string

    Cron format: Minute Hour Day-of-Month Month Day-of-Week
    Example: "0 12 * * 2" means:
    - Minute: 0 (The task starts at the 0th minute of the hour)
    - Hour: 12 (The task starts at the 12th hour - 12:00 PM in 24-hour time)
    - Day of Month: * (Every day of the month)
    - Month: * (Every month)
    - Day of Week: 2 (Tuesday)

    :param day: Day of month (optional)
    :param time: Time in HH:MM format
    :return: Cron-style frequency string
    """
    DAY_MAP = {"sunday": 0, "monday": 1, "tuesday": 2, "wednesday": 3, "thursday": 4, "friday": 5, "saturday": 6}

    target_time = time if time else "12:00"
    try:
        hours, minutes = map(int, target_time.split(":"))
        if not (0 <= hours < 24 and 0 <= minutes < 60):
            raise ValueError("Invalid time format. Use HH:MM in 24-hour format.")
    except ValueError:
        raise ValueError("Invalid time format. Use HH:MM.")

    if day is None:
        # If no day is provided -> Daily (represented by * in cron)
        cron_day = "*"
    else:
        # If day is provided -> Weekly (look up the day index)
        day_key = day.lower()
        if day_key not in DAY_MAP:
            raise ValueError(f"Invalid day. Must be one of {list(DAY_MAP.keys())}.")
        cron_day = str(DAY_MAP[day_key])

    return f"{minutes} {hours} * * {cron_day}"


def create_assessment_profile_payload(
    name: str,
    description: str,
    standard_id: str,
    asset_group_id: str,
    day: str | None,
    time: str | None,
    report_type: str = "ALL",
) -> Dict[str, Any]:
    """
    Prepare assessment profile payload

    :param name: Name of the assessment profile
    :param description: Description of the profile
    :param standard_id: ID of the compliance standard
    :param asset_group_id: ID of the asset group
    :param day: Day of evaluation (optional)
    :param time: Time of evaluation (optional)
    :param report_type: Type of report (default: ALL)
    :return: Assessment profile payload
    """

    report_frequency = parse_frequency(day, time)

    payload = {
        "request_data": {
            "profile_name": name,
            "asset_group_id": asset_group_id,
            "standard_id": standard_id,
            "description": description,
            "report_targets": [],
            "report_type": report_type,
            "evaluation_frequency": report_frequency,
        }
    }

    return payload


def list_compliance_standards_payload(
    name: str | None = None,
    created_by: str | None = None,
    labels: list[str] | None = None,
    page=0,
    page_size=MAX_COMPLIANCE_STANDARDS,
) -> Dict[str, Any]:
    """
    Prepare assessment profile payload

    :param name: Name of the assessment profile
    :param description: Description of the profile
    :param standard_id: ID of the compliance standard
    :param asset_group_id: ID of the asset group
    :param day: Day of evaluation (optional)
    :param time: Time of evaluation (optional)
    :param report_type: Type of report (default: ALL)
    :return: Assessment profile payload
    """

    start_index = page * page_size
    end_index = start_index + page_size
    payload: dict = {"request_data": {"filters": []}}

    if name:
        payload["request_data"]["filters"].append({"field": "name", "operator": "contains", "value": name})

    if created_by:
        payload["request_data"]["filters"].append(
            {"field": "IS_CUSTOM", "operator": "in", "value": ["yes" if created_by == "Custom" else "no"]}
        )

    if labels:
        for label in labels:
            payload["request_data"]["filters"].append({"field": "labels", "operator": "contains", "value": label})

    payload["request_data"]["sort"] = {"field": "insertion_time", "keyword": "desc"}

    payload["request_data"]["pagination"] = {
        "search_from": start_index,
        "search_to": end_index,
    }

    return payload


def core_add_assessment_profile_command(client: Client, args: dict) -> CommandResults:
    """
    Adds a new assessment profile to the Cortex Platform.

    Args:
        client (Client): The integration client used to add the assessment profile.
        args (dict): Command arguments containing profile details.

    Returns:
        CommandResults: Contains the result of adding the assessment profile.
    """
    profile_name = args.get("profile_name", "")
    profile_description = args.get("profile_description", "")
    standard_name = args.get("standard_name", "")
    asset_group_name = args.get("asset_group_name", "")
    day = args.get("day")
    time = args.get("time", "12:00")

    payload = list_compliance_standards_payload(
        name=standard_name,
    )
    demisto.debug(f"Listing compliance standards with payload: {payload}")
    response = client.list_compliance_standards_command(payload)
    reply = response.get("reply", {})
    standards = reply.get("standards")
    demisto.debug(f"{standards=}")

    if not standards:
        return_error("No compliance standards found matching the provided name.")

    if len(standards) > 1:
        standard_names = [standard.get("name") for standard in standards]
        new_line = "\n"
        return_error(
            f"The name you provided matches more than one standard:\n\n{new_line.join(standard_names)}\n\n"
            "Please provide a more specific name."
        )

    standard_id = standards[0].get("id")

    filter = FilterBuilder()
    filter.add_field("XDM.ASSET_GROUP.NAME", FilterType.CONTAINS, asset_group_name)
    filter_str = filter.to_dict()
    groups = client.search_asset_groups(filter_str).get("reply", {}).get("data", [])
    group_ids = [group.get("XDM.ASSET_GROUP.ID") for group in groups if group.get("XDM.ASSET_GROUP.ID")]
    group_names = [group.get("XDM.ASSET_GROUP.NAME") for group in groups if group.get("XDM.ASSET_GROUP.NAME")]

    if not group_ids:
        return_error("No asset group found matching the provided name.")

    if len(group_ids) > 1:
        new_line = "\n"
        return_error(
            f"The name you provided matches more than one asset group:\n\n{new_line.join(group_names)}\n\n"
            "Please provide a more specific name."
        )
    demisto.debug(f"{group_ids=}")
    asset_group_id = group_ids[0]

    payload = create_assessment_profile_payload(
        name=profile_name,
        description=profile_description,
        standard_id=str(standard_id),
        asset_group_id=asset_group_id,
        day=day,
        time=time,
        report_type="ALL",
    )
    demisto.debug(f"Creating assessment profile with payload: {payload}")

    reply = client.add_assessment_profile(payload)
    assessment_profile_id = reply.get("assessment_profile_id")
    return CommandResults(
        readable_output=f"Assessment Profile {assessment_profile_id} successfully added",
        outputs_prefix="Core.AssessmentProfile",
        outputs_key_field="assessment_profile_id",
        outputs=assessment_profile_id,
        raw_response=reply,
    )


def core_list_compliance_standards_command(client: Client, args: dict) -> list[CommandResults]:
    """
    Lists compliance standards with optional filtering.

    Args:
        client (Client): The client instance for API communication.
        args (dict): Command arguments containing optional filters:
            - name (str): Filter by standard name
            - created_by (str): Filter by creator
            - labels (list): Filter by labels (converts "Alibaba Cloud" to "alibaba_cloud" and "On Prem" to "on_prem")
            - page (int): Page number for pagination (default: 0)
            - page_size (int): Number of results per page (default: MAX_COMPLIANCE_STANDARDS)

    Returns:
        list[CommandResults]: List containing:
            - CommandResults with filtered compliance standards data and metadata
            - CommandResults with pagination metadata (filtered_count, returned_count)
    """
    name = args.get("name", "")
    created_by = args.get("created_by", "")
    labels = argToList(args.get("labels", ""))
    labels = ["alibaba_cloud" if label == "Alibaba Cloud" else "on_prem" if label == "On Prem" else label for label in labels]
    page = arg_to_number(args.get("page", "0"))
    page_size = arg_to_number(args.get("page_size", MAX_COMPLIANCE_STANDARDS))

    payload = list_compliance_standards_payload(
        name=name,
        created_by=created_by,
        labels=labels,
        page=page,
        page_size=page_size,
    )

    response = client.list_compliance_standards_command(payload)
    reply = response.get("reply", {})
    standards = reply.get("standards")
    demisto.debug(f"{standards=}")
    filtered_count = reply.get("result_count")
    returned_count = len(standards)

    filtered_standards = [
        {
            "id": s.get("id"),
            "name": s.get("name"),
            "description": s.get("description"),
            "controls_count": len(s.get("controls_ids", [])),
            "assessments_profiles_count": s.get("assessments_profiles_count", 0),
            "labels": s.get("labels", []),
        }
        for s in standards
    ]

    demisto.debug(f"{filtered_standards=}")
    command_results = []
    command_results.append(
        CommandResults(
            readable_output=tableToMarkdown("Compliance Standards", filtered_standards),
            outputs_prefix="Core.ComplianceStandards",
            outputs_key_field="id",
            outputs=filtered_standards,
            raw_response=reply,
        )
    )
    command_results.append(
        CommandResults(
            outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.ComplianceStandardsMetadata",
            outputs={"filtered_count": filtered_count, "returned_count": returned_count},
        )
    )

    return command_results


def validate_start_end_times(start_time, end_time):
    """
    Validate that start_time and end_time are provided correctly and represent
    a time range of at least two hours.

    Args:
        start_time (str | None): Start time in "HH:MM" format.
        end_time (str | None): End time in "HH:MM" format.

    Raises:
        DemistoException: If only one of the times is provided or the time range is less than two hours.
    """
    if (start_time and not end_time) or (end_time and not start_time):
        raise DemistoException("Both start_time and end_time must be provided together.")

    if start_time and end_time:
        start_dt = datetime.strptime(start_time, "%H:%M")
        end_dt = datetime.strptime(end_time, "%H:%M")
        diff = (end_dt - start_dt).total_seconds()
        if diff < 0:
            diff += SECONDS_IN_DAY

        if diff < MIN_DIFF_SECONDS:
            raise DemistoException("Start and end times must be at least two hours apart (midnight crossing is supported).")


def transform_distributions(response):
    """
    Takes the full API response and replaces `distributions` dict
    with a single flattened list while keeping everything else the same.
    """
    flattened = []
    distributions = response.get("distributions", {})
    total_count = arg_to_number(response.get("total_count"))

    for platform, items in distributions.items():
        for item in items:
            unsupported_os = arg_to_number(item.get("unsupported_os"))
            if item.get("is_beta") or item.get("less") == 0 or (unsupported_os != 0 and unsupported_os == total_count):
                continue

            new_item = remove_empty_elements(
                {
                    "platform": platform,
                    "endpoints_with_lower_version_count": item.get("less"),
                    "endpoints_with_higher_version_count": item.get("greater"),
                    "endpoints_with_same_version_count": item.get("equal"),
                    "version": item.get("version"),
                }
            )
            flattened.append(new_item)

    new_response = {"platform_count": response.get("platform_count"), "total_count": total_count, "distributions": flattened}
    return new_response


def get_endpoint_update_version_command(client, args):
    """
    Get the endpoint update version for specified endpoints.

    Args:
        client (Client): Integration client.
        args (dict): Command arguments containing endpoint list.

    Returns:
        CommandResults: Formatted results of endpoint update versions.
    """
    filter_builder = FilterBuilder()
    endpoint_ids = argToList(args.get("endpoint_ids", ""))
    filter_builder.add_field("AGENT_ID", FilterType.EQ, endpoint_ids)
    filter_data = {
        "filter": filter_builder.to_dict(),
    }
    request_data = {"filter_data": filter_data, "filter_type": "static"}
    demisto.debug(f"{request_data=}")
    response = client.get_endpoint_update_version(request_data)
    flattened_response = transform_distributions(response)
    return CommandResults(
        readable_output=tableToMarkdown(
            "Endpoint Update Versions", flattened_response.get("distributions"), headerTransform=string_to_table_header
        ),
        outputs=flattened_response,
        outputs_prefix="Core.EndpointUpdateVersion",
    )


def update_endpoint_version_command(client, args):
    """
    Update the agent version on one or more endpoints, optionally scheduling
    the update by days and time window.

    Args:
        client: API client used to communicate with the backend service.
        args (dict): Command arguments provided by the user.

    Returns:
        CommandResults: Object containing a human-readable summary and outputs
        with the endpoint IDs and the resulting group action ID (if created).
    """
    filter_builder = FilterBuilder()
    endpoint_ids = argToList(args.get("endpoint_ids", ""))
    filter_builder.add_field("AGENT_ID", FilterType.EQ, endpoint_ids)
    versions = {args.get("platform"): args.get("version")}
    days_arg = argToList(args.get("days", ""))

    if days_arg:
        days = [DAYS_MAPPING.get(day.lower()) for day in days_arg]
        if any(d is None for d in days):
            raise DemistoException("Please provide valid days.")
    else:
        days = None

    start_time = args.get("start_time")
    end_time = args.get("end_time")
    validate_start_end_times(start_time, end_time)

    filter_data = {
        "filter": filter_builder.to_dict(),
    }
    request_data = {
        "filter_data": filter_data,
        "filter_type": "static",
        "versions": versions,
        "upgrade_to_pkg_manager": False,
        "schedule_data": {"START_TIME": start_time, "END_TIME": end_time, "DAYS": days},
    }
    demisto.debug(f"Request data of the command core-update-endpoint-version: {request_data}")
    response = client.update_endpoint_version(request_data)
    demisto.debug(f"Response of the command core-update-endpoint-version: {response}")
    group_action_id = response.get("reply", {}).get("group_action_id")
    if not group_action_id:
        summary = "The update to the target versions was unsuccessful."
    else:
        summary = f"The update to the target versions was successful. Action ID: {group_action_id}"

    return CommandResults(
        readable_output=summary,
        outputs={"endpoint_ids": endpoint_ids, "action_id": group_action_id},
        outputs_prefix="Core.EndpointUpdate",
    )


def build_column_mapping(column):
    """
    Extracts the mapping of module NAME (ugly name)
    to PRETTY_NAME from the column definitions metadata.
    """
    mapping = {}
    enum_values = column.get("FILTER_PARAMS", {}).get("ENUM_VALUES", [])
    for enum in enum_values:
        mapping[enum.get("NAME")] = enum.get("PRETTY_NAME")
    return mapping


def extract_mappings_from_view_def(view_def: dict, columns_to_map: set[str]):
    """
    Extracts the mapping of module listed in columns_to_map NAME (ugly name)
    to PRETTY_NAME from the column definitions metadata.
    """
    mapping = {}
    column_definitions = view_def.get("COLUMN_DEFINITIONS", [])
    for column in column_definitions:
        column_name = column.get("FIELD_NAME")
        if column_name in columns_to_map:
            mapping[column_name] = build_column_mapping(column)
    return mapping


def combine_pretty_names(list_of_criteria: list[list[dict[str, Any]]]) -> list[str]:
    """
    Takes a list of criteria (where each criterion is a list of dictionaries)
    and combines the 'pretty_name' values from the dictionaries in each criterion
    into a single string.

    Args:
        list_of_criteria: A list of lists, where the inner list contains
                          dictionaries with a 'pretty_name' key.

    Returns:
        A list of strings, where each string is the concatenation of the
        'pretty_name' values for one inner list.
    """
    result_strings = []
    for criterion in list_of_criteria:
        if isinstance(criterion, list):
            combined_string = "".join(item.get("pretty_name", "") for item in criterion)
            result_strings.append(combined_string)
        else:
            result_strings.append(criterion)
    return result_strings


def postprocess_exception_rules_response(view_def, data):
    view_def_data = view_def[0]
    mappings = extract_mappings_from_view_def(view_def_data, EXCEPTION_RULES_OUTPUT_FIELDS_TO_MAP)
    for record in data:
        for field in EXCEPTION_RULES_OUTPUT_FIELDS_TO_MAP:
            record[field] = [mappings.get(field, {}).get(val, val) for val in record.get(field, [])]
        record["ASSOCIATED_TARGETS"] = combine_pretty_names(record.get("ASSOCIATED_TARGETS", []))
        record["CONDITIONS"] = record.get("CONDITIONS_PRETTY")
        record["RULE_TYPE"] = record.get("SUBTYPE")
        record["CREATION_TIMESTAMP"] = record.get("CREATION_TIME")
        record["MODIFICATION_TIMESTAMP"] = record.get("MODIFICATION_TIME")
        record["MODIFICATION_TIME"] = timestamp_to_datestring(record["MODIFICATION_TIMESTAMP"])
        record["CREATION_TIME"] = timestamp_to_datestring(record["CREATION_TIMESTAMP"])
        record.pop("CONDITIONS_PRETTY", None)
        record.pop("SUBTYPE", None)

    readable_output = tableToMarkdown(
        view_def_data.get("TABLE_NAME"),
        data,
        headerTransform=string_to_table_header,
        sort_headers=False,
    )
    return readable_output


def get_webapp_data(
    client,
    table_name: str,
    filter_dict: Any,
    sort_field: str,
    sort_order: str,
    retrieve_all: bool,
    base_limit: int,
    max_limit: int,
    offset: int = 0,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], int]:
    """
    Helper function to iteratively fetch records for a single table with optional pagination.
    """
    all_records = []
    raw_responses = []

    limit = max_limit if retrieve_all else base_limit
    paging_from = offset
    paging_to = offset + limit
    filter_count = 0
    while True:
        demisto.debug(f"get_webapp_data pagination: {paging_from}, {paging_to}")
        request_data = build_webapp_request_data(
            table_name=table_name,
            filter_dict=filter_dict.to_dict(),
            sort_field=sort_field,
            sort_order=sort_order,
            limit=paging_to,
            start_page=paging_from,
        )
        response = client.get_webapp_data(request_data)
        raw_responses.append(copy.deepcopy(response))

        reply = response.get("reply", {})
        data = reply.get("DATA", [])
        filter_count = int(reply.get("FILTER_COUNT", "0"))
        all_records.extend(data)

        if not retrieve_all or len(data) < limit:
            break

        paging_from += limit
        paging_to += limit

    return all_records, raw_responses, filter_count


def list_exception_rules_command(client, args: dict[str, Any]) -> list[CommandResults]:
    """
    Retrieves Disable Prevention Rules and Legacy Agent Exceptions using the
    generic /api/webapp/get_data endpoint, handling pagination.
    """

    exception_rule_type = args.get("type")
    sort_field = args.get("sort_field", "MODIFICATION_TIME")
    sort_order = args.get("sort_order", "DESC")

    default_limit = arg_to_number(args.get("page_size")) or MAX_GET_EXCEPTION_RULES_LIMIT
    page_number = arg_to_number(args.get("page", 0)) or 0
    offset = page_number * default_limit if args.get("page") else 0
    retrieve_all = argToBoolean(args.get("retrieve_all", False))

    base_limit = MAX_GET_EXCEPTION_RULES_LIMIT if retrieve_all else default_limit

    exception_rule_filter = build_exception_rules_filter(args)

    if exception_rule_type in EXCEPTION_RULES_TYPE_TO_TABLE_MAPPING:
        table_names = [EXCEPTION_RULES_TYPE_TO_TABLE_MAPPING.get(exception_rule_type)]
    else:
        table_names = [LEGACY_AGENT_EXCEPTIONS_TABLE, DISABLE_PREVENTION_RULES_TABLE]

    all_outputs = []
    all_raw_responses = []
    readable_output_lines = []
    total_filter_count = 0

    for table_name in table_names:
        demisto.debug(f"Retrieving {table_name}")
        records, raw_responses, filter_count = get_webapp_data(
            client=client,
            table_name=str(table_name),
            filter_dict=exception_rule_filter,
            sort_field=sort_field,
            sort_order=sort_order,
            retrieve_all=retrieve_all,
            base_limit=base_limit,
            max_limit=MAX_GET_EXCEPTION_RULES_LIMIT,
            offset=offset,
        )

        all_raw_responses.extend(raw_responses)
        total_filter_count += filter_count

        demisto.debug(f"Retrieved {len(records)} records")
        if records:
            view_def = client.get_webapp_view_def({"table_name": table_name})
            hr_output = postprocess_exception_rules_response(view_def, records)
            all_outputs.extend(records)
            readable_output_lines.append(hr_output)
        else:
            readable_output_lines.append(f"No data found for {table_name} matching the filter.")

    final_readable_output = "\n".join(readable_output_lines)

    return [
        CommandResults(
            readable_output=final_readable_output,
            outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.ExceptionRules",
            outputs_key_field="ID",
            outputs=all_outputs,
            raw_response=all_raw_responses,
        ),
        CommandResults(
            outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.ExceptionRulesMetadata",
            outputs={"filter_count": total_filter_count, "returned_count": len(all_outputs)},
        ),
    ]


def list_system_users_command(client, args):
    """
    Retrieves system user optionally filtered by email using the public api ep /public_api/v1/rbac/get_users
    This function calls the client to fetch all available system users. If specific
    emails are provided via the 'email' argument, it filters the results. If no
    emails are provided, it limits the results to 50.
    """
    emails = argToList(args.get("email", ""))
    if len(emails) > MAX_GET_SYSTEM_USERS_LIMIT:
        raise DemistoException("The maximum number of emails allowed is 50.")

    response = client.get_users()
    data = response.get("reply", {})
    if emails:
        data = [user for user in data if user.get("user_email") in emails]

    if len(data) > MAX_GET_SYSTEM_USERS_LIMIT:
        data = data[:MAX_GET_SYSTEM_USERS_LIMIT]

    return CommandResults(
        readable_output=tableToMarkdown("System Users", data, headerTransform=string_to_table_header),
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.User",
        outputs_key_field="user_email",
        outputs=data,
        raw_response=response,
    )


def convert_timeframe_string_to_json(time_to_convert: str) -> Dict[str, int]:
    """Convert a timeframe string to a json required for XQL queries.

    Args:
        time_to_convert (str): The time frame string to convert (supports seconds, minutes, hours, days, months, years, between).

    Returns:
        dict: The timeframe parameters in JSON.
    """
    try:
        time_to_convert_lower = time_to_convert.strip().lower()
        if time_to_convert_lower.startswith("between "):
            tokens = time_to_convert_lower[len("between ") :].split(" and ")
            if len(tokens) == 2:
                time_from = dateparser.parse(tokens[0], settings={"TIMEZONE": "UTC"})
                time_to = dateparser.parse(tokens[1], settings={"TIMEZONE": "UTC"})
                if time_from is None or time_to is None:
                    raise DemistoException(
                        "Failed to parse timeframe argument, please use a valid format."
                        " (e.g. '1 day', '3 weeks ago', 'between 2021-01-01 12:34:56 +02:00 and 2021-02-01 12:34:56 +02:00')"
                    )
                return {"from": int(time_from.timestamp() * 1000), "to": int(time_to.timestamp() * 1000)}
        else:
            relative = dateparser.parse(time_to_convert, settings={"TIMEZONE": "UTC"})
            now_date = datetime.utcnow()
            if relative is None or now_date is None:
                raise DemistoException(
                    "Failed to parse timeframe argument, please use a valid format."
                    " (e.g. '1 day', '3 weeks ago', 'between 2021-01-01 12:34:56 +02:00 and 2021-02-01 12:34:56 +02:00')"
                )
            return {"relativeTime": int((now_date - relative).total_seconds() * 1000)}

        raise ValueError(f"Invalid timeframe: {time_to_convert}")
    except Exception as exc:
        raise DemistoException(
            f"Please enter a valid time frame (seconds, minutes, hours, days, weeks, months, years, between).\n{exc!s}"
        )


def get_xql_query_results_platform(client: Client, execution_id: str) -> dict:
    """Retrieve results of an executed XQL query using Platform API.

    Args:
        client (Client): The XDR Client.
        execution_id (str): The execution ID of the query to retrieve.

    Returns:
        dict: The query results including status, execution_id, and results if completed.
    """
    data: dict[str, Any] = {
        "query_id": execution_id,
    }

    # Call the Client function and get the raw response
    demisto.debug(f"Calling get_query_results with {data=}")
    response = client.platform_http_request(
        method="POST", json_data=data, url_suffix="/xql_queries/results/info/", ok_codes=[200]
    )

    response["execution_id"] = execution_id
    stream_id = response.get("stream_id")
    if response.get("status") != "PENDING" and stream_id:
        data = {
            "stream_id": stream_id,
        }
        demisto.debug(f"Requesting query results using {data=}")
        query_data = client.platform_http_request(
            method="POST", json_data=data, url_suffix="/xql_queries/results/", ok_codes=[200]
        )
        if isinstance(query_data, str):
            response["results"] = [json.loads(line) for line in query_data.split("\n") if line.strip()]
        else:
            response["results"] = query_data

    if response.get("status") == "FAIL":
        # Get full error details using PAPI
        data = {
            "request_data": {
                "query_id": execution_id,
                "pending_flag": True,
                "format": "json",
            }
        }
        res = client._http_request(method="POST", url_suffix="/xql/get_query_results", json_data=data)
        response["error_details"] = res.get("reply", "")

    return response


def get_xql_query_results_platform_polling(client: Client, execution_id: str, timeout: int) -> dict:
    """Retrieve results of an executed XQL query using Platform API with polling.

    Args:
        client (Client): The XDR Client.
        execution_id (str): The execution ID of the query to fetch.
        timeout (int): The polling timeout in seconds.

    Returns:
        dict: The query results after polling completes or timeout is reached.
    """
    interval_in_secs = 10

    # Block execution until the execution status isn't pending or we time out
    polling_start_time = datetime.now()
    while (datetime.now() - polling_start_time).total_seconds() < timeout:
        outputs = get_xql_query_results_platform(client, execution_id)
        if outputs.get("status") != "PENDING":
            break

        t_to_timeout = (datetime.now() - polling_start_time).total_seconds()
        demisto.debug(
            f"Got status 'PENDING' for {execution_id}, next poll in {interval_in_secs} seconds. Timeout in {t_to_timeout}"
        )
        time.sleep(interval_in_secs)  # pylint: disable=E9003

    return outputs


def handle_xql_limit(query: str, max_limit: int) -> str:
    """Ensure the given query does not exceed the max limit.
    Overrides the limit if it exceeds the maximum or if a limit clause isn't present.

    Args:
        query (str): The XQL query string to process.
        max_limit (int): The max limit value.

    Returns:
        str: The original query if it already contains a valid limit clause, or the query
            with a max limit clause appended or the limit value replaced if it exceeds max_limit.
    """
    if not query or not query.strip():
        return query

    # Pattern to match limit keyword with number, skipping over comments and quotes
    # The pattern uses alternation: first try to match things to skip (comments/quotes),
    # then try to match the actual limit clause. This ensures we don't match "limit"
    # inside comments or quoted strings.
    limit_pattern = re.compile(
        r"""
        (?P<skip>                           # Group for things to skip (not replace)
            /\*.*?\*/                       # Block comments
            |//[^\n]*                       # Line comments
            |"(?:[^"\\]|\\.)*"              # Double-quoted strings
            |'(?:[^'\\]|\\.)*'              # Single-quoted strings
        )
        |(?P<limit>limit\s+)(?P<num>\d+)    # Or match limit keyword with number
        """,
        re.IGNORECASE | re.DOTALL | re.VERBOSE,
    )

    limit_found = False

    def replace_limit(match):
        """Replace limit value if it exceeds max_limit, skip comments/quotes."""
        nonlocal limit_found
        # We matched a limit clause
        if match.group("limit"):
            limit_found = True
            current_limit = int(match.group("num"))
            if current_limit > max_limit:
                return f"{match.group('limit')}{max_limit}"

        return match.group(0)

    result = limit_pattern.sub(replace_limit, query)

    # Add a max limit clause if no limit was found anywhere in the query
    if not limit_found:
        result = f"{result}\n| limit {max_limit}"

    return result


def start_xql_query_platform(client: Client, query: str, timeframe: dict) -> str:
    """Execute an XQL query using Platform API.

    Args:
        client (Client): The XDR Client.
        query (str): The XQL query string to execute.
        timeframe (dict): The timeframe for the query.

    Returns:
        str: The query execution ID.
    """
    data: Dict[str, Any] = {
        "query": query,
        "timeframe": timeframe,
    }

    demisto.debug(f"Calling xql_queries/submit with {data=}")
    res = client.platform_http_request(url_suffix="/xql_queries/submit/", method="POST", json_data=data, ok_codes=[200])
    return str(res)


def xql_query_platform_command(client: Client, args: dict) -> CommandResults:
    """Execute an XQL query using Platform API and poll for results.

    Args:
        client (Client): The XDR Client.
        args (dict): Command arguments including query, timeframe, wait_for_results, and timeout_in_seconds.

    Returns:
        CommandResults: The command results with execution_id, query_url, and optionally status and results.
    """
    query = args.get("query", "")
    if not query:
        raise ValueError("query is not specified")

    MAX_QUERY_LIMIT = 1000
    query_with_limit = handle_xql_limit(query, MAX_QUERY_LIMIT)
    timeframe = convert_timeframe_string_to_json(args.get("timeframe", "24 hours") or "24 hours")

    execution_id = start_xql_query_platform(client, query_with_limit, timeframe)

    if not execution_id:
        raise DemistoException("Failed to start query\n")

    query_url = "/".join([demisto.demistoUrls().get("server", ""), "xql/xql-search", execution_id])
    outputs = {
        "execution_id": execution_id,
        "query_url": query_url,
    }
    if query != query_with_limit:
        outputs["query_limit_modified"] = (
            f"Limit clauses larger than {MAX_QUERY_LIMIT} are currently not supported and have been reduced to {MAX_QUERY_LIMIT}"
        )

    if argToBoolean(args.get("wait_for_results", True)):
        demisto.debug(f"Polling query execution with {execution_id=}")
        timeout_in_secs = int(args.get("timeout_in_seconds", 180))
        outputs.update(get_xql_query_results_platform_polling(client, execution_id, timeout_in_secs))

    return CommandResults(
        outputs_prefix="GenericXQLQuery", outputs_key_field="execution_id", outputs=outputs, raw_response=outputs
    )


def init_client(api_type: str) -> Client:
    """
    Initializes the Client for a specific API type.

    Args:
        api_type (str): The category of the API (e.g., 'public', 'webapp', 'data_platform', etc.)
    """
    params = demisto.params()

    # Connection parameters
    proxy = params.get("proxy", False)
    verify_cert = not params.get("insecure", False)

    try:
        timeout = int(params.get("timeout", 120))
    except (ValueError, TypeError):
        timeout = 120

    # Base URL Mapping logic based on api_type
    webapp_root = "/api/webapp"

    url_map = {
        "webapp": webapp_root,
        "public": f"{webapp_root}/public_api/v1",
        "data_platform": f"{webapp_root}/data-platform",
        "appsec": f"{webapp_root}/public_api/appsec",
        "xsoar": "/xsoar",
        "agents": f"{webapp_root}/agents",
    }

    # Fallback to public API if the type isn't recognized
    client_url = url_map.get(api_type, url_map["public"])

    headers: dict = {"Authorization": params.get("api_key"), "Content-Type": "application/json"}

    return Client(
        base_url=client_url,
        proxy=proxy,
        verify=verify_cert,
        headers=headers,
        timeout=timeout,
    )


def enhance_with_pb_details(pb_id_to_data: dict, playbook: dict):
    related_pb = pb_id_to_data.get(playbook.get("id"))
    if related_pb:
        playbook["name"] = related_pb.get("name")
        playbook["description"] = related_pb.get("comment")


def postprocess_case_resolution_statuses(client, response: dict):
    response = copy.deepcopy(response)
    pbs_metadata = client.get_playbooks_metadata() or []
    pb_id_to_data = map_pb_id_to_data(pbs_metadata)

    all_items = []
    categories = ["done", "inProgress", "pending", "recommended"]

    for category in categories:
        tasks = (response.get(category) or {}).get("caseTasks", [])
        for task in tasks:
            # Add category field to identify which list this came from
            task["category"] = category
            if category in ["done", "inProgress", "recommended"]:
                task["itemType"] = "playbook"
            else:
                task["itemType"] = "playbookTask"

            if category in ["done", "inProgress"]:
                enhance_with_pb_details(pb_id_to_data, task)
            elif category == "pending":
                # A pending task's parent playbook may not be resolved yet, so parentdetails
                # can be null (seen in prod). Only enhance when it's an actual dict.
                if parent_details := task.get("parentdetails"):
                    enhance_with_pb_details(pb_id_to_data, parent_details)
                task["parentPlaybook"] = task.pop("parentdetails", None)

            all_items.append(task)

    return all_items


def get_case_resolution_statuses(client, args):
    case_ids = argToList(args.get("case_id"))
    raw_responses = []
    outputs = []
    headers = ["category", "itemType", "id", "name", "description", "taskName"]
    for case_id in case_ids:
        response = client.get_case_resolution_statuses(case_id)
        raw_responses.append(response)
        outputs.append(postprocess_case_resolution_statuses(client, response))

    readable_parts = []
    for case_id, case_output in zip(case_ids, outputs):
        readable_parts.append(
            tableToMarkdown(
                f"Case {case_id} Resolution Statuses",
                case_output,
                headers=headers,
                headerTransform=pascalToSpace,
            )
        )
    readable_output = "\n".join(readable_parts)

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="Core.CaseResolutionStatus",
        outputs=outputs,
        raw_response=raw_responses,
    )


def list_findings_command(client: Client, args: dict[str, Any]) -> list[CommandResults]:
    """
    Retrieves findings from the Cortex platform filtered by asset ID and asset name.

    Args:
        client: The client instance used to send the request.
        args: Dictionary containing the arguments for the command.
              Expected to include:
                  - asset_id (str, optional): Filter by asset ID (supports comma-separated list).
                  - asset_name (str, optional): Filter by asset name (supports comma-separated list).
                  - page (int, optional): Page number for pagination. Default is 0.
                  - page_size (int, optional): Number of findings to return per page. Default is 100.

    Returns:
        list[CommandResults]: List containing:
            - CommandResults with findings data
            - CommandResults with metadata (filtered_count, returned_count)
    """
    asset_ids = argToList(args.get("asset_id"))
    asset_names = argToList(args.get("asset_name"))
    asset_category = argToList(args.get("asset_category"))
    asset_class = argToList(args.get("asset_class"))
    category = [c.replace(" ", "_").upper() for c in argToList(args.get("category"))]
    finding_source = [c.replace(" ", "_").upper() for c in argToList(args.get("finding_source"))]
    page = arg_to_number(args.get("page")) or 0
    page_size = arg_to_number(args.get("page_size")) or 100

    filter_builder = FilterBuilder()
    filter_builder.add_field("XDM_FINDING_ASSET_ID", FilterType.WILDCARD, asset_ids)
    filter_builder.add_field("XDM_FINDING_ASSET_NAME", FilterType.CONTAINS, asset_names)
    filter_builder.add_field("XDM_FINDING_ASSET_CLASS", FilterType.EQ, asset_class)
    filter_builder.add_field("XDM_FINDING_ASSET_CATEGORY", FilterType.EQ, asset_category)
    filter_builder.add_field("XDM_FINDING_CATEGORY", FilterType.EQ, category)
    filter_builder.add_field("xdm.finding_sources", FilterType.ARRAY_CONTAINS, finding_source)

    start_index = page * page_size
    end_index = start_index + page_size

    request_data = build_webapp_request_data(
        table_name=FINDINGS_TABLE,
        filter_dict=filter_builder.to_dict(),
        limit=end_index,
        sort_field="XDM_FINDING_LAST_OBSERVED",
        sort_order="DESC",
        start_page=start_index,
    )

    response = client.get_webapp_data(request_data)
    reply = response.get("reply", {})
    data = reply.get("DATA", [])

    counts_request_data = build_webapp_counts_request_data(
        table_name=FINDINGS_TABLE,
        filter_dict=filter_builder.to_dict(),
    )

    counts_response = client.get_webapp_counts(counts_request_data)
    counts_reply = counts_response.get("reply", {})
    filtered_count = counts_reply.get("FILTER_COUNT", 0)

    def map_findings(findings):
        return [{k.replace("XDM_FINDING_", "").lower(): v for k, v in finding.items()} for finding in findings]

    findings = map_findings(data)

    metadata = {
        "filtered_count": filtered_count,
        "returned_count": len(findings),
    }

    command_results = []

    command_results.append(
        CommandResults(
            outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.Finding",
            outputs_key_field="id",
            outputs=findings,
            raw_response=response,
        )
    )

    command_results.append(
        CommandResults(
            outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.FindingMetadata",
            outputs=metadata,
        )
    )

    return command_results


def build_target_filter_from_endpoint_ids(endpoint_ids: list[str]) -> dict:
    """
    Build a TARGET_FILTER object from endpoint names.

    Args:
        endpoint_names: List of endpoint names to target.

    Returns:
        dict: Filter object for targeting specific endpoints.
    """
    if len(endpoint_ids) == 1:
        # Single endpoint
        return {"filter": {"AND": [{"SEARCH_FIELD": "AGENT_ID", "SEARCH_TYPE": "EQ", "SEARCH_VALUE": endpoint_ids[0]}]}}
    else:
        # Multiple endpoints - use OR logic
        or_conditions = [
            {"SEARCH_FIELD": "AGENT_ID", "SEARCH_TYPE": "EQ", "SEARCH_VALUE": endpoint_id} for endpoint_id in endpoint_ids
        ]
        return {"filter": {"AND": [{"OR": or_conditions}]}}


def validate_profile_platform_compatibility(platform: str, profile_args: dict[str, str | None]) -> None:
    """
    Validate that the provided profiles are supported by the specified platform.

    Platform-specific profile restrictions:
    - serverless: Only 'restrictions' profile allowed
    - Android, iOS: Only 'malware' and 'agent_settings' profiles allowed
    - Linux, macOS, Windows: All profiles allowed

    Args:
        platform: The platform type (e.g., AGENT_OS_WINDOWS, AGENT_OS_SERVERLESS).
        profile_args: Dictionary of profile argument names to their values.
                     Keys should be profile types (e.g., 'exploit_profile', 'malware_profile').

    Raises:
        DemistoException: If a profile is provided that is not supported by the platform.
    """
    # Define allowed profiles per platform
    PLATFORM_ALLOWED_PROFILES: dict[str, list[str]] = {
        "serverless": ["restrictions"],
        "android": ["malware", "agent_settings"],
        "ios": ["malware", "agent_settings"],
        "linux": ["exploit", "malware", "agent_settings", "restrictions", "exceptions"],
        "mac": ["exploit", "malware", "agent_settings", "restrictions", "exceptions"],
        "windows": ["exploit", "malware", "agent_settings", "restrictions", "exceptions"],
    }

    allowed_profiles: list[str] = PLATFORM_ALLOWED_PROFILES.get(platform) or []

    # Check each provided profile
    unsupported_profiles = []
    for profile_name, profile_value in profile_args.items():
        # Skip if profile is not provided
        if not profile_value:
            continue

        # profile_name is the key like 'exploit', 'malware', etc.
        if profile_name not in allowed_profiles:
            unsupported_profiles.append(f"{profile_name} (value: {profile_value})")

    if unsupported_profiles:
        allowed_list = ", ".join(sorted(allowed_profiles))
        unsupported_list = ", ".join(unsupported_profiles)

        raise DemistoException(
            f"The following profiles are not supported for platform '{platform}': {unsupported_list}. "
            f"Allowed profiles for this platform: {allowed_list}."
        )


def get_profile_ids(client: Client, platform: str, profile_args: dict[str, str | None]) -> dict[str, dict[str, Any]]:
    """
    Get profile IDs for multiple profiles from AGENT_PROFILES_TABLE using OR filters.

    Args:
        client: The Cortex Platform client instance.
        platform: The platform type (e.g., AGENT_OS_WINDOWS, AGENT_OS_LINUX).
        profile_args: Dictionary mapping profile type (lowercase) to profile name or ID.
                     Example: {'exploit': 'Default', 'malware': 'Default'}

    Returns:
        dict: Dictionary mapping 'PROFILE_TYPE' (uppercase) to profile data.
              Example: {'EXPLOIT': {'id': 11, 'name': 'Default'}, 'MALWARE': {'id': 10, 'name': 'Default'}}

    Raises:
        DemistoException: If multiple profiles with the same name exist for a profile type,
                         or if a requested profile doesn't exist.
    """
    or_filters = []
    for profile_type_lower, profile_name_or_id in profile_args.items():
        if not profile_name_or_id:  # Skip profiles without values
            continue

        profile_type = profile_type_lower.upper()

        # Create AND condition for each profile (name OR id + type)
        and_condition = [
            {
                "OR": [
                    {"SEARCH_FIELD": "PROFILE_NAME", "SEARCH_TYPE": "EQ", "SEARCH_VALUE": profile_name_or_id},
                    {"SEARCH_FIELD": "PROFILE_ID", "SEARCH_TYPE": "EQ", "SEARCH_VALUE": profile_name_or_id},
                ]
            },
            {"SEARCH_FIELD": "PROFILE_TYPE", "SEARCH_TYPE": "EQ", "SEARCH_VALUE": profile_type},
        ]
        or_filters.append({"AND": and_condition})

    # Build the complete filter structure with platform at the top level
    combined_filter = {
        "AND": [
            {"SEARCH_FIELD": "PROFILE_PLATFORM", "SEARCH_TYPE": "EQ", "SEARCH_VALUE": Endpoints.ENDPOINT_PLATFORM[platform]},
            {"OR": or_filters},
        ]
    }

    request_data = {
        "type": "grid",
        "table_name": AGENT_PROFILES_TABLE,
        "filter_data": {
            "sort": [],
            "filter": combined_filter,
        },
    }

    demisto.debug(f"Querying profiles with filter: {combined_filter}")

    response = client.get_webapp_data(request_data)
    profiles_data = response.get("reply", {})

    # Group profiles by type to detect duplicates
    profiles_by_type: dict[str, list[dict]] = {}
    for profile in profiles_data:
        profile_type = profile.get("PROFILE_TYPE")
        if profile_type:
            if profile_type not in profiles_by_type:
                profiles_by_type[profile_type] = []
            profiles_by_type[profile_type].append(profile)

    # Build mapping and check for duplicates
    profile_map: dict[str, dict[str, Any]] = {}

    for profile_type_lower, profile_name_or_id in profile_args.items():
        if not profile_name_or_id:  # Skip profiles without values
            continue

        profile_type = profile_type_lower.upper()
        matching_profiles = profiles_by_type.get(profile_type, [])

        # Check if profile exists
        if not matching_profiles:
            raise DemistoException(
                f"Profile '{profile_name_or_id}' of type '{profile_type}' not found for platform '{platform}'. "
                f"Please verify the profile name or ID exists."
            )

        # Check for multiple profiles with the same name
        if len(matching_profiles) > 1:
            profile_details = "\n".join(
                [f"  - Name: {p.get('PROFILE_NAME')}, ID: {p.get('PROFILE_ID')}" for p in matching_profiles]
            )
            raise DemistoException(
                f"Multiple profiles found with name '{profile_name_or_id}' for type '{profile_type}':\n{profile_details}\n"
                f"Please use the profile ID instead to specify which one you want."
            )

        # Single profile found - add to map
        profile = matching_profiles[0]
        profile_id = profile.get("PROFILE_ID")
        profile_name = profile.get("PROFILE_NAME")
        profile_map[profile_type] = {"id": profile_id, "name": profile_name}

    demisto.debug(f"Retrieved profile IDs for platform {platform}: {profile_map}")
    return profile_map


def fetch_policy_table(client: Client) -> tuple[list[dict], str]:
    """
    Fetch the current agent policy table and hash.

    Returns:
        tuple: (policies_list, policy_hash)

    Raises:
        DemistoException: If policy hash cannot be retrieved.
    """
    demisto.debug("Fetching current agent policy table")
    policy_response = client.get_agent_policy_table()
    reply = policy_response.get("reply", {})
    current_policies = reply.get("DATA", [])
    policy_hash = reply.get("POLICY_HASH", "")

    if not policy_hash:
        raise DemistoException("Failed to retrieve policy hash from the current policy table.")

    demisto.debug(f"Current policy hash: {policy_hash}")
    demisto.debug(f"Current policies count: {len(current_policies)}")

    return current_policies, policy_hash


def resolve_platform_name(platform: str) -> str:
    """
    Resolve platform name.

    Args:
        platform: Platform name (e.g., 'windows', 'linux')

    Returns:
        str: Platform value (e.g., 'AGENT_OS_WINDOWS')

    Raises:
        DemistoException: If platform is invalid.
    """
    platform_value = Endpoints.ENDPOINT_PLATFORM.get(platform)
    if not platform_value:
        raise DemistoException(
            f"Invalid platform '{platform}'. Valid platforms are: {', '.join(Endpoints.ENDPOINT_PLATFORM.keys())}"
        )
    return platform_value


def resolve_endpoint_names_to_ids(client: Client, endpoint_names: list[str]) -> list[str]:
    """
    Resolve endpoint names to their corresponding endpoint IDs, validating that no duplicate names exist.

    Args:
        client (Client): The Cortex Platform client instance used to query the agents table.
        endpoint_names (list[str]): List of endpoint names to resolve to IDs.

    Returns:
        list[str]: List of endpoint IDs corresponding to the provided endpoint names.

    Raises:
        DemistoException: If no endpoints are found with the specified names, or if multiple
                          endpoints share the same name (ambiguous resolution).
    """
    demisto.debug(f"Resolving endpoint names to IDs: {endpoint_names}")

    filter_builder = FilterBuilder()
    filter_builder.add_field(Endpoints.ENDPOINT_FIELDS["endpoint_name"], FilterType.EQ, endpoint_names)

    request_data = build_webapp_request_data(
        table_name=AGENTS_TABLE,
        filter_dict=filter_builder.to_dict(),
        limit=MAX_GET_ENDPOINTS_LIMIT,
        sort_field="AGENT_NAME",
        sort_order="ASC",
    )

    response = client.get_webapp_data(request_data)
    reply = response.get("reply", {})
    raw_endpoints = reply.get("DATA", [])

    if not raw_endpoints:
        raise DemistoException(f'No endpoints found with the specified names: {", ".join(endpoint_names)}')

    endpoints = map_endpoint_format(raw_endpoints)

    # Check for duplicate endpoint names
    endpoint_name_to_ids: dict[str, list[str]] = {}
    for endpoint in endpoints:
        endpoint_name = endpoint.get("endpoint_name")
        endpoint_id = endpoint.get("endpoint_id")

        if endpoint_name and endpoint_id:
            if endpoint_name not in endpoint_name_to_ids:
                endpoint_name_to_ids[endpoint_name] = []
            endpoint_name_to_ids[endpoint_name].append(endpoint_id)

    duplicates = {name: ids for name, ids in endpoint_name_to_ids.items() if len(ids) > 1}

    if duplicates:
        error_message = "Multiple endpoints found with the same name. Please use target_endpoint_ids instead:\n\n"
        for endpoint_name, endpoint_ids in duplicates.items():
            error_message += f'Endpoint Name: "{endpoint_name}"\n'
            for endpoint_id in endpoint_ids:
                error_message += f"  - ID: {endpoint_id}\n"
            error_message += "\n"
        raise DemistoException(error_message.strip())

    endpoint_ids = [endpoint["endpoint_id"] for endpoint in endpoints if endpoint.get("endpoint_id")]
    demisto.debug(f"Resolved endpoint IDs: {endpoint_ids}")
    return endpoint_ids


def calculate_policy_priority(current_policies: list[dict], platform_value: str, requested_priority: int | None) -> int:
    """
    Calculate and validate the policy priority for a new endpoint policy, handling auto-assignment.

    If no priority is requested, assigns one higher than the current maximum for the platform.
    If the requested priority exceeds the current maximum, it is clamped to max + 1.

    Args:
        current_policies (list[dict]): The full list of existing policies from the policy table.
        platform_value (str): The platform identifier (e.g., 'AGENT_OS_WINDOWS', 'AGENT_OS_LINUX').
        requested_priority (int | None): The desired priority, or None to auto-assign.

    Returns:
        int: The validated and calculated priority value for the new policy.

    Raises:
        DemistoException: If the calculated priority is below the minimum allowed user policy priority (1).
    """
    MIN_USER_POLICY_PRIORITY = 1
    platform_policies = [p for p in current_policies if p.get("PLATFORM") == platform_value]

    if not platform_policies:
        priority = MIN_USER_POLICY_PRIORITY
    else:
        max_existing_priority = max(p.get("PRIORITY", 0) for p in platform_policies)

        if requested_priority is None:
            priority = max_existing_priority + 1
        elif requested_priority > max_existing_priority:
            demisto.debug(
                f"Priority {requested_priority} is higher than max ({max_existing_priority}). "
                f"Setting to {max_existing_priority + 1}."
            )
            priority = max_existing_priority + 1
        else:
            priority = requested_priority

    if priority < MIN_USER_POLICY_PRIORITY:
        raise DemistoException(f"Priority must be at least {MIN_USER_POLICY_PRIORITY}.")

    return priority


def shift_policy_priorities(current_policies: list[dict], platform_value: str, new_priority: int) -> None:
    """
    Shift existing policies with priority >= new_priority up by 1 to make room for a new policy.

    Modifies the policy list in-place. If no policy currently holds the given priority for the
    specified platform, no changes are made.

    Args:
        current_policies (list[dict]): The full list of existing policies from the policy table.
                                       Modified in-place.
        platform_value (str): The platform identifier (e.g., 'AGENT_OS_WINDOWS', 'AGENT_OS_LINUX').
        new_priority (int): The priority value at which to insert the new policy; all existing
                            policies at this priority or higher will be incremented by 1.

    Returns:
        None
    """
    existing_priority_policy = next(
        (p for p in current_policies if p.get("PLATFORM") == platform_value and p.get("PRIORITY") == new_priority), None
    )

    if not existing_priority_policy:
        return

    demisto.debug(f"Priority {new_priority} exists. Shifting policies.")

    policies_to_shift = sorted(
        [p for p in current_policies if p.get("PLATFORM") == platform_value and p.get("PRIORITY", 0) >= new_priority],
        key=lambda p: p.get("PRIORITY", 0),
        reverse=True,
    )

    for policy in policies_to_shift:
        current_priority = policy.get("PRIORITY", 0)
        new_priority_value = current_priority + 1
        policy["PRIORITY"] = new_priority_value
        demisto.debug(f"Shifted '{policy.get('NAME')}' from {current_priority} to {new_priority_value}")


def get_identity_and_web_api_profile_defaults(platform_value: str) -> dict[str, Any]:
    """
    Get platform-specific default profile IDs for identity and web_and_api profiles.

    Note: identity and web_and_api profiles are currently not supported by the UI, but the API
    requires them to be present in the policy creation request payload.

    Args:
        platform_value (str): The platform identifier (e.g., 'AGENT_OS_WINDOWS', 'AGENT_OS_LINUX').

    Returns:
        dict[str, Any]: A dictionary containing the identity and web_and_api profile names and IDs:
            - identity (str | None): The identity profile name, or None if not applicable.
            - identity_id (int | None): The identity profile ID, or None if not applicable.
            - web_and_api (str | None): The web_and_api profile name, or None if not applicable.
            - web_and_api_id (int | None): The web_and_api profile ID, or None if not applicable.
    """
    WINDOWS_IDENTITY_PROFILE_ID = 17
    LINUX_WEB_AND_API_PROFILE_ID = 12
    if platform_value == "AGENT_OS_WINDOWS":
        return {
            "identity": "Default",
            "identity_id": WINDOWS_IDENTITY_PROFILE_ID,
            "web_and_api": None,
            "web_and_api_id": None,
        }
    elif platform_value == "AGENT_OS_LINUX":
        return {
            "identity": None,
            "identity_id": None,
            "web_and_api": "Default",
            "web_and_api_id": LINUX_WEB_AND_API_PROFILE_ID,
        }
    else:
        return {
            "identity": None,
            "identity_id": None,
            "web_and_api": None,
            "web_and_api_id": None,
        }


def get_platform_specific_profile_defaults(platform: str, args: dict) -> dict[str, str | None]:
    """
    Get platform-specific default profile values.

    Platform-specific default values:
    - serverless: restrictions = 'Default', all others = None
    - android, ios: malware, agent_settings = 'Default', all others = None
    - linux, mac, windows: exceptions = 'Default (No Exceptions)', all others = 'Default'

    Args:
        platform: The platform type (e.g., 'serverless', 'android', 'linux')
        args: Command arguments containing user-provided profile values

    Returns:
        dict: Profile arguments with platform-specific defaults applied
    """
    # Get user-provided values (None if not provided)
    user_exploit = args.get("exploit_profile")
    user_malware = args.get("malware_profile")
    user_agent_settings = args.get("agent_settings_profile")
    user_restrictions = args.get("restrictions_profile")
    user_exceptions = args.get("exceptions_profile")

    # Set platform-specific defaults
    if platform == "serverless":
        return {
            "exploit": user_exploit,
            "malware": user_malware,
            "agent_settings": user_agent_settings,
            "restrictions": user_restrictions or "Default",
            "exceptions": user_exceptions,
        }
    elif platform in ["android", "ios"]:
        return {
            "exploit": user_exploit,
            "malware": user_malware or "Default",
            "agent_settings": user_agent_settings or "Default",
            "restrictions": user_restrictions,
            "exceptions": user_exceptions,
        }
    else:  # linux, mac, windows
        return {
            "exploit": user_exploit or "Default",
            "malware": user_malware or "Default",
            "agent_settings": user_agent_settings or "Default",
            "restrictions": user_restrictions or "Default",
            "exceptions": user_exceptions or "Default (No Exceptions)",
        }


def build_policy_object(
    policy_name: str,
    platform_value: str,
    priority: int,
    target_endpoint_ids: list[str],
    profile_map: dict[str, dict[str, Any]],
    description: str,
) -> dict[str, Any]:
    """
    Build the complete policy object with all required fields for the agent policy table.

    Assembles the full policy dictionary including platform, priority, target filter,
    profile assignments (exploit, malware, agent_settings, restrictions, exceptions),
    and platform-specific identity/web_and_api profile defaults.

    Args:
        policy_name (str): The name for the new policy.
        platform_value (str): The platform identifier (e.g., 'AGENT_OS_WINDOWS', 'AGENT_OS_LINUX').
        priority (int): The priority level for the policy.
        target_endpoint_ids (list[str]): List of endpoint IDs that this policy targets.
        profile_map (dict[str, dict[str, Any]]): Mapping of profile type (uppercase) to profile data
                                                  containing 'id' and 'name' keys.
                                                  Example: {'EXPLOIT': {'id': 11, 'name': 'Default'}}
        description (str): A human-readable description for the policy.

    Returns:
        dict[str, Any]: The complete policy object ready to be inserted into the agent policy table.
    """
    identity_and_web_api_profiles = get_identity_and_web_api_profile_defaults(platform_value)
    target_filter = build_target_filter_from_endpoint_ids(target_endpoint_ids)

    # Extract profile data
    policy = {
        "IS_ANY": False,
        "PLATFORM": platform_value,
        "NAME": policy_name,
        "IS_ENABLED": True,
        "TARGET_FILTER": target_filter,
        "TARGET_GROUP_TYPE": "STATIC",
        "EXPLOIT": profile_map.get("EXPLOIT", {}).get("name"),
        "EXPLOIT_ID": profile_map.get("EXPLOIT", {}).get("id"),
        "MALWARE": profile_map.get("MALWARE", {}).get("name"),
        "MALWARE_ID": profile_map.get("MALWARE", {}).get("id"),
        "AGENT_SETTINGS": profile_map.get("AGENT_SETTINGS", {}).get("name"),
        "AGENT_SETTINGS_ID": profile_map.get("AGENT_SETTINGS", {}).get("id"),
        "RESTRICTIONS": profile_map.get("RESTRICTIONS", {}).get("name"),
        "RESTRICTIONS_ID": profile_map.get("RESTRICTIONS", {}).get("id"),
        "EXCEPTIONS": profile_map.get("EXCEPTIONS", {}).get("name"),
        "EXCEPTIONS_ID": profile_map.get("EXCEPTIONS", {}).get("id"),
        "IDENTITY_ID": identity_and_web_api_profiles["identity_id"],
        "IDENTITY": identity_and_web_api_profiles["identity"],
        "WEB_AND_API_ID": identity_and_web_api_profiles["web_and_api_id"],
        "WEB_AND_API": identity_and_web_api_profiles["web_and_api"],
        "TARGET": [],
        "PRIORITY": priority,
        "DESCRIPTION": description,
    }

    return policy


def create_endpoint_policy_command(client: Client, args: dict) -> CommandResults:
    """
    Creates a new endpoint policy and applies it to specified endpoints.

    This command handles the complex logic of:
    1. Fetching the current policy table and hash
    2. Managing priority conflicts (shifting existing policies if needed)
    3. Creating the new policy with proper schema (TARGET, TARGET_FILTER, profile IDs, etc.)
    4. Updating the policy table with the complete data

    Policy Priority Specifications:
    - Default Policy: Fixed at priority 0 (system-level constant). This is a system-level constant
    and cannot be assigned to user-defined policies.
    - Priority Ranking: Higher numbers = higher precedence
    - UI Display: Policies sorted descending (highest priority first)

    Args:
        client: The Cortex Platform client instance.
        args: Dictionary containing policy configuration parameters:
            - policy_name (required): Name for the new policy
            - target_endpoint_names (optional): Comma-separated list of endpoint names
            - target_endpoint_ids (optional): Comma-separated list of endpoint IDs
            - platform (required): Platform type (AGENT_OS_WINDOWS, AGENT_OS_MAC, etc.)
            - description (optional): Policy description
            - priority (optional): Policy priority (default: auto-assigned)
            - exploit_profile (optional): Exploit protection profile name
            - malware_profile (optional): Malware protection profile name
            - agent_settings_profile (optional): Agent settings profile name


    Returns:
        CommandResults: Results object with success message and created policy details.

    Raises:
        DemistoException: If required parameters are missing or policy creation fails.
    """
    policy_name = args.get("policy_name", "")
    target_endpoint_names = argToList(args.get("target_endpoint_names", ""))
    target_endpoint_ids = argToList(args.get("target_endpoint_ids", ""))
    platform = args.get("platform", "")
    description = args.get("description", "")
    requested_priority = arg_to_number(args.get("priority"))

    if target_endpoint_names and target_endpoint_ids:
        raise DemistoException(
            "Cannot provide both target_endpoint_names and target_endpoint_ids. " "Please use one or the other."
        )

    if not target_endpoint_names and not target_endpoint_ids:
        raise DemistoException("Either target_endpoint_names or target_endpoint_ids must be provided.")

    platform_value = resolve_platform_name(platform)

    if target_endpoint_names:
        target_endpoint_ids = resolve_endpoint_names_to_ids(client, target_endpoint_names)

    current_policies, policy_hash = fetch_policy_table(client)

    priority = calculate_policy_priority(current_policies, platform_value, requested_priority)
    shift_policy_priorities(current_policies, platform_value, priority)

    profile_args = get_platform_specific_profile_defaults(platform, args)

    # Validate that user isn't trying to set profiles not allowed for the platform
    # This will raise an error if incompatible profiles are provided
    validate_profile_platform_compatibility(platform, profile_args)
    profile_map = get_profile_ids(client, platform, profile_args)

    new_policy = build_policy_object(
        policy_name=policy_name,
        platform_value=platform_value,
        priority=priority,
        target_endpoint_ids=target_endpoint_ids,
        profile_map=profile_map,
        description=description,
    )

    demisto.debug(f"New policy to be created: {new_policy}")

    updated_policies = current_policies + [new_policy]
    update_payload = {
        "DATA": updated_policies,
        "POLICY_HASH": policy_hash,
    }

    demisto.debug("Updating agent policy table")
    response = client.update_agent_policy(update_payload)

    readable_output = (
        f"Successfully created endpoint policy '{policy_name}' with priority {priority} "
        f"for platform {platform}.\n"
        f"Target endpoints (by ID): {', '.join(target_endpoint_ids)}"
    )

    outputs = {
        "PolicyName": policy_name,
        "Platform": platform,
        "Priority": priority,
        "TargetEndpointIds": target_endpoint_ids,
        "ExploitProfile": profile_args["exploit"],
        "MalwareProfile": profile_args["malware"],
        "AgentSettingsProfile": profile_args["agent_settings"],
        "RestrictionsProfile": profile_args["restrictions"],
        "ExceptionsProfile": profile_args["exceptions"],
        "Description": description,
    }
    remove_nulls_from_dictionary(outputs)

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.EndpointPolicy",
        outputs_key_field="PolicyName",
        outputs=outputs,
        raw_response=response,
    )


def find_policies_to_delete(
    platform_policies: list[dict],
    policy_names: list[str],
    policy_ids: list[str],
    platform: str,
) -> list[dict]:
    """
    Find the policies to delete from the platform policy list based on names or IDs.

    Searches by policy IDs if provided, otherwise searches by policy names. When searching
    by name, raises an error if multiple policies share the same name to avoid ambiguous deletion.

    Args:
        platform_policies (list[dict]): List of existing policies filtered to the target platform.
        policy_names (list[str]): List of policy names to search for. Used when policy_ids is empty.
        policy_ids (list[str]): List of policy IDs to search for. Takes precedence over policy_names.
        platform (str): The platform name (e.g., 'windows', 'linux') used in error messages.

    Returns:
        list[dict]: List of policy dictionaries that match the specified names or IDs.

    Raises:
        DemistoException: If a policy ID or name is not found, or if multiple policies share
                          the same name (ambiguous match when searching by name).
    """
    policies_to_delete = []

    if policy_ids:
        # Delete by IDs
        for policy_id in policy_ids:
            matching_policies = [p for p in platform_policies if str(p.get("ID")) == str(policy_id)]

            if not matching_policies:
                raise DemistoException(f"No policy found with ID '{policy_id}' for platform '{platform}'.")

            policies_to_delete.append(matching_policies[0])
    else:
        # Delete by names
        for policy_name in policy_names:
            matching_policies = [p for p in platform_policies if p.get("NAME") == policy_name]

            if not matching_policies:
                raise DemistoException(f"No policy found with name '{policy_name}' for platform '{platform}'.")

            if len(matching_policies) > 1:
                policy_details = "\n".join(
                    [f"  - Name: {p.get('NAME')}, ID: {p.get('ID')}, Priority: {p.get('PRIORITY')}" for p in matching_policies]
                )
                raise DemistoException(
                    f"Multiple policies found with name '{policy_name}' for platform '{platform}':\n{policy_details}\n"
                    f"Please use policy_id to specify which one to delete."
                )

            policies_to_delete.append(matching_policies[0])

    return policies_to_delete


def validate_policy_deletable(policy: dict, platform: str) -> None:
    """
    Validate that a policy is eligible for deletion.

    Default policies (priority 0) are system-level and cannot be deleted.

    Args:
        policy (dict): The policy dictionary to validate, expected to contain 'PRIORITY' and 'NAME' keys.
        platform (str): The platform name (e.g., 'windows', 'linux') used in error messages.

    Returns:
        None

    Raises:
        DemistoException: If the policy has priority 0 (default system policy), which cannot be deleted.
    """
    DEFAULT_POLICY_PRIORITY = 0  # System default, cannot be deleted
    priority = policy.get("PRIORITY")

    if priority == DEFAULT_POLICY_PRIORITY:
        policy_name = policy.get("NAME")
        raise DemistoException(
            f"Cannot delete the default policy '{policy_name}' "
            f"(priority {DEFAULT_POLICY_PRIORITY}) for platform '{platform}'. "
            f"Default policies are system-level and cannot be removed."
        )


def delete_endpoint_policy_command(client: Client, args: dict) -> CommandResults:
    """
    Delete one or more existing endpoint policies from the agent policy table.

    This command:
    1. Fetches the current policy table and hash
    2. Identifies policies to delete by names or IDs
    3. Validates that the policies can be deleted (not default system policies)
    4. Removes the policies and updates the table with the new hash

    Args:
        client (Client): The Cortex Platform client instance.
        args (dict): Dictionary containing policy identification parameters:
            - policy_name (str, optional): Comma-separated list of policy names to delete.
            - policy_id (str, optional): Comma-separated list of policy IDs to delete.
            - platform (str): The platform type (e.g., 'windows', 'linux', 'mac').

    Returns:
        CommandResults: Results object containing a success message and the list of deleted
                        policy details (PolicyName, PolicyID, Platform, Priority, Deleted).

    Raises:
        DemistoException: If neither policy_name nor policy_id is provided, if both are provided,
                          if the platform is invalid, if no policies exist for the platform,
                          if a specified policy is not found, or if a default policy deletion
                          is attempted.
    """
    policy_names = argToList(args.get("policy_name"))
    policy_ids = argToList(args.get("policy_id"))
    platform = args.get("platform", "")

    if not policy_names and not policy_ids:
        raise DemistoException("Either policy_name or policy_id must be provided to identify the policy to delete.")

    if policy_names and policy_ids:
        raise DemistoException("Cannot provide both policy_name and policy_id. Please use one or the other.")

    platform_value = resolve_platform_name(platform)

    current_policies, policy_hash = fetch_policy_table(client)

    platform_policies = [p for p in current_policies if p.get("PLATFORM") == platform_value]
    demisto.debug(f"Found {len(platform_policies)} policies for platform '{platform}'")

    if not platform_policies:
        raise DemistoException(f"No policies found for platform '{platform}'.")

    policies_to_delete = find_policies_to_delete(platform_policies, policy_names, policy_ids, platform)

    for policy in policies_to_delete:
        validate_policy_deletable(policy, platform)

    deleted_policies_info = []
    for policy in policies_to_delete:
        policy_info = {
            "PolicyName": policy.get("NAME"),
            "PolicyID": policy.get("ID"),
            "Platform": platform,
            "Priority": policy.get("PRIORITY"),
            "Deleted": True,
        }
        deleted_policies_info.append(policy_info)
        demisto.debug(
            f"Queuing policy for deletion: Name='{policy_info['PolicyName']}', "
            f"ID={policy_info['PolicyID']}, Priority={policy_info['Priority']}"
        )

    policies_to_delete_set = {p.get("ID") for p in policies_to_delete}
    updated_policies = [p for p in current_policies if p.get("ID") not in policies_to_delete_set]

    update_payload = {
        "DATA": updated_policies,
        "POLICY_HASH": policy_hash,
    }

    demisto.debug("Updating agent policy table")
    response = client.update_agent_policy(update_payload)

    demisto.debug(
        f"Update successful, policies count after deletion: {len(updated_policies)} (removed {len(policies_to_delete)})"
    )
    readable_output = "Successfully deleted the following endpoint policies:\n\n"
    for policy_info in deleted_policies_info:
        readable_output += (
            f"- Name: {policy_info['PolicyName']}, ID: {policy_info['PolicyID']}, Priority: {policy_info['Priority']}\n"
        )
    return CommandResults(
        readable_output=readable_output,
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.DeletedEndpointPolicy",
        outputs_key_field="PolicyID",
        outputs=deleted_policies_info,
        raw_response=response,
    )


def verify_platform_version(version: str = "8.13.0"):
    if not is_demisto_version_ge(version):
        raise DemistoException("This command is not available for this platform version")


def create_profile_modules_by_type(args: dict, profile_type: str):
    """
    Creates the modules configuration for a profile based on the profile type.

    Args:
        args (dict): The arguments containing the configuration for the profile modules.
        profile_type (str): The type of the profile ("Malware" or "Exploit").

    Returns:
        dict: A dictionary containing the configured modules for the profile.
    """
    profile_modules = {}
    if profile_type == MALWARE_TYPE:
        # Configuration for periodic scan to be used when enabled
        scan_endpoints_periodic_config = {"type": "weekly", "days": ["sun"], "hour": "00:00", "removableMedia": "disabled"}

        scan_endpoints_arg = args.get(Profile.FIELDS.get("scanEndpoints"), "disabled")
        if scan_endpoints_arg == "enabled":
            periodic_scan_config = {"mode": "enabled", **scan_endpoints_periodic_config}
        else:
            periodic_scan_config = {"mode": "disabled"}

        profile_modules = {
            "aspFiles": {
                "mode": args.get(Profile.FIELDS.get("aspFiles"), "disabled"),
                "upload": "enabled",
                "actionOnUnknown": "runLocalAnalysis",
            },
            "basTools": {"mode": args.get(Profile.FIELDS.get("basTools"), "disabled")},
            "uacBypass": {"mode": args.get(Profile.FIELDS.get("uacBypass"), "block"), "quarantine": "enabled"},
            "ransomware": {
                "mode": args.get(Profile.FIELDS.get("ransomware"), "block"),
                "quarantine": "disabled",
                "smbEncryption": "enabled",
                "protectionMode": "normal",
            },
            "cryptominers": {"mode": args.get(Profile.FIELDS.get("cryptominers"), "block"), "quarantine": "enabled"},
            "antiTampering": {
                "mode": args.get(Profile.FIELDS.get("antiTampering"), "block"),
                "safeMode": args.get(Profile.FIELDS.get("antiTampering"), "block"),
                "quarantine": "enabled",
            },
            "iisProtection": {"mode": args.get(Profile.FIELDS.get("iisProtection"), "block"), "quarantine": "enabled"},
            "scanEndpoints": {
                "periodicScan": periodic_scan_config,
                "endUserInitiatedLocalScan": args.get(Profile.FIELDS.get("endUserInitiatedLocalScan"), "enabled"),
            },
            "uefiProtection": {"mode": args.get(Profile.FIELDS.get("uefiProtection"), "block"), "quarantine": "enabled"},
            "maliciousDevice": {"mode": args.get(Profile.FIELDS.get("maliciousDevice"), "block"), "quarantine": "disabled"},
            "networkSignature": {"mode": args.get(Profile.FIELDS.get("networkSignature"), "terminateSession")},
            "passwordStealing": {"mode": args.get(Profile.FIELDS.get("passwordStealing"), "block"), "quarantine": "enabled"},
            "webshellDroppers": {"mode": args.get(Profile.FIELDS.get("webshellDroppers"), "block"), "quarantine": "enabled"},
            "onWriteProtection": {
                "examinePortableExecutables": "disabled",
                "examineOfficeFiles": "disabled",
                "powerShellScriptFiles": "disabled",
                "aspFiles": "disabled",
                "examineVBScriptFiles": "disabled",
                "examineJScriptFiles": "disabled",
            },
            "examineOfficeFiles": {
                "mode": args.get(Profile.FIELDS.get("examineOfficeFiles"), "block"),
                "upload": "enabled",
                "networkDrives": "enabled",
                "actionOnUnknown": "runLocalAnalysis",
                "actionOnLowConfidence": "runLocalAnalysis",
            },
            "inProcessShellcode": {
                "mode": args.get(Profile.FIELDS.get("inProcessShellcode"), "block"),
                "quarantine": "enabled",
                "processInjection32Bit": "enabled",
                "aiPoweredShellcodeProtection": "enabled",
            },
            "examineJScriptFiles": {
                "mode": args.get(Profile.FIELDS.get("examineJScriptFiles"), "block"),
                "upload": "enabled",
                "quarantine": "disabled",
                "actionOnUnknown": "runLocalAnalysis",
            },
            "legitimateProcesses": {"mode": args.get(Profile.FIELDS.get("legitimateProcesses"), "block")},
            "examineVBScriptFiles": {
                "mode": args.get(Profile.FIELDS.get("examineVBScriptFiles"), "block"),
                "upload": "enabled",
                "quarantine": "disabled",
                "actionOnUnknown": "runLocalAnalysis",
            },
            "dynamicSecurityEngine": {
                "mode": args.get(Profile.FIELDS.get("dynamicSecurityEngine"), "block"),
                "quarantine": "enabled",
                "advancedApiMonitoring": "enabled",
                "driversProtectionMode": "block",
            },
            "powerShellScriptFiles": {
                "mode": args.get(Profile.FIELDS.get("powerShellScriptFiles"), "block"),
                "upload": "enabled",
                "quarantine": "disabled",
                "actionOnUnknown": "runLocalAnalysis",
            },
            "financialMalwareThreat": {
                "mode": args.get(Profile.FIELDS.get("financialMalwareThreat"), "block"),
                "quarantine": "enabled",
                "cryptoWalletProtection": "enabled",
            },
            "securityMeasuresBypass": {
                "mode": args.get(Profile.FIELDS.get("securityMeasuresBypass"), "block"),
                "quarantine": "enabled",
            },
            "dynamicDriverProtection": {
                "mode": args.get(Profile.FIELDS.get("dynamicDriverProtection"), "block"),
                "quarantine": "disabled",
            },
            "dynamicKernelProtection": {"mode": args.get(Profile.FIELDS.get("dynamicKernelProtection"), "block")},
            "passwordTheftProtection": {"mode": args.get(Profile.FIELDS.get("passwordTheftProtection"), "enabled")},
            "examinePortableExecutables": {
                "mode": args.get(Profile.FIELDS.get("examinePortableExecutables"), "block"),
                "upload": "enabled",
                "grayware": "disabled",
                "quarantine": "disabled",
                "actionOnUnknown": "runLocalAnalysis",
                "actionOnLowConfidence": "runLocalAnalysis",
            },
            "maliciousCausalityChainsResponse": {
                "mode": args.get(Profile.FIELDS.get("maliciousCausalityChainsResponse"), "enabled")
            },
        }

    elif profile_type == EXPLOIT_TYPE:
        profile_modules = {
            "vulnerableApps": {"mode": args.get(Profile.FIELDS.get("vulnerableApps"), "block"), "javaProtection": "enabled"},
            "logicalExploits": {"mode": args.get(Profile.FIELDS.get("logicalExploits"), "block"), "forbidDllLoad": []},
            "osKernelExploits": {"mode": args.get(Profile.FIELDS.get("osKernelExploits"), "block")},
            "browserExploitKits": {"mode": args.get(Profile.FIELDS.get("browserExploitKits"), "block")},
            "additionalProcesses": {"mode": args.get(Profile.FIELDS.get("additionalProcesses"), "disabled"), "processes": []},
        }

    return profile_modules


def validate_profile_args(args: dict):
    """
    Validates that the arguments provided in args match the predefined values in Profile.VALIDATION.
    """
    invalid_args = []
    for arg, value in args.items():
        if arg in Profile.VALIDATION:
            allowed_values = Profile.VALIDATION[arg]
            if value not in allowed_values:
                invalid_args.append(
                    f"Invalid value '{value}' for argument '{arg}'. Allowed values are: {', '.join(allowed_values)}."
                )

    if invalid_args:
        raise DemistoException("\n".join(invalid_args))


def create_profile_command(
    client: Client, args: dict, profile_type: str, profile_platform: str = WINDOWS_PLATFORM
) -> CommandResults:
    """
    Creates a new profile in the Cortex Platform.

    Args:
        client (Client): The client instance used to send the request.
        args (dict): The arguments containing the profile details.
        profile_type (str): The type of the profile ("Malware" or "Exploit").

    Returns:
        CommandResults: The command results containing the ID of the created profile.
    """
    validate_profile_args(args)
    profile_name = args.get("profile_name")
    profile_description = args.get("profile_description", "")

    profile_modules = create_profile_modules_by_type(args, profile_type)
    payload = {
        "request_data": {
            "name": profile_name,
            "profile_type": profile_type,
            "platform": profile_platform,
            "description": profile_description,
            "modules": profile_modules,
        }
    }
    response = client.create_profile(payload).get("reply", "")

    return CommandResults(
        readable_output=f"Profile {response} created successfully.",
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.Profile",
        outputs={"profile_id": response},
        raw_response={"profile_id": response},
    )


def update_profile_command(client, args):
    """
    Updates an existing profile in the Cortex Platform.

    Args:
        client (Client): The client instance used to send the request.
        args (dict): The arguments containing the profile ID and fields to update.

    Returns:
        CommandResults: The command results indicating the success of the update.
    """
    validate_profile_args(args)
    profile_id = args.get("profile_id")

    current_profile = client.get_profile(profile_id)
    if not current_profile or current_profile.get("reply") is None:
        raise DemistoException(f"Profile {profile_id} doesn't exist.")

    update_data = current_profile.get("reply", {})
    current_profile_modules = update_data.get("PROFILE_MODULES", {})

    for module_name, module_data in current_profile_modules.items():
        arg_value = args.get(Profile.FIELDS.get(module_name))
        if arg_value and "mode" in module_data:
            module_data["mode"]["value"] = arg_value
            if "safeMode" in module_data:
                module_data["safeMode"]["value"] = arg_value

    if profile_name := args.get("profile_name"):
        update_data["PROFILE_NAME"] = profile_name

    if profile_description := args.get("profile_description"):
        update_data["PROFILE_DESCRIPTION"] = profile_description

    update_data["PROFILE_MODULES"] = current_profile_modules
    update_profile = {"profile_id": profile_id, "update_data": update_data}

    client.update_profile(update_profile)

    return CommandResults(readable_output=f"Profile {profile_id} updated successfully.")


def delete_profile_command(client, args):
    """
    Deletes one or more profiles from the Cortex Platform.

    Args:
        client (Client): The client instance used to send the request.
        args (dict): The arguments containing the profile IDs to delete.

    Returns:
        CommandResults: The command results indicating the success of the deletion.
    """
    profile_ids = argToList(args.get("profile_ids"))
    client.delete_profile(profile_ids)
    return CommandResults(readable_output="Your request was sent successfully.")


def list_brokers_command(client: Client, args: dict) -> CommandResults:
    """
    Retrieves broker information from the BROKER_CLUSTER_TABLE.

    Args:
        client (Client): The client instance used to send the request.
        args (dict): Dictionary containing the arguments for the command.
                     Expected to include:
                         - broker_vm_names (str, optional): Comma-separated list of broker VM names to filter by.
                         - limit (int, optional): Maximum number of brokers to return (default: 50).

    Returns:
        CommandResults: Object containing the broker data.
    """
    broker_vm_names = argToList(args.get("broker_vm_names"))
    limit = arg_to_number(args.get("limit")) or 50

    filter_builder = FilterBuilder()
    if broker_vm_names:
        filter_builder.add_field("DEVICE_NAME", FilterType.EQ, broker_vm_names)

    request_data = build_webapp_request_data(
        table_name=BROKER_CLUSTER_TABLE,
        filter_dict=filter_builder.to_dict(),
        limit=limit,
        sort_field="DEVICE_NAME",
    )

    demisto.debug(f"Querying BROKER_CLUSTER_TABLE with request: {request_data}")
    response = client.get_webapp_data(request_data)
    reply = response.get("reply", {})
    brokers = reply.get("brokers", [])

    return CommandResults(
        readable_output=tableToMarkdown("Brokers", brokers, headerTransform=string_to_table_header),
        outputs_prefix=f"{INTEGRATION_CONTEXT_BRAND}.Broker",
        outputs_key_field="DEVICE_NAME",
        outputs=brokers,
    )


def main():  # pragma: no cover
    """
    Executes an integration command
    """
    command = demisto.command()
    demisto.debug(f"Command being called is {command}")
    args = demisto.args()
    args["integration_context_brand"] = INTEGRATION_CONTEXT_BRAND
    args["integration_name"] = INTEGRATION_NAME
    remove_nulls_from_dictionary(args)
    # Logic to determine which API type the current command belongs to
    if command in WEBAPP_COMMANDS:
        api_type = "webapp"
    elif command in DATA_PLATFORM_COMMANDS:
        api_type = "data_platform"
    elif command in APPSEC_COMMANDS:
        api_type = "appsec"
    elif command in ENDPOINT_COMMANDS:
        api_type = "agents"
    elif command in XSOAR_COMMANDS:
        api_type = "xsoar"
    else:
        api_type = "public"

    client = init_client(api_type)

    try:
        if command == "test-module":
            client.test_module()
            demisto.results("ok")

        elif command == "core-get-asset-details":
            return_results(get_asset_details_command(client, args))

        elif command == "core-search-asset-groups":
            return_results(search_asset_groups_command(client, args))

        elif command == "core-get-issues":
            return_results(get_issues_command(client, args))

        elif command == "core-get-cases":
            return_results(get_cases_command(client, args))

        elif command == "core-get-case-extra-data":
            return_results(get_extra_data_for_case_id_command(client, args))

        elif command == "core-search-assets":
            return_results(search_assets_command(client, args))

        elif command == "core-get-vulnerabilities":
            return_results(get_vulnerabilities_command(client, args))

        elif command == "core-update-issue":
            return_results(update_issue_command(client, args))

        elif command == "core-get-issue-recommendations":
            return_results(get_issue_recommendations_command(client, args))

        elif command == "core-enable-scanners":
            return_results(enable_scanners_command(client, args))

        elif command == "core-appsec-remediate-issue":
            return_results(appsec_remediate_issue_command(client, args))

        elif command == "core-get-asset-coverage":
            return_results(get_asset_coverage_command(client, args))

        elif command == "core-get-asset-coverage-histogram":
            return_results(get_asset_coverage_histogram_command(client, args))
        elif command == "core-create-appsec-policy":
            return_results(create_policy_command(client, args))
        elif command == "core-get-appsec-issues":
            return_results(get_appsec_issues_command(client, args))
        elif command == "core-update-case":
            return_results(update_case_command(client, args))
        elif command == "core-run-playbook":
            return_results(run_playbook_command(client, args))
        elif command == "core-list-scripts":
            return_results(list_scripts_command(client, args))
        elif command == "core-run-script-agentix":
            return_results(run_script_agentix_command(client, args))

        elif command == "core-get-endpoint-support-file":
            return_results(get_endpoint_support_file_command(client, args))

        elif command == "core-send-endpoint-heartbeat":
            return_results(send_endpoint_heartbeat_command(client, args))

        elif command == "core-list-exception-rules":
            return_results(list_exception_rules_command(client, args))
        elif command == "core-list-system-users":
            return_results(list_system_users_command(client, args))
        elif command == "core-list-endpoints":
            return_results(core_list_endpoints_command(client, args))
        elif command == "core-add-assessment-profile":
            return_results(core_add_assessment_profile_command(client, args))
        elif command == "core-list-compliance-standards":
            return_results(core_list_compliance_standards_command(client, args))

        elif command == "core-get-endpoint-update-version":
            return_results(get_endpoint_update_version_command(client, args))

        elif command == "core-update-endpoint-version":
            return_results(update_endpoint_version_command(client, args))

        elif command == "core-get-case-resolution-statuses":
            verify_platform_version()
            return_results(get_case_resolution_statuses(client, args))

        elif command == "core-xql-generic-query-platform":
            verify_platform_version()
            return_results(xql_query_platform_command(client, args))

        elif command == "core-get-ai-model-activity":
            return_results(get_ai_model_activity_command(client, args))

        elif command == "core-create-windows-malware-profile":
            return_results(create_profile_command(client, args, MALWARE_TYPE, WINDOWS_PLATFORM))

        elif command == "core-create-windows-exploit-profile":
            return_results(create_profile_command(client, args, EXPLOIT_TYPE, WINDOWS_PLATFORM))

        elif command == "core-update-windows-malware-profile":
            return_results(update_profile_command(client, args))

        elif command == "core-update-windows-exploit-profile":
            return_results(update_profile_command(client, args))

        elif command == "core-delete-profile":
            return_results(delete_profile_command(client, args))

        elif command == "core-list-findings":
            return_results(list_findings_command(client, args))

        elif command == "core-list-brokers":
            return_results(list_brokers_command(client, args))

        elif command == "core-create-endpoint-policy":
            return_results(create_endpoint_policy_command(client, args))

        elif command == "core-delete-endpoint-policy":
            return_results(delete_endpoint_policy_command(client, args))
    except Exception as err:
        demisto.error(traceback.format_exc())
        return_error(str(err), error=err)


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