Cortex Core - Platform
This integration uses the Cortex API to access all the core services and capabilities of the Cortex platform.
Endpoint · Core
Details
| ID | Cortex Core - Platform |
|---|---|
| Provider | Palo Alto Networks |
| Category | Endpoint |
| From Version | 6.2.0 |
| Docker Image | demisto/google-cloud-storage:1.0.0.10120494 |
| Supported Modules | Exposure 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-filecore-list-scriptscore-run-script-agentixcore-get-endpoint-update-versioncore-update-endpoint-version
The following commands require one of these licenses: Cloud Posture (Security) or Cloud Runtime Security:
core-add-assessment-profilecore-list-compliance-standards
</~PLATFORM>
Configuration parameters
timeout— HTTP Timeout
Commands (40)
-
core-add-assessment-profileCreate new assessment profile.
-
core-appsec-remediate-issueCreate automated pull requests to fix multiple security issues in a single bulk operation.
-
core-create-appsec-policyCreates a new AppSec policy in Cortex Platform with defined conditions, scope, and triggers for application security governance.
-
core-create-endpoint-policyCreates a new endpoint policy and applies it to specified endpoints. Automatically handles priority conflicts by shifting existing policies when needed.
-
core-create-windows-exploit-profileCreates a new Windows exploit profile.
-
core-create-windows-malware-profileCreates a new windows malware profile.
-
core-delete-endpoint-policyDeletes one or more existing endpoint policies from the policy table.
-
core-delete-profileDeletes the provided profiles.
-
core-enable-scannersEnable or disable scanners with the specified configuration.
-
core-get-ai-model-activityRetrieves AI model activity information including usage statistics and inactive status.
-
core-get-appsec-issuesRetrieves application security issues based on specified filters.
-
core-get-asset-coverageRetrieves a list of assets (e.g., Repositories, CI/CD Pipelines, Container Image Repositories) along with their scan coverage status.
-
core-get-asset-coverage-histogramCalculates the distribution of values (counts and percentages) for specified categorical fields.
-
core-get-asset-detailsGet asset information.
-
core-get-case-extra-dataGet extra data fields of a specific case, including issues and key artifacts.
-
core-get-case-resolution-statusesRetrieves resolution status information for a specific case.
-
core-get-casesGet case information based on the specified filters.
-
core-get-endpoint-support-fileRetrieves endpoint support files based on specified endpoint IDs.
-
core-get-endpoint-update-versionRetrieves endpoint update versions for the provided endpoint IDs.
-
core-get-issue-recommendationsGet comprehensive recommendations for an issue, including remediation steps, playbook suggestions, and recommended actions.
-
core-get-issuesReturns 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-vulnerabilitiesRetrieves vulnerabilities based on specified filters.
-
core-list-brokersRetrieves information about broker VMs configured on the system.
-
core-list-compliance-standardsCreate new assessment profile.
-
core-list-endpointsRetrieves endpoints based on the provided filters.
-
core-list-exception-rulesReturns 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-findingsRetrieves findings from the Cortex platform.
-
core-list-scriptsGets a list of scripts available in the scripts library.
-
core-list-system-usersReturns a list users and their core properties within the system. Results are limited to 50.
-
core-run-playbookRuns a playbook on specific issue IDs.
-
core-run-script-agentixRun a script on endpoints.
-
core-search-asset-groupsSearches for asset groups from the Cortex platform using one or more filter criteria.
-
core-search-assetsRetrieves asset from the Cortex platform using optional filter criteria.
-
core-send-endpoint-heartbeatSends endpoint heartbeat.
-
core-update-caseUpdates the properties of a case.
-
core-update-endpoint-versionUpdates the version of the given endpoint to the target version supplied.
-
core-update-issueUpdates the properties of an issue. This command does not provide an explicit indication of success.
-
core-update-windows-exploit-profileUpdates the specified Windows exploit profile.
-
core-update-windows-malware-profileUpdates the specified Windows malware profile.
-
core-xql-generic-query-platformExecute 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()