Vectra_Detect
This integration allows to create incidents based on Vectra Accounts/Hosts/Detections objects.
Network Security · Vectra AI
Details
| ID | Vectra_Detect |
|---|---|
| Provider | Vectra AI |
| Category | Network Security |
| From Version | 6.5.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
This integration allows to create incidents based on Vectra Accounts/Hosts/Detections objects.
This integration was integrated and tested with version 7.1 of Vectra Detect
Use cases
- Fetch accounts, hosts and detections from Vectra Detect.
- Bi-Directional mirroring for accounts and hosts.
- List and describe accounts, hosts, detections, and users.
- List, describe, create, and resolve assignments for accounts and hosts.
- List, describe, and create assignment outcomes.
- List, create, update, and delete notes for accounts, hosts, and detections.
- List, create, and remove tags for accounts, hosts, and detections.
- List, assign, and unassign members in group.
- Mark and unmark detection as fixed.
- Mark all detections as fixed for accounts and hosts.
- Get detection’s PCAP file.
- Clean up all incidents in Cortex XSOAR by closing duplicate incidents from Vectra Detect.
Configure Vectra Detect on Cortex XSOAR
- Navigate to Settings > Integrations > Servers & Services.
- Search for Vectra Detect.
-
Click Add instance to create and configure a new integration instance.
Parameter Description Required Vectra Detect FQDN or IP Enter the FQDN or IP to reach the Vectra Detect API. (e.g. “my-vectra-box.local” or “192.168.1.1”) True Authentication Type Select the authentication type (Default - API Token). False API Token Enter the API token that can be retrieved from the Vectra UI > My Profile > General (tab) > API Token. You can also use the XSOAR credentials wallet to store it. In that case, the token should be the password. False Client ID and Client Secret Key Enter the Client ID and Client Secret Key that can be retrieved from the Vectra UI > Manage > API Clients. You can also use the XSOAR credentials wallet to store it. False Trust any certificate (not secure) When checked, no SSL certificates check will be done when interacting with the Vectra Detect API. It’s insecure. (Default - unchecked) False Use system proxy settings Use the system proxy settings to reach with the Vectra Detect API. False Fetch incidents False Incident type False First fetch timestamp The date or relative timestamp from which to begin fetching entities.
Supported formats: 2 minutes, 2 hours, 2 days, 2 weeks, 2 months, 2 years, yyyy-mm-dd, yyyy-mm-ddTHH:MM:SSZ.
For example: 01 May 2024, 01 Aug 2024 04:45:33, 2024-07-17T14:05:44Z. (default - 7 days)False Mirroring Direction The mirroring direction in which to mirror the account and host. You can mirror “Incoming” (from Vectra to Cortex XSOAR), “Outgoing” (from Cortex XSOAR to Vectra), or in both directions. False Re-Fetch closed incidents via mirroring If selected, new incidents will be created (via Outgoing Mirroring). If not selected, it reopens previously closed incidents (via Incoming Mirroring).
Note: This flow is triggered only when the relevant account or host is still active and the previously fetched incident is closed.False Mirror tag for notes The tag value should be used to mirror the account and host note by adding the same tag in the notes. False Entity types to fetch Choose what to fetch - Accounts and/or Hosts and/or Detections. (Default - Accounts,Hosts) False Tags Only Accounts or Hosts that contain any of the tags specified will be fetched.
Note: For the partial match of the tag, use ‘*’ at the start and end of word (Only a single word is allowed). Ex. *MDR*.False Detection Category Filter the detections belonging to a specified category displayed as part of layout.
Note: This filter applies on the ‘Vectra Account’ and ‘Vectra Host’ incident type.False Detection Type Filter the detections belonging to a specified type displayed as part of layout.
Note: This filter applies on the ‘Vectra Account’ and ‘Vectra Host’ incident type.Hosts fetch query Only “active” Hosts matching this fetch query will be fetched. Will be used only if “Hosts” is selected in the “Entity types to fetch”. (default - host.threat:>=50) False Accounts fetch query Only “active” Accounts matching this fetch query will be fetched. Will be used only if “Accounts” is selected in the “Entity types to fetch”. (default - account.threat:>=50) False Detections fetch query Only “active” Detections matching this fetch query will be fetched. Will be used only if “Detections” is selected in the “Entity types to fetch”. (default - detection.threat:>=50 AND detection.certainty:>=50) False Max created incidents per fetch The maximum number of new incidents to create per fetch. This value would be split between selected “Entity types to fetch”. If the value is greater than 200, it will be considered as 200. The maximum is 200. (Default - 50) False Advanced: Minutes to look back when fetching Use this parameter to determine how long backward to look in the search for incidents that were created before the last run time and did not match the query when they were created. False Advanced: Fetch escalated Accounts and Hosts Use this parameter to fetch escalated Accounts and Hosts.
Note: If this parameter is set to True, it will fetch Accounts and Hosts that have been updated based on the provided filter parameters, even if their detection timestamps are older than the first fetch time.False Re-Fetch Account/Host entity and Detection for new activity/event Whether to create multiple incidents for the same entity (Account or Host) or Detection if new detection activity occurs for the entity (Account or Host) or a new event is detected for the detection. False - Click Test to validate the URLs, token, and connection.
Configuration for fetching Vectra Account or Vectra Host as a Cortex XSOAR incident
To fetch Vectra Account or Vectra Host as a Cortex XSOAR incident:
- Select Fetches incidents.
- Under Classifier, select “Vectra Detect”.
- Under Incident type, select “N/A”.
- Under Mapper (incoming), select “Vectra Detect - Incoming Mapper” for default mapping.
- Select the “Authentication Type” (Default - API Token).
- Enter connection parameters. (Vectra Detect FQDN or IP, API Token or Client ID and Client Secret Key)
- Select SSL certificate validation and Proxy if required.
- Update “Max created incidents per fetch” & “First fetch timestamp” based on your requirements.
- Select the Incident Mirroring Direction:
- Incoming - Mirrors changes from the Vectra into the Cortex XSOAR incident.
- Outgoing - Mirrors changes from the Cortex XSOAR incident to the Vectra.
- Incoming And Outgoing - Mirrors changes both Incoming and Outgoing directions on incidents.
- Check the “Re-Fetch closed incidents via mirroring” option if you want to prevent reopening of closed incidents and refetch them via mirroring on modification of the Vectra Account or Vectra Host.
- Enter the relevant tag name for mirror notes.
Note: This value is mapped to the dbotMirrorTags incident field in Cortex XSOAR, which defines how Cortex XSOAR handles notes when you tag them in the War Room. This is required for mirroring notes from Cortex XSOAR to Vectra. - Provide the filter parameter “Tags”, to filter entities by specific tag/s for fetch type account and host.
- Provide the filter parameter “Detection Category” and “Detection Type”, to filter detections by the specified category and type for fetch type account and host.
- Provide the “Minutes to look back when fetching” parameter to determine how long backward to look in the search for incidents that were created before the last run time and did not match the query when they were created.
- Check the “Fetch escalated Accounts and Hosts” option to fetch escalated Accounts and Hosts that have been updated based on the provided filter parameters, even if their detection timestamps are older than the first fetch time.
- Check the “Re-Fetch Account/Host entity and Detection for new activity/event” option if you want multiple Cortex XSOAR incidents to be created for the same Entity (Account or Host) or Detection whenever new detection activity is recorded. Disable this option if you want only one incident per Entity (Account or Host) or Detection regardless of ongoing activity.
Notes for mirroring:
- The mirroring is strictly tied to incident types “Vectra Account” and “Vectra Host”, as well as the incoming mapper “Vectra Detect - Incoming Mapper”. If you want to change or use a custom incident type/mapper, ensure that related changes are also present.
- The mirroring settings apply only for incidents that are fetched after applying the settings.
- Any tags removed from the Vectra Account or Vectra Host will not be removed in the Cortex XSOAR incident, as Cortex XSOAR doesn’t allow the removal of the tags field via the backend. However, tags removed from the Cortex XSOAR incident UI will be removed from the Vectra Account or Vectra Host.
- New notes from the Cortex XSOAR incident will be created as notes in the Vectra Account or Vectra Host. Updates to existing notes in the Cortex XSOAR incident will not be reflected in the Vectra Account or Vectra Host.
- New notes from the Vectra Account or Vectra Host will be created as notes in the Cortex XSOAR incident. Updates to existing notes in the Vectra Account or Vectra Host will create new notes in the Cortex XSOAR incident.
- If a closed Cortex XSOAR incident is tied to a specific Vectra Account or Vectra Host and new detections for that Vectra Account or Vectra Host arise or existing detections become active again:
- If “Re-Fetch closed Incidents while Mirroring” checkbox is not selected and “Incoming Mirroring” is enabled, the incident will be automatically reopened.
- If “Re-Fetch closed Incidents while Mirroring” checkbox is selected and “Outgoing Mirroring” is enabled, a new incident will be created for the Vectra Account or Vectra Host.
- When a Cortex XSOAR incident is closed but there are still active detections on the Vectra side, and the Vectra Account or Vectra Host is subsequently updated:
- If “Re-Fetch closed Incidents while Mirroring” checkbox is not selected and “Incoming Mirroring” is enabled, the corresponding XSOAR incident for that Vectra Account or Vectra Host will be reopened.
- If “Re-Fetch closed Incidents while Mirroring” checkbox is selected and “Outgoing Mirroring” is enabled, a new incident will be created for the Vectra Account or Vectra Host.
- If a Cortex XSOAR incident is reopened and the corresponding entity has an assignment in Vectra, the assignment will be removed from Vectra.
- If you want to use the mirror mechanism and you’re using custom mappers, then the incoming mapper must contain the following fields: dbotMirrorDirection, dbotMirrorId, dbotMirrorInstance, and dbotMirrorTags.
- To use a custom mapper, you must first duplicate the mapper and update the fields in the copy of the mapper. (Refer to the “Create a custom mapper consisting of the default Vectra Detect - Incoming Mapper” section for more information.)
- Following new fields are introduced in the response of the incident to enable the mirroring:
- mirror_direction: This field determines the mirroring direction for the incident. It is a required field for Cortex XSOAR to enable mirroring support.
- mirror_tags: This field determines what would be the tag needed to mirror the Cortex XSOAR entry out to Vectra. It is a required field for XSOAR to enable mirroring support.
- mirror_instance: This field determines from which instance the Cortex XSOAR incident was created. It is a required field for Cortex XSOAR to enable mirroring support.
Cleanup Duplicate Incidents
- Use the Close All Duplicate XSOAR Incidents - Vectra Detect playbook to clean up duplicate incidents. You can use VectraDetectCloseDuplicateIncidents script individually to clean up duplicate incidents.
- You can also schedule a job with Close All Duplicate XSOAR Incidents - Vectra Detect playbook in Cortex XSOAR to clean up incidents periodically. Refer to this Cortex XSOAR documentation for more information.
Commands
You can execute these commands from the Cortex XSOAR 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.
vectra-search-accounts
Returns a list of Account objects. All search attributes will be cumulative unless you’re using the search_query_only one, in that case, only this one will be taken into account.
Base Command
vectra-search-accounts
Input
| Argument Name | Description | Required |
|---|---|---|
| min_id | Returns Accounts with an ID greater than or equal to the specified ID. | Optional |
| max_id | Returns Accounts with an ID less than or equal to the specified ID. | Optional |
| min_threat | Returns Accounts with a threat score greater than or equal to the specified score. | Optional |
| max_threat | Returns Accounts with a threat score less than or equal to the specified score. | Optional |
| min_certainty | Returns Accounts with a certainty score greater than or equal to the specified score. | Optional |
| max_certainty | Returns Accounts with a certainty score less than or equal to the specified score. | Optional |
| state | Filters by state (‘active’, ‘inactive’). Possible values are: active, inactive. | Optional |
| search_query | Search query in Lucene query syntax. | Optional |
| search_query_only | Use specifically this search query. Compared to “search_query” where default arguments are appended. | Optional |
| min_privilege_level | Returns entries with a privilege level greater than or equal to the specified score. | Optional |
| max_privilege_level | Returns entries with a privilege level greater than or equal to the specified score. | Optional |
| privilege_category | Filters by the privilege category (“low”, “medium”, “high”) provided. | Optional |
| tags | Filters by a tag or a comma-separated list tags. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Account.Assignee | String | Vectra user account this Account is assigned to |
| Vectra.Account.AssignedDate | String | Assignment date |
| Vectra.Account.CertaintyScore | Number | Account certainty score |
| Vectra.Account.ID | Number | Account ID (unique) |
| Vectra.Account.LastDetectionTimestamp | String | Last time a detection linked to this account has been seen |
| Vectra.Account.PrivilegeLevel | Number | Account privilege level (from 1 to 10) |
| Vectra.Account.PrivilegeCategory | String | Account privilege category (Either ‘Low’, ‘Medium’ or ‘High’ - Privilege levels of 1-2 > ‘Low’, 3-7 > ‘Medium’, 8-10 > ‘High’) |
| Vectra.Account.Severity | String | Account severity according to scores (‘Low’, ‘Medium’, ‘High’, ‘Critical’) |
| Vectra.Account.State | String | Account state (‘active’, ‘inactive’) |
| Vectra.Account.Tags | String | Account tags |
| Vectra.Account.ThreatScore | Number | Account threat score |
| Vectra.Account.Type | String | Account type (‘kerberos’ or ‘o365’) |
| Vectra.Account.URL | String | Account URL to pivot to Vectra UI |
| Vectra.Account.Name | String | The username of the account |
vectra-search-hosts
Returns a list of Host objects. All search attributes will be cumulative unless you’re using the search_query_only one, in that case, only this one will be taken into account.
Base Command
vectra-search-hosts
Input
| Argument Name | Description | Required |
|---|---|---|
| min_id | Returns Hosts with an ID greater than or equal to the specified ID. | Optional |
| max_id | Returns Hosts with an ID less than or equal to the specified ID. | Optional |
| min_threat | Returns Hosts with a threat score greater than or equal to the specified score. | Optional |
| max_threat | Returns Hosts with a threat score less than or equal to the specified score. | Optional |
| min_certainty | Returns Hosts with a certainty score greater than or equal to the specified score. | Optional |
| max_certainty | Returns Hosts with a certainty score less than or equal to the specified score. | Optional |
| state | Filters by state (‘active’, ‘inactive’). Possible values are: active, inactive. | Optional |
| search_query | Search query in Lucene query syntax. | Optional |
| search_query_only | Use specifically this search query. Compared to “search_query” where default arguments are appended. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Host.Assignee | String | Vectra user account this Host is assigned to |
| Vectra.Host.AssignedDate | String | Assignment date |
| Vectra.Host.CertaintyScore | Number | Host certainty score |
| Vectra.Host.HasActiveTraffic | Boolean | Whether this Host has active traffic |
| Vectra.Host.Hostname | String | Host name |
| Vectra.Host.ID | Number | Host ID (Unique) |
| Vectra.Host.IP | String | Host IP address |
| Vectra.Host.IsKeyAsset | Boolean | Whether this Host is seen as a key asset |
| Vectra.Host.IsTargetingKeyAsset | Boolean | Whether this Host is targeting a key asset |
| Vectra.Host.PrivilegeLevel | Number | Host privilege level (from 1 to 10) |
| Vectra.Host.PrivilegeCategory | String | Host privilege category. (Either ‘Low’, ‘Medium’ or ‘High’ - Privilege levels of 1-2 > ‘Low’, 3-7 > ‘Medium’, 8-10 > ‘High’) |
| Vectra.Host.ProbableOwner | String | Host probable owner |
| Vectra.Host.SensorLUID | String | Sensor LUID that saw this Host |
| Vectra.Host.SensorName | String | Sensor Name that saw this Host |
| Vectra.Host.Sensor | String | Sensor details that have seen this Host |
| Vectra.Host.Severity | String | Host severity according to scores (‘Low’, ‘Medium’, ‘High’, ‘Critical’) |
| Vectra.Host.State | String | Host state (‘active’, ‘inactive’) |
| Vectra.Host.Tags | String | Host tags |
| Vectra.Host.ThreatScore | Number | Host threat score |
| Vectra.Host.URL | String | Host URL to pivot to Vectra UI |
vectra-search-detections
Returns a list of Detection objects. All search attributes will be cumulative unless you’re using the search_query_only one, in that case, only this one will be taken into account.
Base Command
vectra-search-detections
Input
| Argument Name | Description | Required |
|---|---|---|
| min_id | Returns Detections with an ID greater than or equal to the specified ID. | Optional |
| max_id | Returns Detections with an ID less than or equal to the specified ID. | Optional |
| min_threat | Returns Detections with a threat score greater than or equal to the specified score. | Optional |
| max_threat | Returns Detections with a threat score less than or equal to the specified score. | Optional |
| min_certainty | Returns Detections with a certainty score greater than or equal to the specified score. | Optional |
| max_certainty | Returns Detections with a certainty score less than or equal to the specified score. | Optional |
| state | Filters by state (‘active’, ‘inactive’). Possible values are: active, inactive. | Optional |
| search_query | Search query in Lucene query syntax. | Optional |
| search_query_only | Use specifically this search query. Compared to “search_query” where default arguments are appended. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Detection.Assignee | String | Vectra user account this detection is assigned to |
| Vectra.Detection.AssignedDate | String | Assignment date |
| Vectra.Detection.Category | String | Detection category (Lateral, Exfil, …) |
| Vectra.Detection.CertaintyScore | Number | Detection certainty score |
| Vectra.Detection.Description | String | Detection description |
| Vectra.Detection.DestinationIPs | String | Detection destination IPs |
| Vectra.Detection.DestinationPorts | String | Detection destination ports |
| Vectra.Detection.FirstTimestamp | String | First time this detection has been seen |
| Vectra.Detection.ID | Number | Detection ID (unique) |
| Vectra.Detection.IsTargetingKeyAsset | Boolean | Whether this detection is targeting a key asset |
| Vectra.Detection.LastTimestamp | String | Last time this detection has been seen |
| Vectra.Detection.Name | String | The name of the detection. Would be a user defined name if this detection is triaged or the default type name instead |
| Vectra.Detection.Severity | String | Detection severity according to scores (‘Low’, ‘Medium’, ‘High’, ‘Critical’) |
| Vectra.Detection.SensorLUID | String | Sensor LUID that saw this detection |
| Vectra.Detection.SensorName | String | Sensor name that saw this detection. |
| Vectra.Detection.SourceAccountID | String | Account ID relating to this detection |
| Vectra.Detection.SourceHostID | String | Host ID relating to this detection |
| Vectra.Detection.SourceIP | String | Source IP relating to this detection |
| Vectra.Detection.State | String | Detection state (‘active’, ‘inactive’) |
| Vectra.Detection.Tags | String | Detection tags |
| Vectra.Detection.ThreatScore | Number | Detection threat score |
| Vectra.Detection.TriageRuleID | String | Triage rule ID related to this detection |
| Vectra.Detection.Type | String | Detection type (Brute Force, Port Sweep, …) |
| Vectra.Detection.URL | String | Detection URL to pivot to Vectra UI |
vectra-search-assignments
Return a list of assignments. By default already resolved assignment are not returned.
Base Command
vectra-search-assignments
Input
| Argument Name | Description | Required |
|---|---|---|
| account_ids | Filters by accounts IDs. | Optional |
| assignee_ids | Filters by assignees IDs. | Optional |
| host_ids | Filters by hosts IDs. | Optional |
| outcome_ids | Filters by outcomes IDs. | Optional |
| resolved | Filters by resolution state. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Assignment.AccountID | Number | Account ID this assignment is linked to |
| Vectra.Assignment.AssignedBy | String | Who lastly assigned this assignment |
| Vectra.Assignment.AssignedDate | String | When this assignment was lastly assigned |
| Vectra.Assignment.AssignedTo | String | To who this assignment is assigned |
| Vectra.Assignment.HostID | String | Host ID this assignment is linked to |
| Vectra.Assignment.ID | Number | Assignment ID (unique) |
| Vectra.Assignment.IsResolved | Boolean | Is this assignment resolved |
| Vectra.Assignment.OutcomeCategory | String | Assignment Outcome category |
| Vectra.Assignment.OutcomeTitle | String | Assignment Outcome title |
| Vectra.Assignment.TriagedDetections | String | List of Detection that have been triaged with the resolution |
| Vectra.Assignment.TriagedAs | String | Name of the triage rule if any |
| Vectra.Assignment.ResolvedBy | String | Who resolved this assignment |
| Vectra.Assignment.ResolvedDate | string | When this assignment was resolved |
vectra-search-users
Returns a list of Vectra Users. All search attributes will be cumulative.
Base Command
vectra-search-users
Input
| Argument Name | Description | Required |
|---|---|---|
| username | Filters by user name. | Optional |
| role | Filters by user role. | Optional |
| type | Filters by type (‘Local’, ‘SAML’, …). Possible values are: local, SAML. | Optional |
| last_login_datetime | Filters for Users that logged in since the given datetime. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.User.Email | String | User’s email address |
| Vectra.User.ID | Number | User ID (unique) |
| Vectra.User.Role | String | User’s role |
| Vectra.User.Type | String | User type (‘Local’, ‘SAML’, …) |
| Vectra.User.Username | String | Username |
| Vectra.User.LastLoginDate | String | User’s last login datetime |
vectra-search-outcomes
Returns a list of assignment outcomes.
Base Command
vectra-search-outcomes
Input
There are no input arguments for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Outcome.IsBuiltIn | String | Is this Outcome a builtin Outcome |
| Vectra.Outcome.Category | String | Outcome’s category (‘False Positive’, ‘Benign True Positive’, ‘Malicious True Positive’) |
| Vectra.Outcome.ID | Number | Outcome ID (unique) |
| Vectra.Outcome.Title | String | Outcome title |
vectra-account-describe
Returns a single Account details
Base Command
vectra-account-describe
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Account ID you want to get details on. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Account.Assignee | String | Vectra user account this Account is assigned to |
| Vectra.Account.AssignedDate | String | Assignment date |
| Vectra.Account.CertaintyScore | Number | Account certainty score |
| Vectra.Account.ID | Number | Account ID (unique) |
| Vectra.Account.LastDetectionTimestamp | String | Last time a detection linked to this account has been seen |
| Vectra.Account.PrivilegeLevel | Number | Account privilege level (from 1 to 10) |
| Vectra.Account.PrivilegeCategory | String | Account privilege category (Either ‘Low’, ‘Medium’ or ‘High’ - Privilege levels of 1-2 > ‘Low’, 3-7 > ‘Medium’, 8-10 > ‘High’) |
| Vectra.Account.Severity | String | Account severity according to scores (‘Low’, ‘Medium’, ‘High’, ‘Critical’) |
| Vectra.Account.State | String | Account state (‘active’, ‘inactive’) |
| Vectra.Account.Tags | String | Account tags |
| Vectra.Account.ThreatScore | Number | Account threat score |
| Vectra.Account.Type | String | Account type (‘kerberos’ or ‘o365’) |
| Vectra.Account.URL | String | Account URL to pivot to Vectra UI |
| Vectra.Account.Name | String | The username of the account |
vectra-account-add-tags
Add tags to an Account
Base Command
vectra-account-add-tags
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Account ID you want to add tags on. | Optional |
| tags | The tags list (comma separated). | Optional |
Context Output
There is no context output for this command.
vectra-account-del-tags
Delete tags from an Account
Base Command
vectra-account-del-tags
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Account ID you want to del tags from. | Optional |
| tags | The tags list (comma separated). | Optional |
Context Output
There is no context output for this command.
vectra-account-tag-list
Returns a list of tags for a specified account.
Base Command
vectra-account-tag-list
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Specify the ID of the account. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Account.ID | Number | ID of the account associated with the tags. |
| Vectra.Account.Tags | String | Tags associated to the account. |
Command example
!vectra-account-tag-list id="2"
Context Example
{
"Vectra.Account": {
"ID": 2,
"Tags": [
"note",
"tag_from_xsoar",
"tag_from_vectra"
]
}
}
Human Readable Output
List of tags: note, tag_from_xsoar, tag_from_vectra
vectra-account-note-add
Add a note to the account.
Base Command
vectra-account-note-add
Input
| Argument Name | Description | Required |
|---|---|---|
| account_id | Specify the ID of the account. | Required |
| note | Note to be added in the specified account_id. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Account.Notes.account_id | Number | ID of the account associated with the note. |
| Vectra.Account.Notes.note_id | Number | ID of the note. |
| Vectra.Account.Notes.date_created | Date | Date when the note was created. |
| Vectra.Account.Notes.date_modified | Date | Date when the note was last modified. |
| Vectra.Account.Notes.created_by | String | User who created the note. |
| Vectra.Account.Notes.modified_by | String | User who last modified the note. |
| Vectra.Account.Notes.note | String | Content of the note. |
Command example
!vectra-account-note-add account_id="2" note="test note"
Context Example
{
"Vectra.Account.Notes": {
"date_created": "2024-07-10T07:30:58.574942Z",
"created_by": "xsoar",
"note": "test note",
"note_id": 1959,
"account_id": 2
}
}
Human Readable Output
The note has been successfully added to the account
Returned Note ID: 1959
vectra-account-note-update
Update a note in the account.
Base Command
vectra-account-note-update
Input
| Argument Name | Description | Required |
|---|---|---|
| account_id | Specify the ID of the account. | Required |
| note_id | Specify the ID of the note. Note: Use the vectra-account-note-list command to get note_id. |
Required |
| note | Note to be updated for the specified note_id. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Account.Notes.account_id | Number | ID of the account associated with the note. |
| Vectra.Account.Notes.note_id | Number | ID of the note. |
| Vectra.Account.Notes.date_created | Date | Date when the note was created. |
| Vectra.Account.Notes.date_modified | Date | Date when the note was last modified. |
| Vectra.Account.Notes.created_by | String | User who created the note. |
| Vectra.Account.Notes.modified_by | String | User who last modified the note. |
| Vectra.Account.Notes.note | String | Content of the note. |
Command example
!vectra-account-note-update account_id="2" note_id="1959" note="updated test note"
Context Example
{
"Vectra.Account.Notes": {
"date_created": "2024-07-10T07:30:58.574942Z",
"date_modified": "2024-07-12T06:42:29.546835Z",
"created_by": "xsoar",
"modified_by": "xsoar",
"note": "updated test note",
"note_id": 1959,
"account_id": 2
}
}
Human Readable Output
The note has been successfully updated in the account
vectra-account-note-remove
Remove a note from the account.
Base Command
vectra-account-note-remove
Input
| Argument Name | Description | Required |
|---|---|---|
| account_id | Specify the ID of the account. | Required |
| note_id | Specify the ID of the note. Note: Use the vectra-account-note-list command to get note_id. |
Required |
Context Output
There is no context output for this command.
Command example
!vectra-account-note-remove account_id="2" note_id="1959"
Human Readable Output
The note has been successfully removed from the account
vectra-account-note-list
List all notes of the specific account.
Base Command
vectra-account-note-list
Input
| Argument Name | Description | Required |
|---|---|---|
| account_id | Specify the ID of the account. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Account.Notes.account_id | Number | ID of the account associated with the note. |
| Vectra.Account.Notes.note_id | Number | ID of the note. |
| Vectra.Account.Notes.date_created | Date | Date when the note was created. |
| Vectra.Account.Notes.date_modified | Date | Date when the note was last modified. |
| Vectra.Account.Notes.created_by | String | User who created the note. |
| Vectra.Account.Notes.modified_by | String | User who last modified the note. |
| Vectra.Account.Notes.note | String | Content of the note. |
Command example
!vectra-account-note-list account_id="2"
Context Example
{
"Vectra.Account.Notes": [
{
"date_created": "2024-07-10T05:40:31Z",
"date_modified": "2024-07-16T12:56:30Z",
"created_by": "xsoar",
"modified_by": "xsoar",
"note": "updated_note",
"note_id": 1959,
"account_id": 2
},
{
"date_created": "2024-07-08T07:11:49Z",
"created_by": "xsoar",
"note": "Here comes your note TEST",
"note_id": 1906,
"account_id": 2
}
]
}
Human Readable Output
Notes Table
Note ID Note Created By Created Date Modified By Modified Date 1959 updated_note xsoar 2024-07-10T05:40:31Z xsoar 2024-07-16T12:56:30Z 1906 Here comes your note TEST xsoar 2024-07-08T07:11:49Z
vectra-account-markall-detections-asfixed
Mark active detections as fixed by providing the ID of the account in the argument.
Base Command
vectra-account-markall-detections-asfixed
Input
| Argument Name | Description | Required |
|---|---|---|
| account_id | Provide an account ID. | Required |
Context Output
There is no context output for this command.
Command example
!vectra-account-markall-detections-asfixed account_id=109
Human Readable Output
The active detections of the provided account have been successfully marked as fixed.
vectra-account-markall-detections-asclosed
Mark active detections as closed by providing the ID of the account in the argument.
Base Command
vectra-account-markall-detections-asclosed
Input
| Argument Name | Description | Required |
|---|---|---|
| account_id | Provide an account ID. | Required |
| close_reason | Provide the close reason. Possible values are: benign, remediated. | Required |
Context Output
There is no context output for this command.
Command example
!vectra-account-markall-detections-asclosed account_id=108 close_reason=benign
Human Readable Output
The active detections of the provided account have been successfully closed as benign
vectra-host-describe
Returns a single Host details
Base Command
vectra-host-describe
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Host ID you want to get details on. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Host.Assignee | String | Vectra user account this Host is assigned to |
| Vectra.Host.AssignedDate | String | Assignment date |
| Vectra.Host.CertaintyScore | Number | Host certainty score |
| Vectra.Host.HasActiveTraffic | Boolean | Whether this Host has active traffic |
| Vectra.Host.Hostname | String | Host name |
| Vectra.Host.ID | Number | Host ID (Unique) |
| Vectra.Host.IP | String | Host IP address |
| Vectra.Host.IsKeyAsset | Boolean | Whether this Host is seen as a key asset |
| Vectra.Host.IsTargetingKeyAsset | Boolean | Whether this Host is targeting a key asset |
| Vectra.Host.PrivilegeLevel | Number | Host privilege level (from 1 to 10) |
| Vectra.Host.PrivilegeCategory | String | Host privilege category. (Either ‘Low’, ‘Medium’ or ‘High’ - Privilege levels of 1-2 > ‘Low’, 3-7 > ‘Medium’, 8-10 > ‘High’) |
| Vectra.Host.ProbableOwner | String | Host probable owner |
| Vectra.Host.SensorLUID | String | Sensor LUID that saw this Host |
| Vectra.Host.SensorName | String | Sensor Name that saw this Host |
| Vectra.Host.Sensor | String | Sensor details that have seen this Host |
| Vectra.Host.Severity | String | Host severity according to scores (‘Low’, ‘Medium’, ‘High’, ‘Critical’) |
| Vectra.Host.State | String | Host state (‘active’, ‘inactive’) |
| Vectra.Host.Tags | String | Host tags |
| Vectra.Host.ThreatScore | Number | Host threat score |
| Vectra.Host.URL | String | Host URL to pivot to Vectra UI |
vectra-host-add-tags
Add tags to an Host
Base Command
vectra-host-add-tags
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Host ID you want to add tags on. | Optional |
| tags | The tags list (comma separated). | Optional |
Context Output
There is no context output for this command.
vectra-host-del-tags
Delete tags from an Host
Base Command
vectra-host-del-tags
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Host ID you want to del tags from. | Optional |
| tags | The tags list (comma separated). | Optional |
Context Output
There is no context output for this command.
vectra-host-tag-list
Returns a list of tags for a specified host.
Base Command
vectra-host-tag-list
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Specify the ID of the host. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Host.ID | Number | ID of the host associated with the tags. |
| Vectra.Host.Tags | String | Tags associated to the host. |
Command example
!vectra-host-tag-list id="2"
Context Example
{
"Vectra.Host": {
"ID": 2,
"Tags": [
"note",
"tag_from_xsoar",
"tag_from_vectra"
]
}
}
Human Readable Output
List of tags: note, tag_from_xsoar, tag_from_vectra
vectra-host-note-add
Add a note to the host.
Base Command
vectra-host-note-add
Input
| Argument Name | Description | Required |
|---|---|---|
| host_id | Specify the ID of the host. | Required |
| note | Note to be added in the specified host_id. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Host.Notes.host_id | Number | ID of the host associated with the note. |
| Vectra.Host.Notes.note_id | Number | ID of the note. |
| Vectra.Host.Notes.date_created | Date | Date when the note was created. |
| Vectra.Host.Notes.date_modified | Date | Date when the note was last modified. |
| Vectra.Host.Notes.created_by | String | User who created the note. |
| Vectra.Host.Notes.modified_by | String | User who last modified the note. |
| Vectra.Host.Notes.note | String | Content of the note. |
Command example
!vectra-host-note-add host_id="5" note="test note"
Context Example
{
"Vectra.Host.Notes": {
"date_created": "2024-07-10T07:31:58.574942Z",
"created_by": "xsoar",
"note": "test note",
"note_id": 1960,
"host_id": 5
}
}
Human Readable Output
The note has been successfully added to the host
Returned Note ID: 1960
vectra-host-note-update
Update a note in the host.
Base Command
vectra-host-note-update
Input
| Argument Name | Description | Required |
|---|---|---|
| host_id | Specify the ID of the host. | Required |
| note_id | Specify the ID of the note. Note: Use the vectra-host-note-list command to get note_id. |
Required |
| note | Note to be updated for the specified note_id. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Host.Notes.host_id | Number | ID of the host associated with the note. |
| Vectra.Host.Notes.note_id | Number | ID of the note. |
| Vectra.Host.Notes.date_created | Date | Date when the note was created. |
| Vectra.Host.Notes.date_modified | Date | Date when the note was last modified. |
| Vectra.Host.Notes.created_by | String | User who created the note. |
| Vectra.Host.Notes.modified_by | String | User who last modified the note. |
| Vectra.Host.Notes.note | String | Content of the note. |
Command example
!vectra-account-note-update host_id="7" note_id="1960" note="updated test note"
Context Example
{
"Vectra.Host.Notes": {
"date_created": "2024-07-10T07:31:58.574942Z",
"date_modified": "2024-07-12T06:44:29.546835Z",
"created_by": "xsoar",
"modified_by": "xsoar",
"note": "updated test note",
"note_id": 1960,
"host_id": 7
}
}
Human Readable Output
The note has been successfully updated in the host
vectra-host-note-remove
Remove a note from the host.
Base Command
vectra-host-note-remove
Input
| Argument Name | Description | Required |
|---|---|---|
| host_id | Specify the ID of the host. | Required |
| note_id | Specify the ID of the note. Note: Use the vectra-host-note-list command to get note_id. |
Required |
Context Output
There is no context output for this command.
Command example
!vectra-host-note-remove host_id="7" note_id="1960"
Human Readable Output
The note has been successfully removed from the host
vectra-host-note-list
List all notes of the specific host.
Base Command
vectra-host-note-list
Input
| Argument Name | Description | Required |
|---|---|---|
| host_id | Specify the ID of the host. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Host.Notes.host_id | Number | ID of the host associated with the note. |
| Vectra.Host.Notes.note_id | Number | ID of the note. |
| Vectra.Host.Notes.date_created | Date | Date when the note was created. |
| Vectra.Host.Notes.date_modified | Date | Date when the note was last modified. |
| Vectra.Host.Notes.created_by | String | User who created the note. |
| Vectra.Host.Notes.modified_by | String | User who last modified the note. |
| Vectra.Host.Notes.note | String | Content of the note. |
Command example
!vectra-host-note-list host_id="7"
Context Example
{
"Vectra.Host.Notes": [
{
"date_created": "2024-07-11T07:32:31Z",
"created_by": "xsoar",
"note": "test note",
"note_id": 1960,
"host_id": 7
},
{
"date_created": "2024-07-11T06:23:07Z",
"created_by": "cds_xsoar",
"note": "test note",
"note_id": 1982,
"host_id": 7
}
]
}
Human Readable Output
Notes Table
Note ID Note Created By Created Date 1960 test note xsoar 2024-07-11T07:32:31Z 1982 test note cds_xsoar 2024-07-11T06:23:07Z
vectra-host-markall-detections-asfixed
Mark active detections as fixed by providing ID of the host in the argument.
Base Command
vectra-host-markall-detections-asfixed
Input
| Argument Name | Description | Required |
|---|---|---|
| host_id | Provide a host ID. | Required |
Context Output
There is no context output for this command.
Command example
!vectra-host-markall-detections-asfixed host_id=23176
Human Readable Output
The active detections of the provided host have been successfully marked as fixed.
vectra-host-markall-detections-asclosed
Mark active detections as closed by providing the ID of the host in the argument.
Base Command
vectra-host-markall-detections-asclosed
Input
| Argument Name | Description | Required |
|---|---|---|
| host_id | Provide a host ID. | Required |
| close_reason | Provide the close reason. Possible values are: benign, remediated. | Required |
Context Output
There is no context output for this command.
Command example
!vectra-host-markall-detections-asclosed host_id=23176 close_reason=remediated
Human Readable Output
The active detections of the provided host have been successfully closed as remediated
vectra-detection-describe
Returns a single detection details
Base Command
vectra-detection-describe
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Detection ID you want to get details on. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Detection.Assignee | String | Vectra user account this detection is assigned to |
| Vectra.Detection.AssignedDate | String | Assignment date |
| Vectra.Detection.Category | String | Detection category (Lateral, Exfil, …) |
| Vectra.Detection.CertaintyScore | Number | Detection certainty score |
| Vectra.Detection.Description | String | Detection description |
| Vectra.Detection.DestinationIPs | String | Detection destination IPs |
| Vectra.Detection.DestinationPorts | String | Detection destination ports |
| Vectra.Detection.FirstTimestamp | String | First time this detection has been seen |
| Vectra.Detection.ID | Number | Detection ID (unique) |
| Vectra.Detection.IsTargetingKeyAsset | Boolean | Whether this detection is targeting a key asset |
| Vectra.Detection.LastTimestamp | String | Last time this detection has been seen |
| Vectra.Detection.Name | String | The name of the detection. Would be a user defined name if this detection is triaged or the default type name instead |
| Vectra.Detection.Severity | String | Detection severity according to scores (‘Low’, ‘Medium’, ‘High’, ‘Critical’) |
| Vectra.Detection.SensorLUID | String | Sensor LUID that saw this detection |
| Vectra.Detection.SensorName | String | Sensor name that saw this detection. |
| Vectra.Detection.SourceAccountID | String | Account ID relating to this detection |
| Vectra.Detection.SourceHostID | String | Host ID relating to this detection |
| Vectra.Detection.SourceIP | String | Source IP relating to this detection |
| Vectra.Detection.State | String | Detection state (‘active’, ‘inactive’) |
| Vectra.Detection.Tags | String | Detection tags |
| Vectra.Detection.ThreatScore | Number | Detection threat score |
| Vectra.Detection.TriageRuleID | String | Triage rule ID related to this detection |
| Vectra.Detection.Type | String | Detection type (Brute Force, Port Sweep, …) |
| Vectra.Detection.URL | String | Detection URL to pivot to Vectra UI |
vectra-detection-get-pcap
Returns a Detection’s PCAP file (if available)
Base Command
vectra-detection-get-pcap
Input
| Argument Name | Description | Required |
|---|---|---|
| id | The Detection ID you want to get the PCAP file from. | Optional |
Context Output
There is no context output for this command.
vectra-detection-markasfixed
Marks/Unmarks a Detection as fixed by providing the Detection ID
Base Command
vectra-detection-markasfixed
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Detection ID you want to mark/unmark as fixed. | Optional |
| fixed | The wanted detection status (“true”, “false”). No default value. Possible values are: true, false. | Optional |
Context Output
There is no context output for this command.
vectra-detection-add-tags
Add tags to a Detection
Base Command
vectra-detection-add-tags
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Detection ID you want to add tags on. | Optional |
| tags | The tags list (comma separated). | Optional |
Context Output
There is no context output for this command.
vectra-detection-del-tags
Delete tags from a Detection
Base Command
vectra-detection-del-tags
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Detection ID you want to del tags from. | Optional |
| tags | The tags list (comma separated). | Optional |
Context Output
There is no context output for this command.
vectra-detection-tag-list
Returns a list of tags for a specified detection.
Base Command
vectra-detection-tag-list
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Specify the ID of the detection. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Detection.ID | Number | ID of the detection associated with the tags. |
| Vectra.Detection.Tags | String | Tags associated to the detection. |
Command example
!vectra-detection-tag-list id="2"
Context Example
{
"Vectra.Detection": {
"ID": 2,
"Tags": [
"note",
"tag_from_xsoar",
"tag_from_vectra"
]
}
}
Human Readable Output
List of tags: note, tag_from_xsoar, tag_from_vectra
vectra-detection-note-add
Add a note to the detection.
Base Command
vectra-detection-note-add
Input
| Argument Name | Description | Required |
|---|---|---|
| detection_id | Specify the ID of the detection. | Required |
| note | Note to be added in the specified detection_id. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Detection.Notes.detection_id | Number | ID of the detection associated with the note. |
| Vectra.Detection.Notes.note_id | Number | ID of the note. |
| Vectra.Detection.Notes.date_created | Date | Date when the note was created. |
| Vectra.Detection.Notes.date_modified | Date | Date when the note was last modified. |
| Vectra.Detection.Notes.created_by | String | User who created the note. |
| Vectra.Detection.Notes.modified_by | String | User who last modified the note. |
| Vectra.Detection.Notes.note | String | Content of the note. |
Command example
!vectra-detection-note-add detection_id="7" note="test note"
Context Example
{
"Vectra.Detection.Notes": {
"date_created": "2024-07-10T07:32:58.574942Z",
"created_by": "xsoar",
"note": "test note",
"note_id": 1961,
"detection_id": 7
}
}
Human Readable Output
The note has been successfully added to the detection
Returned Note ID: 1961
vectra-detection-note-update
Update a note in the detection.
Base Command
vectra-detection-note-update
Input
| Argument Name | Description | Required |
|---|---|---|
| detection_id | Specify the ID of the detection. | Required |
| note_id | Specify the ID of the note. Note: Use the vectra-detection-note-list command to get note_id. |
Required |
| note | Note to be updated for the specified note_id. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Detection.Notes.detection_id | Number | ID of the detection associated with the note. |
| Vectra.Detection.Notes.note_id | Number | ID of the note. |
| Vectra.Detection.Notes.date_created | Date | Date when the note was created. |
| Vectra.Detection.Notes.date_modified | Date | Date when the note was last modified. |
| Vectra.Detection.Notes.created_by | String | User who created the note. |
| Vectra.Detection.Notes.modified_by | String | User who last modified the note. |
| Vectra.Detection.Notes.note | String | Content of the note. |
Command example
!vectra-detection-note-update detection_id="9" note_id="1961" note="updated test note"
Context Example
{
"Vectra.Detection.Notes": {
"date_created": "2024-07-10T07:32:58.574942Z",
"date_modified": "2024-07-12T06:43:29.546835Z",
"created_by": "xsoar",
"modified_by": "xsoar",
"note": "updated test note",
"note_id": 1961,
"detection_id": 9
}
}
Human Readable Output
The note has been successfully updated in the detection
vectra-detection-note-remove
Remove a note from the detection.
Base Command
vectra-detection-note-remove
Input
| Argument Name | Description | Required |
|---|---|---|
| detection_id | Specify the ID of the detection. | Required |
| note_id | Specify the ID of the note. Note: Use the vectra-detection-note-list command to get note_id. |
Required |
Context Output
There is no context output for this command.
Command example
!vectra-detection-note-remove detection_id=97" note_id="1961"
Human Readable Output
The note has been successfully removed from the detection
vectra-detection-note-list
List all notes of the specific detection.
Base Command
vectra-detection-note-list
Input
| Argument Name | Description | Required |
|---|---|---|
| detection_id | Specify the ID of the detection. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Detection.Notes.detection_id | Number | ID of the detection associated with the note. |
| Vectra.Detection.Notes.note_id | Number | ID of the note. |
| Vectra.Detection.Notes.date_created | Date | Date when the note was created. |
| Vectra.Detection.Notes.date_modified | Date | Date when the note was last modified. |
| Vectra.Detection.Notes.created_by | String | User who created the note. |
| Vectra.Detection.Notes.modified_by | String | User who last modified the note. |
| Vectra.Detection.Notes.note | String | Content of the note. |
Command example
!vectra-detection-note-list detection_id="9"
Context Example
{
"Vectra.Detection.Notes": [
{
"date_created": "2024-07-12T04:52:20Z",
"date_modified": "2024-07-12T10:21:03Z",
"created_by": "xsoar",
"modified_by": "xsoar",
"note": "updated note 2nd",
"note_id": 1961,
"detection_id": 9
},
{
"date_created": "2024-07-11T07:32:20Z",
"created_by": "xsoar",
"note": "your first test note",
"note_id": 1937,
"detection_id": 9
}
]
}
Human Readable Output
Notes Table
Note ID Note Created By Created Date Modified By Modified Date 1961 updated note 2nd xsoar 2024-07-12T04:52:20Z xsoar 2024-07-12T10:21:03Z 1937 your first test note xsoar 2024-07-11T07:32:20Z
vectra-detections-mark-asclosed
Mark detections as closed with provided detection IDs in the argument.
Base Command
vectra-detections-mark-asclosed
Input
| Argument Name | Description | Required |
|---|---|---|
| detection_ids | Provide a list of detection IDs separated by commas or a single detection ID. | Required |
| close_reason | Provide the close reason. Possible values are: benign, remediated. | Required |
Context Output
There is no context output for this command.
Command example
!vectra-detections-mark-asclosed detection_ids=123,345 close_reason=remediated
Human Readable Output
The provided detection IDs have been successfully closed as remediated
vectra-detections-mark-asopen
Open detections with provided detection IDs in the argument.
Base Command
vectra-detections-mark-asopen
Input
| Argument Name | Description | Required |
|---|---|---|
| detection_ids | Provide a list of detection IDs separated by commas or a single detection ID. | Required |
Context Output
There is no context output for this command.
Command example
!vectra-detections-mark-asopen detection_ids=123,345
Human Readable Output
The provided detection IDs have been successfully re-opened
vectra-outcome-describe
Returns a single outcome details
Base Command
vectra-outcome-describe
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Outcome ID you want to get details on. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Outcome.IsBuiltIn | String | Is this Outcome a builtin Outcome |
| Vectra.Outcome.Category | String | Outcome’s category (‘False Positive’, ‘Benign True Positive’, ‘Malicious True Positive’) |
| Vectra.Outcome.ID | Number | Outcome ID (unique) |
| Vectra.Outcome.Title | String | Outcome title |
vectra-outcome-create
Creates a new assignment outcome
Base Command
vectra-outcome-create
Input
| Argument Name | Description | Required |
|---|---|---|
| title | Outcome title (will be visible in the UI). | Optional |
| category | Outcome category (one of the 3). Possible values are: Benign True Positive, Malicious True Positive, False Positive. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Outcome.IsBuiltIn | String | Is this Outcome a builtin Outcome |
| Vectra.Outcome.Category | String | Outcome’s category (‘False Positive’, ‘Benign True Positive’, ‘Malicious True Positive’) |
| Vectra.Outcome.ID | Number | Outcome ID (unique) |
| Vectra.Outcome.Title | String | Outcome title |
vectra-assignment-describe
Returns a single assignment details
Base Command
vectra-assignment-describe
Input
| Argument Name | Description | Required |
|---|---|---|
| id | Assignment ID you want to get details on. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Assignment.AccountID | Number | Account ID this assignment is linked to |
| Vectra.Assignment.AssignedBy | String | Who lastly assigned this assignment |
| Vectra.Assignment.AssignedDate | String | When this assignment was lastly assigned |
| Vectra.Assignment.AssignedTo | String | To who this assignment is assigned |
| Vectra.Assignment.HostID | String | Host ID this assignment is linked to |
| Vectra.Assignment.ID | Number | Assignment ID (unique) |
| Vectra.Assignment.IsResolved | Boolean | Is this assignment resolved |
| Vectra.Assignment.OutcomeCategory | String | Assignment Outcome category |
| Vectra.Assignment.OutcomeTitle | String | Assignment Outcome title |
| Vectra.Assignment.TriagedDetections | String | List of Detection that have been triaged with the resolution |
| Vectra.Assignment.TriagedAs | String | Name of the triage rule if any |
| Vectra.Assignment.ResolvedBy | String | Who resolved this assignment |
| Vectra.Assignment.ResolvedDate | string | When this assignment was resolved |
vectra-assignment-assign
Assigns an Account/Host entity to a Vectra User for investigation. If an assignment already exists on this entity, it will be reassigned
Base Command
vectra-assignment-assign
Input
| Argument Name | Description | Required |
|---|---|---|
| assignee_id | Assignee’s ID (Vectra User ID). | Optional |
| assignment_id | Assignment ID if an assignment already exists for the given entity. | Optional |
| account_id | Account ID. | Optional |
| host_id | Host ID. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Assignment.AccountID | Number | Account ID this assignment is linked to |
| Vectra.Assignment.AssignedBy | String | Who lastly assigned this assignment |
| Vectra.Assignment.AssignedDate | String | When this assignment was lastly assigned |
| Vectra.Assignment.AssignedTo | String | To who this assignment is assigned |
| Vectra.Assignment.HostID | String | Host ID this assignment is linked to |
| Vectra.Assignment.ID | Number | Assignment ID (unique) |
| Vectra.Assignment.IsResolved | Boolean | Is this assignment resolved |
| Vectra.Assignment.OutcomeCategory | String | Assignment Outcome category |
| Vectra.Assignment.OutcomeTitle | String | Assignment Outcome title |
| Vectra.Assignment.TriagedDetections | String | List of Detection that have been triaged with the resolution |
| Vectra.Assignment.TriagedAs | String | Name of the triage rule if any |
| Vectra.Assignment.ResolvedBy | String | Who resolved this assignment |
| Vectra.Assignment.ResolvedDate | string | When this assignment was resolved |
vectra-assignment-resolve
Resolves an assignment by selecting resolution scheme. Could be ‘resolving only’ or ‘resolving by filtering detections’
Base Command
vectra-assignment-resolve
Input
| Argument Name | Description | Required |
|---|---|---|
| assignment_id | Assignment’s ID. | Optional |
| outcome_id | Assignment Outcome’s ID. | Optional |
| note | A note to add to this resolution. | Optional |
| detections_filter | Do you want to filter detections when resolving this assignment ? [Default is None]. Possible values are: None, Filter Rule. | Optional |
| filter_rule_name | Filter rule’s name (when using filter_detections=”Filter Rule”). | Optional |
| detections_list | Detection IDs list you want to filter. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Assignment.AccountID | Number | Account ID this assignment is linked to |
| Vectra.Assignment.AssignedBy | String | Who lastly assigned this assignment |
| Vectra.Assignment.AssignedDate | String | When this assignment was lastly assigned |
| Vectra.Assignment.AssignedTo | String | To who this assignment is assigned |
| Vectra.Assignment.HostID | String | Host ID this assignment is linked to |
| Vectra.Assignment.ID | Number | Assignment ID (unique) |
| Vectra.Assignment.IsResolved | Boolean | Is this assignment resolved |
| Vectra.Assignment.OutcomeCategory | String | Assignment Outcome category |
| Vectra.Assignment.OutcomeTitle | String | Assignment Outcome title |
| Vectra.Assignment.TriagedDetections | String | List of Detection that have been triaged with the resolution |
| Vectra.Assignment.TriagedAs | String | Name of the triage rule if any |
| Vectra.Assignment.ResolvedBy | String | Who resolved this assignment |
| Vectra.Assignment.ResolvedDate | string | When this assignment was resolved |
vectra-user-describe
Returns a single Vectra User details
Base Command
vectra-user-describe
Input
| Argument Name | Description | Required |
|---|---|---|
| id | User ID you want to get details on. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.User.Email | String | User’s email address |
| Vectra.User.ID | Number | User ID (unique) |
| Vectra.User.Role | String | User’s role |
| Vectra.User.Type | String | User type (‘Local’, ‘SAML’, …) |
| Vectra.User.Username | String | Username |
| Vectra.User.LastLoginDate | String | User’s last login datetime |
vectra-group-list
Returns a list of all groups.
Base Command
vectra-group-list
Input
| Argument Name | Description | Required |
|---|---|---|
| group_type | Filter by group type. Possible values are: account, host, ip, domain. | Optional |
| account_names | Filter by Account Names. Supports comma-separated values. Note: Only valid when the group_type parameter is set to “account”. |
Optional |
| domains | Filter by Domains. Supports comma-separated values. Note: Only valid when the group_type parameter is set to “domain”. |
Optional |
| host_ids | Filter by Host IDs. Supports comma-separated values. Note: Only valid when the group_type parameter is set to “host”. |
Optional |
| host_names | Filter by Host Names. Supports comma-separated values. Note: Only valid when the group_type parameter is set to “host”. |
Optional |
| importance | Filter by group importance. Possible values are: high, medium, low, never_prioritize. | Optional |
| ips | Filter by IPs. Supports comma-separated values. Note: Only valid when the group_type parameter is set to “ip”. |
Optional |
| description | Filter by group description. | Optional |
| last_modified_timestamp | Return only the groups which have a last modification timestamp equal to or after the given timestamp. Supported formats: 2 minutes, 2 hours, 2 days, 2 weeks, 2 months, 2 years, yyyy-mm-dd, yyyy-mm-ddTHH:MM:SSZ. For example: 01 May 2023, 01 Mar 2023 04:45:33, 2023-04-17T14:05:44Z. |
Optional |
| last_modified_by | Filters by the user id who made the most recent modification to the group. | Optional |
| group_name | Filters by group name. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Group.group_id | Number | ID of the group. |
| Vectra.Group.name | String | Name of the group. |
| Vectra.Group.description | String | Description of the group. |
| Vectra.Group.last_modified | Date | Date when the group was last modified. |
| Vectra.Group.last_modified_by | String | Name of the user who last modified the group. |
| Vectra.Group.type | String | Type of the group. |
| Vectra.Group.members.id | Number | Entity ID of member. |
| Vectra.Group.members.name | String | Entity name of member. |
| Vectra.Group.members.is_key_asset | Boolean | Indicates key asset. |
| Vectra.Group.members.url | String | Entity URL of member. |
| Vectra.Group.members.uid | String | Entity UID of member. |
| Vectra.Group.rules.triage_category | String | Triage category of rule. |
| Vectra.Group.rules.id | Number | ID of the rule. |
| Vectra.Group.rules.description | String | Description of the rule. |
| Vectra.Group.importance | String | Importance level of the group. |
| Vectra.Group.cognito_managed | Boolean | Whether the group is managed by Cognito or not. |
Command example
!vectra-group-list group_type=account importance=high
Context Example
{
"Vectra": {
"Group": [
{
"description": "",
"group_id": 1,
"id": 1,
"last_modified": "2024-07-22T06:44:44Z",
"last_modified_by": "cds_xsoar",
"members": [
{
"uid": "user@lab.test.local"
},
{
"uid": "O365:serviceprincipal_00000000-0000-0000-0000-000000000001"
}
],
"name": "AccountNoBlock",
"type": "account"
},
{
"description": "",
"group_id": 2,
"id": 2,
"last_modified": "2024-07-22T06:44:40Z",
"last_modified_by": "cds_xsoar",
"members": [
{
"uid": "O365:serviceprincipal_00000000-0000-0000-0000-000000000001"
}
],
"name": "AccountBlock",
"type": "account"
}
]
}
}
Human Readable Output
Groups Table
Group ID Name Group Type Members Last Modified Timestamp 1 AccountNoBlock account user@lab.test.local, O365:serviceprincipal_00000000-0000-0000-0000-000000000001 2024-07-22T06:44:44Z 2 AccountBlock account O365:serviceprincipal_00000000-0000-0000-0000-000000000001 2024-07-22T06:44:40Z
vectra-group-assign
Assign members to the specified group.
Base Command
vectra-group-assign
Input
| Argument Name | Description | Required |
|---|---|---|
| group_id | Specify Group ID to assign members. Note: You can get the group_id by executing the "vectra-group-list" command. |
Required |
| members | A comma-separated list of member values based on the group type. Note: You can get the members by executing the "vectra-group-list" command. If the group type is host, then the “Host IDs”. If the group type is account, then “Account Names”. If the group type is ip, then the list of “IPs”. If the group type is domain, then the list of “Domains”. |
Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Group.group_id | Number | ID of the group. |
| Vectra.Group.name | String | Name of the group. |
| Vectra.Group.description | String | Description of the group. |
| Vectra.Group.last_modified | Date | Date when the group was last modified. |
| Vectra.Group.last_modified_by | String | Name of the user who last modified the group. |
| Vectra.Group.type | String | Type of the group. |
| Vectra.Group.members.id | Number | Entity ID of member. |
| Vectra.Group.members.name | String | Entity name of member. |
| Vectra.Group.members.is_key_asset | Boolean | Indicates key asset. |
| Vectra.Group.members.url | String | Entity URL of member. |
| Vectra.Group.members.uid | String | Entity UID of member. |
| Vectra.Group.rules.triage_category | String | Triage category of rule. |
| Vectra.Group.rules.id | Number | ID of the rule. |
| Vectra.Group.rules.description | String | Description of the rule. |
Command example
!vectra-group-assign group_id=3557 members="account_4"
Context Example
{
"Vectra": {
"Group": {
"id": 3,
"name": "xsoar-account-group-2",
"last_modified": "2023-09-04T09:22:46Z",
"last_modified_by": "TEST Client",
"members": [
{
"uid": "account_1"
},
{
"uid": "account_2"
},
{
"uid": "account_3"
},
{
"uid": "account_4"
}
],
"type": "account",
"group_id": 3
}
}
}
Human Readable Output
Member(s) account_4 have been assigned to the group
Updated group details
Group ID Name Group Type Members Last Modified Timestamp 3 xsoar-account-group-2 account account_1, account_2, account_3, account_4 2023-09-04T09:22:46Z
vectra-group-unassign
Unassign members from the specified group.
Base Command
vectra-group-unassign
Input
| Argument Name | Description | Required |
|---|---|---|
| group_id | Specify Group ID to unassign members. Note: You can get the group_id by executing the "vectra-group-list" command. |
Required |
| members | A comma-separated list of member values based on the group type. Note: You can get the members by executing the "vectra-group-list" command. If the group type is host, then the “Host IDs”. If the group type is account, then “Account Names”. If the group type is ip, then the list of “IPs”. If the group type is domain, then the list of “Domains”. |
Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Vectra.Group.group_id | Number | ID of the group. |
| Vectra.Group.name | String | Name of the group. |
| Vectra.Group.description | String | Description of the group. |
| Vectra.Group.last_modified | Date | Date when the group was last modified. |
| Vectra.Group.last_modified_by | String | Name of the user who last modified the group. |
| Vectra.Group.type | String | Type of the group. |
| Vectra.Group.members.id | Number | Entity ID of member. |
| Vectra.Group.members.name | String | Entity name of member. |
| Vectra.Group.members.is_key_asset | Boolean | Indicates key asset. |
| Vectra.Group.members.url | String | Entity URL of member. |
| Vectra.Group.members.uid | String | Entity UID of member. |
| Vectra.Group.rules.triage_category | String | Triage category of rule. |
| Vectra.Group.rules.id | Number | ID of the rule. |
| Vectra.Group.rules.description | String | Description of the rule. |
Command example
!vectra-group-unassign group_id=5 members="2126"
Context Example
{
"Vectra": {
"Group": {
"id": 2,
"group_id": 2,
"type": "host",
"name": "TEST RENAME",
"description": "TEST RENAME",
"last_modified": "2023-09-04T06:27:57Z",
"last_modified_by": "TEST Client"
}
}
}
Human Readable Output
Member(s) 2126 have been unassigned from the group
Updated group details
Group ID Name Group Type Description Last Modified Timestamp 2 TEST RENAME host TEST RENAME 2023-09-04T06:27:57Z
Troubleshooting
Receive Notification on an Incident Fetch Error
The administrator and Cortex XSOAR users on the recipient’s list receive a notification when an integration experiences an incident fetch error. Cortex XSOAR users can select their notification method, such as email, from their user preferences. Refer to Cortex XSOAR 6.13 documentation or Cortex XSOAR 8 Cloud documentation or Cortex XSOAR 8.7 On-prem documentation for more information.
The following are tips for handling issues with mirroring incidents between Vectra and Cortex XSOAR
| Issue | Recommendation |
|---|---|
| Mirroring is not working. | Open Context Data and search for dbot. Confirm the dbot fields are configured correctly either through the mapper for that specific incident type or using setIncident. Specifically, make sure the integration instance is configured correctly for the mirroring direction (incoming, outgoing, both) - dbotMirrorId, dbotMirrorDirection, dbotMirrorInstance, dbotMirrorTags. |
| Required fields are not getting sent or not visible in UI. | This may be a mapping issue, specifically if you have used a custom mapper make sure you’ve covered all the out of box mapper fields. |
| Notes from Cortex XSOAR have not been mirrored in Vectra | Tag is required for mirroring notes from Cortex XSOAR to Vectra. There might be a reason the note is not tagged as the tag needs to be added manually in Cortex XSOAR. Click Actions > Tags and add the “note” tag (OR the specific tag name which was set up in the Instance Configuration). |
Docker timeout issue for Fetch Incidents
-
If you encounter a timeout error while fetching incidents, you can try adjusting the value of the
max_fetchparameter in the instance configuration. Setting it to a lower value, such as 50 can help prevent the timeout issue. -
Another way to address this issue is to increase the timeout of the Docker container. By default, Docker containers have a timeout of 5 minutes. You can increase this timeout to a higher value, such as 10 minutes, to allow more time for the fetch command to complete. Refer to this XSOAR documentation for more information.
Handling HTTP 429 and 5xx Errors
The commands and fetch incidents mechanism will do up to 3 internal retries with a gap of 15, 30, and 60 seconds (exponentially) between the retries.
Configuration parameters
server_fqdn— Vectra Detect FQDN or IP (required)authentication_type— Authentication Typecredentials— API Tokenoauth_credentials— Client IDinsecure— Trust any certificate (not secure)use_proxy— Use system proxy settingsisFetch— Fetch incidentsincidentType— Incident typeincidentFetchInterval— Incidents Fetch Intervalfirst_fetch— First fetch timestampmirror_direction— Mirroring Directionrefetch_closed_incidents— Re-Fetch closed incidents via mirroringnote_tag— Mirror tag for notesfetch_entity_types— Entity types to fetchtags— Tagsdetection_category— Detection Categorydetection_type— Detection Typehosts_fetch_query— Hosts fetch queryaccounts_fetch_query— Accounts fetch querydetections_fetch_query— Detections fetch querymax_fetch— Max created incidents per fetchlook_back— Advanced: Minutes to look back when fetchingfetch_escalated_accounts_and_hosts— Advanced: Fetch escalated Accounts and Hostscreate_multiple_incidents_by_timestamp— Re-Fetch Account/Host entity and Detection for new activity/event
Commands (47)
-
vectra-account-add-tagsAdd tags to an Account.
-
vectra-account-del-tagsDelete tags from an Account.
-
vectra-account-describeReturns a single Account details.
-
vectra-account-markall-detections-asclosedMark active detections as closed by providing the ID of the account in the argument.
-
vectra-account-markall-detections-asfixedMark active detections as fixed by providing the ID of the account in the argument.
-
vectra-account-note-addAdd a note to the account.
-
vectra-account-note-listList all notes of the specific account.
-
vectra-account-note-removeRemove a note from the account.
-
vectra-account-note-updateUpdate a note in the account.
-
vectra-account-tag-listReturns a list of tags for a specified account.
-
vectra-assignment-assignAssigns an Account/Host entity to a Vectra User for investigation. If an assignment already exists on this entity, it will be reassigned.
-
vectra-assignment-describeReturns a single assignment details.
-
vectra-assignment-resolveResolves an assignment by selecting resolution scheme. Could be 'resolving only' or 'resolving by filtering detections'.
-
vectra-detection-add-tagsAdd tags to a Detection.
-
vectra-detection-del-tagsDelete tags from a Detection.
-
vectra-detection-describeReturns a single detection details.
-
vectra-detection-get-pcapReturns a Detection's PCAP file (if available).
-
vectra-detection-markasfixedMarks/Unmarks a Detection as fixed by providing the Detection ID.
-
vectra-detection-note-addAdd a note to the detection.
-
vectra-detection-note-listList all notes of the specific detection.
-
vectra-detection-note-removeRemove a note from the detection.
-
vectra-detection-note-updateUpdate a note in the detection.
-
vectra-detection-tag-listReturns a list of tags for a specified detection.
-
vectra-detections-mark-asclosedMark detections as closed with provided detection IDs in the argument.
-
vectra-detections-mark-asopenOpen detections with provided detection IDs in the argument.
-
vectra-group-assignAssign members to the specified group.
-
vectra-group-listReturns a list of all groups.
-
vectra-group-unassignUnassign members from the specified group.
-
vectra-host-add-tagsAdd tags to an Host.
-
vectra-host-del-tagsDelete tags from an Host.
-
vectra-host-describeReturns a single Host details.
-
vectra-host-markall-detections-asclosedMark active detections as closed by providing the ID of the host in the argument.
-
vectra-host-markall-detections-asfixedMark active detections as fixed by providing ID of the host in the argument.
-
vectra-host-note-addAdd a note to the host.
-
vectra-host-note-listList all notes of the specific host.
-
vectra-host-note-removeRemove a note from the host.
-
vectra-host-note-updateUpdate a note in the host.
-
vectra-host-tag-listReturns a list of tags for a specified host.
-
vectra-outcome-createCreates a new assignment outcome.
-
vectra-outcome-describeReturns a single outcome details.
-
vectra-search-accountsReturns a list of Account objects. All search attributes will be cumulative unless you're using the search_query_only one, in that case, only this one will be taken into account.
-
vectra-search-assignmentsReturn a list of assignments. By default already resolved assignment are not returned.
-
vectra-search-detectionsReturns a list of Detection objects. All search attributes will be cumulative unless you're using the search_query_only one, in that case, only this one will be taken into account.
-
vectra-search-hostsReturns a list of Host objects. All search attributes will be cumulative unless you're using the search_query_only one, in that case, only this one will be taken into account.
-
vectra-search-outcomesReturns a list of assignment outcomes.
-
vectra-search-usersReturns a list of Vectra Users. All search attributes will be cumulative.
-
vectra-user-describeReturns a single Vectra User details.
# ## ### #### ##### # Vectra Detect Integration for Cortex XSOAR # # Developer Documentation: https://xsoar.pan.dev/docs/welcome # Code Conventions: https://xsoar.pan.dev/docs/integrations/code-conventions # Linting: https://xsoar.pan.dev/docs/integrations/linting # ## ### #### ##### # Python linting disabled example (disable linting on error code E203) # noqa: E203 # Standard libraries import json from copy import deepcopy from typing import Any from urllib.parse import parse_qs, urlparse # Specific libraries import dateparser # type: ignore[import] # XSOAR libraries import demistomock as demisto # noqa: F401 import urllib3 from CommonServerPython import * # noqa: F401 from CommonServerUserPython import * # Disable insecure warnings urllib3.disable_warnings() # #### #### # # ## CONSTANTS ## # TOTAL_RETRIES = 4 STATUS_CODE_TO_RETRY = (429, *(status_code for status_code in requests.status_codes._codes if status_code >= 500)) # type: ignore OK_CODES = (200, 201, 204, 401) BACKOFF_FACTOR = 7.5 # Sleep for [0s, 15s, 30s, 60s] between retries. PACK_VERSION = get_pack_version() or "2.1.0" UTM_PIVOT = f"?pivot=Vectra_AI-XSOAR-{PACK_VERSION}" DATE_FORMAT: str = "%Y-%m-%dT%H:%M:%S.000Z" USER_AGENT = f"Vectra_AI-XSOAR-{PACK_VERSION}" MAX_RESULTS: int = 200 DEFAULT_FIRST_FETCH: str = "7 days" DEFAULT_FETCH_ENTITY_TYPES: list = ["Hosts", "Accounts"] DEFAULT_MAX_FETCH: int = 50 TAGS_REGEX = re.compile(r"^[\w:._ -]+$", re.U) ERRORS = { "REQUIRED_ARGUMENT": "Please provide valid value of the '{}'. It is required field.", "INVALID_INTEGER_VALUE": "'{}' value must be a non-zero and positive integer value.", "POSITIVE_VALUE": 'The value of the "{}" must be greater than or equal to 0', "INVALID_MAX_FETCH": '"{}" is an invalid value for Max incidents per fetch. The value must be between 1 to 200.', "INVALID_COMMAND_ARG_VALUE": ( "Invalid '{}' value provided. Please ensure it is one of the values from the following options: {}." ), "INVALID_SUPPORT_FOR_ARG": 'The argument "{}" must be set to "{}" when providing value for argument "{}".', "INVALID_OBJECT": "Failed to parse {} object from response: {}", "UNAUTHORIZED_REQUEST": "Status code: {}. Unauthorized request: {}.", "GENERAL_AUTH_ERROR": "Status code: {}. Error occurred while creating an authorization token.", } ENDPOINTS = { "ADD_AND_LIST_ACCOUNT_NOTE_ENDPOINT": "/accounts/{}/notes", "ADD_AND_LIST_HOST_NOTE_ENDPOINT": "/hosts/{}/notes", "ADD_AND_LIST_DETECTION_NOTE_ENDPOINT": "/detections/{}/notes", "UPDATE_AND_REMOVE_ACCOUNT_NOTE_ENDPOINT": "/accounts/{}/notes/{}", "UPDATE_AND_REMOVE_HOST_NOTE_ENDPOINT": "/hosts/{}/notes/{}", "UPDATE_AND_REMOVE_DETECTION_NOTE_ENDPOINT": "/detections/{}/notes/{}", } OUTPUT_PREFIXES = { "ACCOUNT_NOTES": "Vectra.Account.Notes", "HOST_NOTES": "Vectra.Host.Notes", "DETECTION_NOTES": "Vectra.Detection.Notes", } NOTE_OUTPUT_KEY_FIELD = "note_id" API_VERSION_URL = "/api/v2.5" API_ENDPOINT_OAUTH_TOKEN = "/oauth2/token" API_ENDPOINT_ACCOUNTS = "/accounts" API_ENDPOINT_ASSIGNMENT = "/assignments" API_ENDPOINT_OUTCOMES = "/assignment_outcomes" API_ENDPOINT_DETECTIONS = "/detections" API_ENDPOINT_HOSTS = "/hosts" API_ENDPOINT_USERS = "/users" API_ENDPOINT_GROUPS = "/groups" API_SEARCH_ENDPOINT_ACCOUNTS = "/search/accounts" API_SEARCH_ENDPOINT_DETECTIONS = "/search/detections" API_SEARCH_ENDPOINT_HOSTS = "/search/hosts" API_TAGGING = "/tagging" UI_ACCOUNTS = "/accounts" UI_DETECTIONS = "/detections" UI_HOSTS = "/hosts" DEFAULT_ORDERING = { "accounts": {"ordering": "last_detection_timestamp"}, "detections": {"ordering": "last_timestamp"}, "hosts": {"ordering": "last_detection_timestamp"}, } DEFAULT_STATE = {"state": "active", "resolved": "false"} ENTITY_TYPES = ("Accounts", "Hosts", "Detections") OUTCOME_CATEGORIES = { "benign_true_positive": "Benign True Positive", "malicious_true_positive": "Malicious True Positive", "false_positive": "False Positive", } ASSIGNMENT_ENTITY_TYPES = ("account", "host") BACK_IN_TIME_SEARCH_IN_MINUTES = "0" VALID_GROUP_TYPE = ["account", "host", "ip", "domain"] VALID_IMPORTANCE_VALUE = ["high", "medium", "low", "never_prioritize"] VALID_CLOSE_REASON = ["benign", "remediated"] MAX_MIRRORING_LIMIT = 5000 ENTITY_TYPES_FOR_MIRRORING = ["host", "account"] EMPTY_ASSIGNMENT = [ { "id": "", "date_assigned": "", "date_resolved": "", "assigned_to": {"username": ""}, "resolved_by": {"username": ""}, "assigned_by": {"username": ""}, "outcome": {"title": ""}, } ] MIRROR_DIRECTION = {"Incoming": "In", "Outgoing": "Out", "Incoming And Outgoing": "Both"} EARLY_EXPIRY_TIME = 300 # (in seconds) # #### #### # # ## GLOBALS ## # global_UI_URL: str | None = None # #### #### # # ## CLIENT CLASS ## # class Client(BaseClient): """Client class to interact with the service API This Client implements API calls, and does not contain any XSOAR logic. Should only do requests and return data. It inherits from BaseClient defined in CommonServer Python. Most calls use http_request() that handles proxy, SSL verification, etc. Supports both API token and OAuth 2.0 authentication methods. """ def __init__( self, base_url: str, verify: bool, proxy: bool, use_oauth: bool = False, api_token: str = None, client_id: str = None, client_secret: str = None, headers: dict = None, ): """ Initializes the Client instance with authentication support. Args: base_url (str): The base URL for the API. verify (bool): Whether to verify SSL certificates. proxy (bool): Whether to use proxy. use_oauth (bool): Whether to use OAuth 2.0 authentication (True) or API token (False). api_token (str): API token for token-based authentication. client_id (str): Client ID for OAuth authentication. client_secret (str): Client secret for OAuth authentication. headers (dict): Additional headers to include in requests. """ super().__init__(base_url=base_url, verify=verify, proxy=proxy, headers=headers) self.use_oauth = use_oauth self.api_token = api_token self.client_id = client_id self.client_secret = client_secret def _get_auth_headers(self) -> dict: """ Get authentication headers based on the configured auth type. Returns: dict: Headers with appropriate authentication. """ headers = {"User-Agent": USER_AGENT} if self.use_oauth: token = self._generate_tokens() headers["Authorization"] = f"Bearer {token}" elif not self.use_oauth and self.api_token: headers["Authorization"] = f"token {self.api_token}" return headers def _generate_tokens(self, use_existing_token: bool = True) -> str: """ Get an OAuth access token that was previously created if it is still valid, else, generate a new authorization token from the client id and client secret. Args: use_existing_token: Use existing token if it is still valid. Returns: str: The access token. """ integration_context: dict = get_integration_context() previous_token: dict = integration_context.get("oauth_token", {}) # Check if there is existing valid authorization token. if ( previous_token.get("access_token") and use_existing_token and previous_token.get("expire") > datetime.now(timezone.utc).timestamp() # type: ignore ): demisto.debug("Got OAuth access token from the integration context.") return previous_token.get("access_token") # type: ignore demisto.debug("Trying to generate a new OAuth access token.") payload = "grant_type=client_credentials" auth = requests.auth.HTTPBasicAuth(self.client_id, self.client_secret) # type: ignore headers = {"User-Agent": USER_AGENT, "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"} res = self._http_request( method="POST", url_suffix=API_ENDPOINT_OAUTH_TOKEN, headers=headers, data=payload, auth=auth, backoff_factor=BACKOFF_FACTOR, status_list_to_retry=STATUS_CODE_TO_RETRY, resp_type="response", ok_codes=OK_CODES, raise_on_status=True, ) res_status_code = res.status_code if res_status_code in [401]: raise DemistoException(ERRORS["GENERAL_AUTH_ERROR"].format(res_status_code)) try: res_json = res.json() except ValueError as exception: raise DemistoException(ERRORS["INVALID_OBJECT"].format("json", res.content), exception) if res_json.get("access_token"): # Calculate expiry time from expires_in (seconds) with early expiry buffer expires_in_seconds = res_json.get("expires_in", 21600) # Default to 6 hours expiry_time = datetime.now(timezone.utc).timestamp() + expires_in_seconds - EARLY_EXPIRY_TIME # 5 min buffer demisto.debug( "Setting the expiry time of the OAuth access token to " + timestamp_to_datestring(expiry_time * 1000, is_utc=True) ) new_token = {"access_token": res_json.get("access_token"), "expire": expiry_time} integration_context.update({"oauth_token": new_token}) set_integration_context(integration_context) return res_json.get("access_token") raise DemistoException("Failed to generate OAuth access token - no access_token in response") def http_request( self, method, url_suffix, params=None, status_list_to_retry=STATUS_CODE_TO_RETRY, backoff_factor=BACKOFF_FACTOR, retries=TOTAL_RETRIES, internal_retries=3, json_data=None, resp_type="json", **kwargs, ): """ Method to override private _http_request of BaseClient to handle specific status code. :type method: ``str`` :param method: The HTTP method, for example: GET, POST, and so on. :type url_suffix: ``str`` :param url_suffix: The API endpoint. :type params: ``dict`` :param params: URL parameters to specify the query. :type status_list_to_retry: ``iterable`` :param status_list_to_retry: A set of integer HTTP status codes that we should force a retry on. :type backoff_factor ``float`` :param backoff_factor: A backoff factor to apply between attempts :type retries: ``int`` :param retries: How many retries should be made in case of a failure. :type internal_retries: ``int`` :param internal_retries: How many retries should be made in case of an auth failure. :type json_data: ``dict`` :param json_data: Json data to submit with the http request. :type resp_type: ``str`` :param resp_type: Response type. :return: Response dict or response object. :rtype: ``Optional[dict]`` or ``requests.Response`` """ demisto.debug( "Requesting Vectra Detect with method: " f"{method}, url_suffix: {url_suffix}, params: {params} and json_data: {json_data}" ) # Get authentication headers dynamically headers = self._get_auth_headers() resp = self._http_request( method=method, url_suffix=url_suffix, headers=headers, params=params, json_data=json_data, retries=retries, status_list_to_retry=status_list_to_retry, backoff_factor=backoff_factor, raise_on_status=True, resp_type="response", ok_codes=OK_CODES, **kwargs, ) resp_status_code = resp.status_code # If authentication failure happens for OAuth. if resp_status_code in [401]: if self.use_oauth: demisto.debug("Handling status code 401 by generating a new OAuth token.") if internal_retries > 0: self._generate_tokens(use_existing_token=False) internal_retries = internal_retries - 1 return self.http_request( method=method, url_suffix=url_suffix, params=params, status_list_to_retry=status_list_to_retry, backoff_factor=backoff_factor, retries=retries, internal_retries=internal_retries, json_data=json_data, resp_type=resp_type, **kwargs, ) try: err_msg = ERRORS["UNAUTHORIZED_REQUEST"].format(resp_status_code, str(resp.json())) except ValueError: err_msg = ERRORS["UNAUTHORIZED_REQUEST"].format(resp_status_code, str(resp)) raise DemistoException(err_msg) else: raise DemistoException(ERRORS["UNAUTHORIZED_REQUEST"].format(resp_status_code, str(resp.json()))) try: result = None if resp_type == "json": result = resp.json() if resp_type == "content": result = resp.content() if resp_type == "response": result = resp if resp_type == "text": result = resp.text except ValueError as exception: raise DemistoException( ERRORS["INVALID_OBJECT"].format(resp_type, resp.content), # type: ignore[str-bytes-safe] exception, resp, ) return result def search_detections( self, min_id=None, max_id=None, min_threat=None, max_threat=None, min_certainty=None, max_certainty=None, last_timestamp=None, state: str = None, # type: ignore search_query: str = None, search_query_only: str = None, # type: ignore max_results=None, # type: ignore **kwargs, ) -> dict[str, Any]: """ Gets Detections using the 'detections' API endpoint :return: dict containing all Detections details :rtype: ``Dict[str, Any]`` """ # Default params demisto.debug("Forcing 'page', 'order_field' and 'page_size' query arguments") query_params: dict[str, Any] = {"page": 1, "order_field": "last_timestamp"} query_params["page_size"] = sanitize_max_results(max_results) params: dict[str, Any] = {} if search_query_only: # Specific search query used query_params["query_string"] = search_query_only else: # Test min_id / max_id validate_min_max("min_id", min_id, "max_id", max_id) if min_id: params["min_id"] = min_id if max_id: params["max_id"] = max_id # Test min_threat / max_threat validate_min_max("min_threat", min_threat, "max_threat", max_threat) if min_threat: params["min_threat"] = min_threat if max_threat: params["max_threat"] = max_threat # Test min_certainty / max_certainty validate_min_max("min_certainty", min_certainty, "max_certainty", max_certainty) if min_certainty: params["min_certainty"] = min_certainty if max_certainty: params["max_certainty"] = max_certainty # Last timestamp if last_timestamp: params["last_timestamp"] = last_timestamp # State if state: params["state"] = state else: params["state"] = DEFAULT_STATE["state"] # Build search query query_params["query_string"] = build_search_query("detection", params) # Adding additional search query if search_query: query_params["query_string"] += f" AND {search_query}" demisto.debug(f"Search query : '{query_params['query_string']}'") # Execute request demisto.debug("Executing API request") return self.http_request(method="GET", params=query_params, url_suffix=f"{API_SEARCH_ENDPOINT_DETECTIONS}") def list_detections_by_host_id( self, host_id: str, state: str = "active", detection_category: str = "", detection_type: str = "" ) -> dict: """ Get Detections by Host ID. :param host_id: Host ID. :param state: The state of the detections. :param detection_category: Detection Category. :param detection_type: Detection Type. :return: List of Detections. :rtype: ``Dict`` """ params = assign_params(detection_category=detection_category, detection_type=detection_type, state=state, host_id=host_id) return self.http_request(method="GET", url_suffix=f"{API_ENDPOINT_DETECTIONS}", params=params) def search_accounts( self, min_id=None, max_id=None, min_threat=None, max_threat=None, min_certainty=None, max_certainty=None, last_timestamp=None, state: str = None, # type: ignore search_query: str = None, search_query_only: str = None, # type: ignore page: int = None, max_results=None, # type: ignore order_field: str = "last_detection_timestamp", # type: ignore doc_modified_time=None, **kwargs, ) -> dict[str, Any]: """ Gets Accounts using the 'Search Accounts' API endpoint. :return: dict containing all Accounts details :rtype: ``Dict[str, Any]`` """ # Default params demisto.debug("Forcing 'page', 'order_field' and 'page_size' query arguments") query_params: dict[str, Any] = {"page": page if page else 1, "order_field": order_field} query_params["page_size"] = sanitize_max_results(max_results) params: dict[str, Any] = {} if search_query_only: # Specific search query used query_params["query_string"] = search_query_only else: # Test min_id / max_id validate_min_max("min_id", min_id, "max_id", max_id) if min_id: params["min_id"] = min_id if max_id: params["max_id"] = max_id # Test min_threat / max_threat validate_min_max("min_threat", min_threat, "max_threat", max_threat) if min_threat: params["min_threat"] = min_threat if max_threat: params["max_threat"] = max_threat # Test min_certainty / max_certainty validate_min_max("min_certainty", min_certainty, "max_certainty", max_certainty) if min_certainty: params["min_certainty"] = min_certainty if max_certainty: params["max_certainty"] = max_certainty # Last timestamp if last_timestamp: params["last_timestamp"] = last_timestamp if doc_modified_time: params["doc_modified_time"] = doc_modified_time # State if state: params["state"] = state else: params["state"] = DEFAULT_STATE["state"] # Build search query query_params["query_string"] = build_search_query("account", params) # Adding additional search query if search_query: query_params["query_string"] += f" AND {search_query}" demisto.debug(f"Search query : '{query_params['query_string']}'") # Execute request demisto.debug("Executing API request") return self.http_request(method="GET", params=query_params, url_suffix=f"{API_SEARCH_ENDPOINT_ACCOUNTS}") def search_hosts( self, min_id=None, max_id=None, min_threat=None, max_threat=None, min_certainty=None, max_certainty=None, last_timestamp=None, state: str = None, # type: ignore search_query: str = None, search_query_only: str = None, # type: ignore page: int = None, max_results=None, # type: ignore order_field: str = "last_detection_timestamp", # type: ignore doc_modified_time=None, **kwargs, ) -> dict[str, Any]: """ Gets Hosts using the 'hosts' API endpoint. :return: dict containing all Hosts details. :rtype: ``Dict[str, Any]`` """ # Default params demisto.debug("Forcing 'page', 'order_field' and 'page_size' query arguments") query_params: dict[str, Any] = {"page": page if page else 1, "order_field": order_field} query_params["page_size"] = sanitize_max_results(max_results) params: dict[str, Any] = {} if search_query_only: # Specific search query used query_params["query_string"] = search_query_only else: # Test min_id / max_id validate_min_max("min_id", min_id, "max_id", max_id) if min_id: params["min_id"] = min_id if max_id: params["max_id"] = max_id # Test min_threat / max_threat validate_min_max("min_threat", min_threat, "max_threat", max_threat) if min_threat: params["min_threat"] = min_threat if max_threat: params["max_threat"] = max_threat # Test min_certainty / max_certainty validate_min_max("min_certainty", min_certainty, "max_certainty", max_certainty) if min_certainty: params["min_certainty"] = min_certainty if max_certainty: params["max_certainty"] = max_certainty # Last timestamp if last_timestamp: params["last_timestamp"] = last_timestamp if doc_modified_time: params["doc_modified_time"] = doc_modified_time # State if state: params["state"] = state else: params["state"] = DEFAULT_STATE["state"] # Build search query query_params["query_string"] = build_search_query("host", params) # Adding additional search query if search_query: query_params["query_string"] += f" AND {search_query}" demisto.debug(f"Search query : '{query_params['query_string']}'") # Execute request return self.http_request(method="GET", params=query_params, url_suffix=f"{API_SEARCH_ENDPOINT_HOSTS}") def search_assignments( self, id=None, account_ids=None, host_ids=None, assignee_ids=None, outcome_ids=None, resolved=None ) -> dict[str, Any]: """ Gets Assignments using the 'assignment' API endpoint. :return: dict containing all Assignments details. :rtype: ``Dict[str, Any]`` """ # Default params # Assignment endpoint doesn't support pagination query_params: dict[str, Any] = {} url_addon = f"/{id}" if id else "" # If id is specified, do not use other params if not id: if account_ids and host_ids: raise VectraException("Cannot use 'account_ids' and 'host_ids' at the same time") # Test Account IDs account_ids_set = sanitize_str_ids_list_to_set(account_ids) if account_ids_set is not None: query_params["accounts"] = account_ids_set # Test Host IDs host_ids_set = sanitize_str_ids_list_to_set(host_ids) if host_ids_set is not None: query_params["hosts"] = host_ids_set # Test Assignee IDs assignee_ids_set = sanitize_str_ids_list_to_set(assignee_ids) if assignee_ids_set is not None: query_params["assignees"] = assignee_ids_set # Test Outcome IDs outcome_ids_set = sanitize_str_ids_list_to_set(outcome_ids) if outcome_ids_set is not None: query_params["resolution"] = outcome_ids_set # Resolved if resolved: query_params["resolved"] = resolved else: query_params["resolved"] = DEFAULT_STATE["resolved"] # Execute request return self.http_request(method="GET", params=query_params, url_suffix=f"{API_ENDPOINT_ASSIGNMENT}{url_addon}") def search_outcomes(self, id=None, max_results=None) -> dict[str, Any]: """ Gets Assignment outcomes using the 'assignment_outcomes' API endpoint. :return: dict containing all Outcomes details. :rtype: ``Dict[str, Any]`` """ # Default params demisto.debug("Forcing 'page' and 'page_size' query arguments") query_params: dict[str, Any] = {"page": 1} query_params["page_size"] = sanitize_max_results(max_results) url_addon = f"/{id}" if id else "" # Execute request return self.http_request(method="GET", params=query_params, url_suffix=f"{API_ENDPOINT_OUTCOMES}{url_addon}") def search_users(self, id=None, last_login_datetime=None, role=None, type=None, username=None) -> dict[str, Any]: """ Gets Vectra Users using the 'assignment_outcomes' API endpoint. :return: dict containing all User details. :rtype: ``Dict[str, Any]`` """ # Default params # Users endpoint doesn't support pagination query_params: dict[str, Any] = {} url_addon = f"/{id}" if id else "" # If id is specified, do not use other params if not id: # Test user name if username: query_params["username"] = username # Test user role if role: query_params["role"] = role # Test user type if type: query_params["account_type"] = type # Test last login datetime if last_login_datetime and convert_date(last_login_datetime) is not None: query_params["last_login_gte"] = last_login_datetime # Execute request return self.http_request(method="GET", params=query_params, url_suffix=f"{API_ENDPOINT_USERS}{url_addon}") def get_pcap_by_detection_id(self, id: str): """ Gets a single detection PCAP file using the detection endpoint - params: - id: The Detection ID - returns: PCAP file if available """ # Execute request return self.http_request(method="GET", url_suffix=f"{API_ENDPOINT_DETECTIONS}/{id}/pcap", resp_type="response") def markasfixed_by_detection_id(self, id: str, fixed: bool): """ Mark/Unmark a single detection as fixed - params: - id: Vectra Detection ID - fixed: Targeted state - returns: Vectra API call result (unused) """ json_payload = {"detectionIdList": [id], "mark_as_fixed": "true" if fixed else "false"} # Execute request return self.http_request(method="PATCH", url_suffix=API_ENDPOINT_DETECTIONS, json_data=json_payload) def add_tags(self, id: str, type: str, tags: list[str]): """ Adds tags from Vectra entity - params: id: The entity ID type: The entity type tags: Tags list - returns Vectra API call result (unused) """ # Must be done in two steps # 1 - get current tags # 2 - merge list and apply # Execute get request api_response = self.http_request(method="GET", url_suffix=f"{API_TAGGING}/{type}/{id}") current_tags: list[str] = api_response.get("tags", []) json_payload = {"tags": list(set(current_tags).union(set(tags)))} # Execute request return self.http_request(method="PATCH", url_suffix=f"{API_TAGGING}/{type}/{id}", json_data=json_payload) def del_tags(self, id: str, type: str, tags: list[str]): """ Deletes tags from Vectra entity - params: id: The entity ID type: The entity type tags: Tags list - returns Vectra API call result (unused) """ # Must be done in two steps # 1 - get current tags # 2 - merge list and apply # Execute get request api_response = self.http_request(method="GET", url_suffix=f"{API_TAGGING}/{type}/{id}") current_tags = api_response.get("tags", []) json_payload = {"tags": list(set(current_tags).difference(set(tags)))} # Execute request return self.http_request(method="PATCH", url_suffix=f"{API_TAGGING}/{type}/{id}", json_data=json_payload) def create_outcome(self, category: str, title: str): """ Creates a new Outcome - params: - category: The Outcome category (one of "BTP,MTP,FP" in human readable format) - title: A custom title for this new outcome - returns: Vectra API call result """ raw_category = convert_outcome_category_text2raw(category) if raw_category is None: raise ValueError('"category" value is invalid') raw_title = title.strip() if raw_title == "": raise ValueError('"title" cannot be empty') json_payload = {"title": raw_title, "category": raw_category} # Execute request return self.http_request(method="POST", url_suffix=API_ENDPOINT_OUTCOMES, json_data=json_payload) def update_assignment( self, assignee_id: str, assignment_id: str = None, # type: ignore account_id: str = None, host_id: str = None, ): # type: ignore """ Creates or updates an assignment - params: - assignee_id: The Vectra User ID who want to assign to - assignment_id: The existing assignment ID associated with the targeted Entity, if there is any - assignee_id: The Vectra User ID who want to assign to - account_id: The Account ID - host_id: The Host ID - returns: Vectra API call result """ # Test Assignee ID try: validate_argument("min_id", assignee_id) except ValueError: raise ValueError('"assignee_id" value is invalid') json_payload = { "assign_to_user_id": assignee_id, } if assignment_id: # Reassign an existing assignment # Test Assignment ID try: validate_argument("min_id", assignment_id) except ValueError: raise ValueError('"assignment_id" value is invalid') url_addon = f"/{assignment_id}" return self.http_request(method="PUT", url_suffix=f"{API_ENDPOINT_ASSIGNMENT}{url_addon}", json_data=json_payload) elif account_id: # Test Entity ID try: validate_argument("min_id", account_id) except ValueError: raise ValueError('"account_id" value is invalid') json_payload.update({"assign_account_id": account_id}) # Execute request return self.http_request(method="POST", url_suffix=API_ENDPOINT_ASSIGNMENT, json_data=json_payload) elif host_id: # Test Entity ID try: validate_argument("min_id", host_id) except ValueError: raise ValueError('"host_id" value is invalid') json_payload.update({"assign_host_id": host_id}) # Execute request return self.http_request(method="POST", url_suffix=API_ENDPOINT_ASSIGNMENT, json_data=json_payload) else: raise ValueError('Either "assignment_id" or "account_id" or "host_id" must be specified.') def resolve_assignment( self, assignment_id: str, outcome_id: str, note: str = None, # type: ignore rule_name: str = None, detections_list: str = None, ): # type: ignore """ Creates or updates an assignment - params: - assignee_id: The Vectra User ID who want to assign to - assignment_id: The existing assignment ID associated with the targeted Entity, if there is any - assignee_id: The Vectra User ID who want to assign to - account_id: The Account ID - host_id: The Host ID - returns: Vectra API call result """ # Test assignment ID try: validate_argument("min_id", assignment_id) except ValueError: raise ValueError('"assignment_id" value is invalid') # Test outcome ID try: validate_argument("min_id", outcome_id) except ValueError: raise ValueError('"outcome_id" value is invalid') json_payload: dict[str, Any] = { "outcome": outcome_id, "note": note, } if rule_name: detection_ids_set = sanitize_str_ids_list_to_set(detections_list) if detection_ids_set is None: raise ValueError('"detections_list" value is invalid') json_payload.update({"triage_as": rule_name, "detection_ids": list(detection_ids_set)}) # Execute request return self.http_request( method="PUT", url_suffix=f"{API_ENDPOINT_ASSIGNMENT}/{assignment_id}/resolve", json_data=json_payload ) def delete_assignment(self, assignment_id: str = ""): # type: ignore """ Delete the assignment. - params: - assignment_id: The existing assignment ID associated with the targeted Entity. - returns: Vectra API call result. """ if assignment_id: url_addon = f"/{assignment_id}" return self.http_request(method="DELETE", url_suffix=f"{API_ENDPOINT_ASSIGNMENT}{url_addon}", resp_type="response") return None def markasfixed_by_detection_ids(self, ids_list: list): """ Mark a list of detections as fixed. - params: - ids_list: Vectra Detection IDs list. - returns: Vectra API call result. """ json_payload = {"detectionIdList": ids_list, "mark_as_fixed": "true"} # Execute request return self.http_request(method="PATCH", url_suffix=API_ENDPOINT_DETECTIONS, json_data=json_payload) def close_detections_by_ids(self, ids_list: list, reason: str): """ Close a list of detections with a specified reason. - params: - ids_list: Vectra Detection IDs list. - reason: The close reason (benign or remediated). - returns: Vectra API call result. """ json_payload = {"detectionIdList": ids_list, "reason": reason} # Execute request return self.http_request(method="PATCH", url_suffix=f"{API_ENDPOINT_DETECTIONS}/close", json_data=json_payload) def open_detections_by_ids(self, ids_list: list): """ Open a detections by ID - params: - ids_list: Vectra Detection IDs list - returns: Vectra API call result """ json_payload = {"detectionIdList": ids_list} # Execute request return self.http_request(method="PATCH", url_suffix=f"{API_ENDPOINT_DETECTIONS}/open", json_data=json_payload) def list_tags_request(self, entity_id: int = None, entity_type: str = None) -> dict: # type: ignore """ List tags for the specified entity. Args: entity_id (int): The ID of the Account/Host/Detection to list tags. entity_type (str): The type of the entity. Returns: Dict: Response from the API containing the tags. """ res = self.http_request(method="GET", url_suffix=f"{API_TAGGING}/{entity_type}/{entity_id}") return res def add_note_request(self, entity_id: int = None, entity_type: str = None, note: str = None) -> dict: # type: ignore """ Add a note to an Account, Host or Detection. Args: entity_id (int): The ID of the Account, Host or Detection to add the note to. entity_type (str): The type Account, Host or Detection. note (str): The note to add. Returns: Dict: Response from the API containing the added note. """ data = {"note": note} notes = self.http_request( method="POST", json_data=data, url_suffix=ENDPOINTS[f"ADD_AND_LIST_{entity_type.upper()}_NOTE_ENDPOINT"].format( # type: ignore[union-attr] entity_id ), # type: ignore ) return notes def update_note_request( self, entity_id: int = None, entity_type: str = None, note: str = None, # type: ignore note_id: int = None, ) -> dict: # type: ignore """ Updates the note of an Account, Host or Detection. Args: entity_id (int): The ID of the Account, Host or Detection to update the note for. entity_type (str): The type Account, Host or Detection. note (str): The updated note for the Account, Host or Detection. note_id (int): The ID of the note to be updated. Returns: Dict: Response from the API containing the updated note details. """ data = {"note": note} notes = self.http_request( method="PATCH", json_data=data, url_suffix=ENDPOINTS[f"UPDATE_AND_REMOVE_{entity_type.upper()}_NOTE_ENDPOINT"].format( # type: ignore[union-attr] entity_id, note_id ), ) return notes def remove_note_request( self, entity_type: str = None, entity_id: int = None, # type: ignore note_id: str = None, ) -> requests.Response: # type: ignore """ Remove a note from an Account, Host or Detection. Args: entity_id (int): The ID of the Account, Host or Detection to remove the note from . entity_type (str): The type Account, Host or Detection. note_id (int): The Id of the note to be removed. Returns: requests.Response: Response from the API containing the added note. """ res = self.http_request( method="DELETE", resp_type="response", url_suffix=ENDPOINTS[f"UPDATE_AND_REMOVE_{entity_type.upper()}_NOTE_ENDPOINT"].format( # type: ignore[union-attr] entity_id, note_id ), ) return res def list_note_request(self, entity_id: int = None, entity_type: str = None) -> dict: # type: ignore """ List Account/Host/Detection notes. Args: entity_id (int): The ID of the Account/Host/Detection to add the note to. entity_type (str): The type Account, Host or Detection. Returns: Dict: Response from the API. """ notes = self.http_request( method="GET", url_suffix=ENDPOINTS[f"ADD_AND_LIST_{entity_type.upper()}_NOTE_ENDPOINT"].format( # type: ignore[union-attr] entity_id ), ) return notes def get_account_by_account_id(self, account_id: str | None = None): """ Get Account by Account ID. - params: - account_id: The Account ID - returns: Vectra API call result. """ return self.http_request(method="GET", url_suffix=f"{API_ENDPOINT_ACCOUNTS}/{account_id}") def get_host_by_host_id(self, host_id: str | None = None): """ Get Host by Host ID. - params: - host_id: The Host ID - returns: Vectra API call result. """ return self.http_request(method="GET", url_suffix=f"{API_ENDPOINT_HOSTS}/{host_id}") def get_group_request(self, group_id: int = None) -> dict: # type: ignore """Get group by ID. Args: group_id (int): The ID of the group to retrieve. Returns: Dict: Response from the API containing the group information. """ return self.http_request(method="GET", url_suffix=f"{API_ENDPOINT_GROUPS}/{group_id}") def update_group_members_request(self, group_id: int = None, members: list = None) -> dict: # type: ignore """Update members in group. Args: group_id (int): The ID of the group to retrieve. members (List): The member list. Returns: Dict: Response from the API containing the group information. """ body = {"members": members} return self.http_request(method="PATCH", url_suffix=f"{API_ENDPOINT_GROUPS}/{group_id}", json_data=body) def list_assignments_request( self, accounts: str | None = None, hosts: str | None = None, page_size: int | None = None ) -> dict: """ Get Assignments by Accounts and Hosts. - params: - accounts: The Accounts IDs - hosts: The Host IDs - page_size: Total number of Assignments - returns: Vectra API call result. """ query_params = {} if page_size: query_params = {"page_size": page_size} if accounts: query_params["accounts"] = accounts # type: ignore if hosts: query_params["hosts"] = hosts # type: ignore return self.http_request(method="GET", url_suffix=f"{API_ENDPOINT_ASSIGNMENT}", params=query_params) def list_entity_tags_request(self, entity_id: str | None = None, entity_type: str | None = None): """ Get Entity Tags by Entity ID and Entity Type. - params: - entity_id: The Entity ID - entity_type: The Entity Type - returns: Vectra API call result. """ return self.http_request(method="GET", url_suffix=f"{API_TAGGING}/{entity_type}/{entity_id}") def update_entity_tags_request( self, entity_id: str | None = None, entity_type: str | None = None, tag_list: list | None = None ): """ Update Entity Tags by Entity ID and Entity Type. - params: - entity_id: The Entity ID - entity_type: The Entity Type - tag_list: The List of Tags - returns: Vectra API call result. """ tags = {"tags": tag_list} return self.http_request(method="PATCH", url_suffix=f"{API_TAGGING}/{entity_type}/{entity_id}", json_data=tags) def list_group_request( self, group_type: str = "", account_names: list[str] = [], domains: list[str] = [], host_ids: list[str] = [], host_names: list[str] = [], importance: str = "", ips: list[str] = [], description: str = "", last_modified_timestamp: datetime | None = None, last_modified_by: str = "", group_name: str = "", ): """ List groups as per the specified parameters. Args: group_type (str): Filter by group type. account_names (List[str]): Filter groups associated with accounts. domains (List[str]): Filter groups associated with domains. host_ids (List[str]): Filter groups associated with hosts. host_names (List[str]): Filter groups associated with hosts. importance (str): User defined group importance. ips (List[str]): Filter groups associated with ips. description (List[str]): Filter by group description. last_modified_timestamp (Optional[datetime]): Filters for all groups modified on or after the given timestamp (GTE). last_modified_by (str): Filters groups by the user id who made the most recent modification. group_name (str): Filters by group name. Returns: Dict: Response from the API containing the tags. """ params = assign_params( type=group_type, account_names=",".join(account_names), domains=",".join(domains), host_ids=",".join(host_ids), host_names=",".join(host_names), importance=importance, ips=",".join(ips), description=description, name=group_name, last_modified_timestamp=last_modified_timestamp, last_modified_by=last_modified_by, ) return self.http_request(method="GET", url_suffix=API_ENDPOINT_GROUPS, params=params) def close_detection_by_id(self, id: str, reason: str): """ Close a single detection by ID - params: - id: Vectra Detection ID - reason: Close reason (benign or remediated) - returns: Vectra API call result """ json_payload = {"reason": reason} # Execute request return self.http_request(method="PATCH", url_suffix=f"{API_ENDPOINT_DETECTIONS}/{id}/close", json_data=json_payload) def open_detection_by_id(self, id: str): """ Open a single detection by ID - params: - id: Vectra Detection ID - returns: Vectra API call result """ # Execute request return self.http_request(method="PATCH", url_suffix=f"{API_ENDPOINT_DETECTIONS}/{id}/open") # #### #### # # ## HELPER FUNCTIONS ## # # # def str2bool(value: str | None) -> bool | None: """ Converts a string into a boolean - params: - value: The string to convert - returns: True if value matches the 'true' list False if value matches the 'false' list None instead """ if value is None: output = None elif value.lower() in ("true", "yes"): output = True elif value.lower() in ("false", "no"): output = False else: output = None return output def sanitize_max_results(max_results=None) -> int: """ Cleans max_results value and ensure it's always lower than the MAX - params: max_results: The max results number - returns: The checked/enforced max results value """ if max_results and isinstance(max_results, str): max_results = int(max_results) if (not max_results) or (max_results > MAX_RESULTS) or (max_results <= 0): return MAX_RESULTS else: return max_results def scores_to_severity(threat: int | None, certainty: int | None) -> str: """ Converts Vectra scores to a severity String - params: - threat: The Vectra threat score - certainty: The Vectra certainty score - returns: The severity as text """ severity = "Unknown" if isinstance(threat, int) and isinstance(certainty, int): if threat < 50 and certainty < 50: severity = "Low" elif threat < 50: # and certainty >= 50 severity = "Medium" elif certainty < 50: # and threat >= 50 severity = "High" else: # threat >= 50 and certainty >= 50 severity = "Critical" return unify_severity(severity) def severity_string_to_int(severity: str | None) -> int: """ Converts a severity String to XSOAR severity value - params: - severity: The severity as text - returns: The XSOAR severity value """ output = 0 if severity == "Critical": output = 4 elif severity == "High": output = 3 elif severity == "Medium": output = 2 elif severity == "Low": output = 1 return output def convert_date(date: str | None) -> str | None: """ Converts a date format to an ISO8601 string Converts the Vectra date (YYYY-mm-ddTHH:MM:SSZ) format in a datetime. :type date: ``str`` :param date: a string with the format 'YYYY-mm-DDTHH:MM:SSZ' :return: Parsed time in ISO8601 format :rtype: ``str`` """ if date: date_dt = dateparser.parse(str(date)) if date_dt: return date_dt.strftime(DATE_FORMAT) else: return None else: return None def remove_space_from_args(args): """Remove space from args.""" for key in args: if isinstance(args[key], str): args[key] = args[key].strip() return args def validate_argument(label: str | None, value: Any) -> int: """ Validates a command argument based on its type - params: - label: The argument label - value: The argument value - returns: The value if OK or raises an Exception if not """ demisto.debug(f"Testing '{label}' argument value") if label in ["min_id", "max_id"]: try: if (value is None) or isinstance(value, float): raise ValueError("Cannot be empty or a float") if value and isinstance(value, str): value = int(value) if not isinstance(value, int): raise ValueError("Should be an int") if int(value) <= 0: raise ValueError("Should be > 0") except ValueError: raise ValueError(f'"{label}" must be an integer greater than 0') elif label in ["min_threat", "min_certainty", "max_threat", "max_certainty"]: try: if (value is None) or isinstance(value, float): raise ValueError("Cannot be empty or a float") if value and isinstance(value, str): value = int(value) if not isinstance(value, int): raise ValueError("Should be an int") if int(value) < 0: raise ValueError("Should be >= 0") if int(value) > 99: raise ValueError("Should be < 100") except ValueError: raise ValueError(f'"{label}" must be an integer between 0 and 99') elif label in ["min_privilege_level"]: try: if (value is None) or isinstance(value, float): raise ValueError("Cannot be empty or a float") if value and isinstance(value, str): value = int(value) if not isinstance(value, int): raise ValueError("Should be an int") if int(value) < 1: raise ValueError("Should be >= 1") if int(value) > 10: raise ValueError("Should be <= 10") except ValueError: raise ValueError(f'"{label}" must be an integer between 1 and 10') else: raise SystemError("Unknown argument type") return value def validate_min_max(min_label: str = None, min_value: str = None, max_label: str = None, max_value: str = None): # type: ignore """ Validates min/max values for a specific search attribute and ensure max_value >= min_value - params: - min_label: The attribute label for the min value - min_value: The min value - max_label: The attribute label for the max value - max_value: The max value - returns: Return True if OK or raises Exception if not """ if min_value: validate_argument(min_label, min_value) if max_value: validate_argument(max_label, max_value) if min_value and max_value and int(min_value) > int(max_value): raise ValueError(f'"{max_label}" must be greater than or equal to "{min_label}"') return True def validate_fetch_incident_params(integration_params: dict): """ Validates the integration parameters for fetching incidents, including timestamps, entity types, fetch queries, and the maximum number of incidents per fetch. Raises ValueErrors for invalid parameters. - params: - integration_params: A dictionary containing the integration parameters. - raises: ValueError: If the integration parameters are invalid. """ demisto.debug("Fetching mode is enabled. Testing settings ...") demisto.debug("Testing Fetch first timestamp ...") fetch_first_time = integration_params.get("first_fetch", DEFAULT_FIRST_FETCH) look_back = integration_params.get("look_back", BACK_IN_TIME_SEARCH_IN_MINUTES) demisto.debug(f"Fetch first timestamp : {fetch_first_time}") try: iso_date_to_vectra_start_time(fetch_first_time, look_back) except SystemError as exc: raise ValueError("Fetch first timestamp is invalid.") from exc demisto.debug("Testing Fetch first timestamp [done]") demisto.debug("Testing Fetch entity types ...") fetch_entity_types = integration_params.get("fetch_entity_types", DEFAULT_FETCH_ENTITY_TYPES) demisto.debug(f"Fetch entity types : {fetch_entity_types}") if len(fetch_entity_types) == 0: raise ValueError("You must select at least one entity type to fetch.") for entity_itt in fetch_entity_types: if entity_itt not in ENTITY_TYPES: raise ValueError(f'This entity type "{entity_itt}" is invalid.') demisto.debug("Testing Fetch entity types [done]") accounts_fetch_query = integration_params.get("accounts_fetch_query") demisto.debug(f"'Accounts' fetch query : {accounts_fetch_query}") hosts_fetch_query = integration_params.get("hosts_fetch_query") demisto.debug(f"'Hosts' fetch query : {hosts_fetch_query}") detections_fetch_query = integration_params.get("detections_fetch_query") demisto.debug(f"'Detections' fetch query : {detections_fetch_query}") demisto.debug("Testing Max incidents per fetch ...") max_incidents_per_fetch = integration_params.get("max_fetch", DEFAULT_MAX_FETCH) demisto.debug(f"Max incidents per fetch (initial value): {max_incidents_per_fetch}") if isinstance(max_incidents_per_fetch, str): try: max_incidents_per_fetch = int(max_incidents_per_fetch) except ValueError as exc: raise ValueError(ERRORS["INVALID_MAX_FETCH"].format(max_incidents_per_fetch)) from exc if max_incidents_per_fetch <= 0: raise ValueError(ERRORS["INVALID_MAX_FETCH"].format(max_incidents_per_fetch)) if (max_incidents_per_fetch // len(fetch_entity_types)) == 0: raise ValueError( f"Max incidents per fetch ({max_incidents_per_fetch}) must be >= " f"to the number of entity types you're fetching ({len(fetch_entity_types)})" ) demisto.debug(f"Max incidents per fetch (final value): {max_incidents_per_fetch}") demisto.debug("Testing Max incidents per fetch [done]") def build_search_query_for_detections( entity_id: str, fetch_type: str, detection_category: str = "", detection_type: str = "" ) -> str: """ Builds a search query for tags. - params: - entity_id: The ID of the entity. - fetch_type: The type of entity. - detection_category: The category of the detection. - detection_type: The type of the detection. - returns: Returns the search query. """ search_query = f"detection.src_linked_{fetch_type}.id:{entity_id}" if detection_category: search_query += f' AND detection.detection_category:"{detection_category}"' if detection_type: search_query += f' AND detection.detection_type:"{detection_type}"' return search_query def build_search_query_for_tags(fetch_query: str, fetch_type: str, tags: list[str]) -> str: """ Builds a search query for tags. - params: - fetch_query: The search query. - fetch_type: The type of entity. - tags: The list of tags. - returns: Returns the search query. """ if not tags: return fetch_query first_tag = tags[0] if "*" not in first_tag: first_tag = f'"{first_tag}"' tag_query = f"({fetch_type}.tags:{first_tag}" if len(tags) > 1: for tag in tags[1:]: if "*" in tag: tag_query += f" OR {fetch_type}.tags:{tag}" else: tag_query += f' OR {fetch_type}.tags:"{tag}"' tag_query += ")" if fetch_query: return f"{fetch_query} AND {tag_query}" return tag_query def sanitize_str_ids_list_to_set(list: str | None) -> set[int] | None: """ Sanitize the given list to ensure all IDs are valid - params: - list: The list to sanitize - returns: Returns the sanitized list (only valid IDs) """ output: set[int] = set() if list is not None and isinstance(list, str): ids_list = [id.strip() for id in list.split(",")] for id in ids_list: if id != "": try: validate_argument("min_id", id) except ValueError: raise ValueError(f'ID "{id}" is invalid') output.add(int(id)) if len(output) > 0: return output else: return None def build_search_query(object_type, params: dict) -> str: """ Builds a Lucene syntax search query depending on the object type to search on (Account, Detection, Host) - params: - object_type: The object type we're searching (Account, Detection, Host) - params: The search params - returns: The Lucene search query """ query = "" attribute = "" operator = "" for key, value in params.items(): if key.startswith("min_"): operator = ":>=" elif key.startswith("max_"): operator = ":<=" if key.endswith("_id"): attribute = "id" elif key.endswith("_threat"): attribute = "threat" elif key.endswith("_certainty"): attribute = "certainty" if key in ["state"]: attribute = key operator = ":" value = f'"{value}"' if key == "last_timestamp": operator = ":>=" if object_type == "detection": attribute = "last_timestamp" else: attribute = "last_detection_timestamp" if key == "doc_modified_time": operator = ":>=" attribute = "_doc_modified_ts" # Append query # No need to add "AND" as implied query += f" {object_type}.{attribute}{operator}{value}" return query.strip() def forge_entity_url(type: str, id: str | None) -> str: """ Generate the UI pivot URL - params: - type: The object type ("account", "detection" or "host") - id: The object ID - returns: The pivot URL using server FQDN """ if type == "account": url_suffix = f"{UI_ACCOUNTS}/" elif type == "detection": url_suffix = f"{UI_DETECTIONS}/" elif type == "host": url_suffix = f"{UI_HOSTS}/" else: raise Exception(f"Unknown type : {type}") if not id: raise Exception("Missing ID") return urljoin(urljoin(global_UI_URL, url_suffix), str(id)) + UTM_PIVOT def common_extract_data(entity: dict[str, Any]) -> dict[str, Any]: """ Extracts common information from Vectra object renaming attributes on the fly. - params: - host: The Vectra object - returns: The extracted data """ return { "Assignee": entity.get("assigned_to"), "AssignedDate": convert_date(entity.get("assigned_date")), "CertaintyScore": entity.get("certainty"), "ID": entity.get("id"), "State": entity.get("state"), "Tags": entity.get("tags"), "ThreatScore": entity.get("threat"), } def extract_account_data(account: dict[str, Any]) -> dict[str, Any]: """ Extracts useful information from Vectra Account object renaming attributes on the fly. - params: - host: The Vectra Account object - returns: The Account extracted data """ return common_extract_data(account) | { # type: ignore "LastDetectionTimestamp": convert_date(account.get("last_detection_timestamp")), "PrivilegeLevel": account.get("privilege_level"), "PrivilegeCategory": account.get("privilege_category"), "Severity": unify_severity(account.get("severity")), "Type": account.get("account_type"), "URL": forge_entity_url("account", account.get("id")), "Username": account.get("name"), "DocModifiedTimestamp": convert_date(account.get("_doc_modified_ts")), } def extract_detection_data(detection: dict[str, Any]) -> dict[str, Any]: """ Extracts useful information from Vectra Detection object renaming attributes on the fly. - params: - host: The Vectra Detection object - returns: The Detection extracted data """ # Complex values detection_name = detection.get("custom_detection") if detection.get("custom_detection") else detection.get("detection") source_account = detection.get("src_account") source_account_id = source_account.get("id") if source_account else None source_host = detection.get("src_host") source_host_id = source_host.get("id") if source_host else None summary = detection.get("summary") if summary: description = summary.get("description") dst_ips = summary.get("dst_ips") dst_ports = summary.get("dst_ports") else: description = dst_ips = dst_ports = None return common_extract_data(detection) | remove_empty_elements( { "Category": detection.get("category"), "Description": description, "DestinationIPs": dst_ips, "DestinationPorts": dst_ports, "FirstTimestamp": convert_date(detection.get("first_timestamp")), "IsTargetingKeyAsset": detection.get("is_targeting_key_asset"), "LastTimestamp": convert_date(detection.get("last_timestamp")), "Name": detection_name, "Severity": scores_to_severity(detection.get("threat"), detection.get("certainty")), "SensorLUID": detection.get("sensor"), "SensorName": detection.get("sensor_name"), "SourceAccountID": source_account_id, "SourceHostID": source_host_id, "SourceIP": detection.get("src_ip"), "TriageRuleID": detection.get("triage_rule_id"), "Type": detection.get("detection"), "URL": forge_entity_url("detection", detection.get("id")), } ) def extract_host_data(host: dict[str, Any]) -> dict[str, Any]: """ Extracts useful information from Vectra Host object renaming attributes on the fly. - params: - host: The Vectra Hosts object - returns: The Host extracted data """ return common_extract_data(host) | { # type: ignore "HasActiveTraffic": host.get("has_active_traffic"), "Hostname": host.get("name"), "IPAddress": host.get("ip"), "IsKeyAsset": host.get("is_key_asset"), "IsTargetingKeyAsset": host.get("is_targeting_key_asset"), "LastDetectionTimestamp": convert_date(host.get("last_detection_timestamp")), "PrivilegeLevel": host.get("privilege_level"), "PrivilegeCategory": host.get("privilege_category"), "ProbableOwner": host.get("probable_owner"), "SensorLUID": host.get("sensor"), "SensorName": host.get("sensor_name"), "Severity": unify_severity(host.get("severity")), "URL": forge_entity_url("host", host.get("id")), "DocModifiedTimestamp": convert_date(host.get("_doc_modified_ts")), } def extract_assignment_data(assignment: dict[str, Any]) -> dict[str, Any]: """ Extracts useful information from Vectra Assignment object renaming attributes on the fly. - params: - assignment: The Vectra Assignment object - returns: The Assignment extracted data """ assigned_by = assignment.get("assigned_by") assigned_by_user = assigned_by.get("username") if assigned_by else None assigned_to = assignment.get("assigned_to") assigned_to_user = assigned_to.get("username") if assigned_to else None outcome = assignment.get("outcome") outcome_title = outcome.get("title") if outcome else None outcome_category = outcome.get("category") if outcome else None resolved_by = assignment.get("resolved_by") resolved_by_user = resolved_by.get("username") if resolved_by else None # assignment['events'][0]['context'] is always present triaged_as = assignment["events"][0]["context"].get("triage_as") return remove_empty_elements( { "AccountID": assignment.get("account_id"), "AssignedBy": assigned_by_user, "AssignedDate": convert_date(assignment.get("date_assigned")), "AssignedTo": assigned_to_user, "HostID": assignment.get("host_id"), "ID": assignment.get("id"), "IsResolved": assignment.get("resolved_by") is not None, "OutcomeCategory": convert_outcome_category_raw2text(outcome_category), "OutcomeTitle": outcome_title, "TriagedDetections": assignment.get("triaged_detections"), "TriagedAs": triaged_as, "ResolvedBy": resolved_by_user, "ResolvedDate": convert_date(assignment.get("date_resolved")), } ) def extract_outcome_data(outcome: dict[str, Any]) -> dict[str, Any]: """ Extracts useful information from Vectra Outcome object renaming attributes on the fly. - params: - outcome: The Vectra Outcome object - returns: The Outcome extracted data """ return { "Category": convert_outcome_category_raw2text(outcome.get("category")), "ID": outcome.get("id"), "IsBuiltIn": outcome.get("builtin"), "Title": outcome.get("title"), } def extract_user_data(user: dict[str, Any]) -> dict[str, Any]: """ Extracts useful information from Vectra User object renaming attributes on the fly. - params: - user: The Vectra User object - returns: The User extracted data """ return { "Email": user.get("email"), "ID": user.get("id"), "Role": user.get("role"), "Type": user.get("account_type"), "Username": user.get("username"), "LastLoginDate": convert_date(user.get("last_login")), } def detection_to_incident(detection: dict): """ Creates an incident of a Detection. :type detection: ``dict`` :param detection: Single detection object :return: Incident representation of a Detection :rtype ``dict`` """ extracted_data = extract_detection_data(detection) incident_name = f"Vectra Detection ID: {extracted_data.get('ID')} - {extracted_data.get('Name')}" vectra_specific = { "entity_type": extracted_data.get("Category"), "UI_URL": extracted_data.get("URL"), } detection.update({"_vectra_specific": vectra_specific}) incident = { "name": incident_name, # name is required field, must be set "occurred": extracted_data.get("LastTimestamp"), # must be string of a format ISO8601 "rawJSON": json.dumps(detection), # the original event, # this will allow mapping of the event in the mapping stage. # Don't forget to `json.dumps` "severity": severity_string_to_int(extracted_data.get("Severity")), # 'dbotMirrorId': extracted_data.get('ID') } incident_last_run = { "last_timestamp": dateparser.parse( extracted_data.get("LastTimestamp"), # type: ignore settings={"TO_TIMEZONE": "UTC"}, ).isoformat(), # type: ignore "id": extracted_data.get("ID"), } return incident, incident_last_run def host_to_incident( client: Client, host: dict, detection_category: str = "", detection_type: str = "", fetch_escalated_accounts_and_hosts: bool = False, ): """ Creates an incident of a Host. :type client: ``Client`` :param client: The client object used to communicate with the Vectra API. :type host: ``dict`` :param host: Single Host object. :type detection_category: ``str`` :param detection_category: The category of the detection. :type detection_type: ``str`` :param detection_type: The type of the detection. :type fetch_escalated_accounts_and_hosts: ``bool`` :param fetch_escalated_accounts_and_hosts: Whether to fetch escalated accounts and hosts. :return: Incident representation of a Host. :rtype ``dict`` """ extracted_data = extract_host_data(host) incident_name = f"Vectra Host ID: {extracted_data.get('ID')} - {extracted_data.get('Hostname')}" detections = [] detections_data = client.list_detections_by_host_id(str(host.get("id")), "active", detection_category, detection_type) detections = detections_data.get("results", []) for detection in detections: detection["url"] = forge_entity_url("detection", detection.get("id")) demisto.debug(f"Found {len(detections)} detection(s) for the host with the ID: {host.get('id')}.") host.update({"detection_details": detections}) response = client.list_assignments_request(hosts=host.get("id"), page_size=1) assignment_details = response.get("results", []) assignment_details = assignment_details[0] if len(assignment_details) > 0 else {} if assignment_details: if not assignment_details.get("resolved_by"): assignment_details["resolved_by"] = {"username": ""} assignment_details["outcome"] = {"title": ""} assignment_details["date_resolved"] = "" else: assignment_details = EMPTY_ASSIGNMENT host.update({"assignment_details": assignment_details}) vectra_specific = { "entity_type": "host", "UI_URL": extracted_data.get("URL"), } host.update({"_vectra_specific": vectra_specific}) mirroring_fields = get_mirroring() mirroring_fields.update({"mirror_id": str(host.get("id")) + "-" + "host"}) host.update(mirroring_fields) calculated_severity = severity_string_to_int(scores_to_severity(host.get("threat"), host.get("certainty"))) host.update({"calculated_severity": calculated_severity}) incident = { "name": incident_name, # name is required field, must be set "occurred": extracted_data.get("LastDetectionTimestamp"), # must be string of a format ISO8601 "rawJSON": json.dumps(host), # the original event, # this will allow mapping of the event in the mapping stage. # Don't forget to `json.dumps` "severity": host.get("calculated_severity"), # 'dbotMirrorId': extracted_data.get('ID') } incident_last_run = { "last_timestamp": dateparser.parse( ( extracted_data.get("DocModifiedTimestamp") # type: ignore if fetch_escalated_accounts_and_hosts else extracted_data.get("LastDetectionTimestamp") ), settings={"TO_TIMEZONE": "UTC"}, ).isoformat(), "id": extracted_data.get("ID"), } return incident, incident_last_run def account_to_incident( client: Client, account: dict, detection_category: str = "", detection_type: str = "", fetch_escalated_accounts_and_hosts: bool = False, ): """ Creates an incident of an Account. :type client: ``Client`` :param client: The client object used to communicate with the Vectra API. :type host: ``dict`` :param host: Single Account object :type detection_category: ``str`` :param detection_category: The category of the detection. :type detection_type: ``str`` :param detection_type: The type of the detection. :type fetch_escalated_accounts_and_hosts: ``bool`` :param fetch_escalated_accounts_and_hosts: Whether to fetch escalated accounts and hosts. :return: Incident representation of a Account :rtype ``dict`` """ extracted_data = extract_account_data(account) account_id: str = extracted_data.get("ID", "") incident_name = f"Vectra Account ID: {account_id} - {account.get('display_name')}" search_query = build_search_query_for_detections(account_id, "account", detection_category, detection_type) api_response = client.search_detections(state="active", search_query=search_query) if (api_response is None) or (api_response.get("count") is None): raise VectraException("API issue - Response is empty or invalid") detections = [] if api_response.get("count") == 0: demisto.info(f"Found 0 detection(s) for the account with the ID: {account_id}.") elif api_response.get("count", 0) > 0: demisto.info(f"Found {api_response.get('count')} detection(s) for the account with the ID: {account_id}.") if api_response.get("results") is None: raise VectraException("API issue - Response is empty or invalid") # Due to backward search we need to avoid creating incidents of already ingested events detections = api_response.get("results", []) for detection in detections: detection["url"] = forge_entity_url("detection", detection.get("id")) groups_response = client.list_group_request(group_type="account", account_names=[account.get("display_name")]) # type: ignore account.update({"groups": groups_response.get("results", [])}) # Add detection details to the entity account.update({"detection_details": detections}) response = client.list_assignments_request(accounts=account.get("id"), page_size=1) assignment_details = response.get("results", []) assignment_details = assignment_details[0] if len(assignment_details) > 0 else {} if assignment_details: if not assignment_details.get("resolved_by"): assignment_details["resolved_by"] = {"username": ""} assignment_details["outcome"] = {"title": ""} assignment_details["date_resolved"] = "" else: assignment_details = EMPTY_ASSIGNMENT account.update({"assignment_details": assignment_details}) vectra_specific = { "entity_type": "account", "UI_URL": extracted_data.get("URL"), } account.update({"_vectra_specific": vectra_specific}) mirroring_fields = get_mirroring() mirroring_fields.update({"mirror_id": str(account.get("id")) + "-" + "account"}) account.update(mirroring_fields) calculated_severity = severity_string_to_int(scores_to_severity(account.get("threat"), account.get("certainty"))) account.update({"calculated_severity": calculated_severity}) incident = { "name": incident_name, # name is required field, must be set "occurred": extracted_data.get("LastDetectionTimestamp"), # must be string of a format ISO8601 "rawJSON": json.dumps(account), # the original event, # this will allow mapping of the event in the mapping stage. # Don't forget to `json.dumps` "severity": account.get("calculated_severity"), } incident_last_run = { "last_timestamp": dateparser.parse( ( extracted_data.get("DocModifiedTimestamp") # type: ignore if fetch_escalated_accounts_and_hosts else extracted_data.get("LastDetectionTimestamp") ), settings={"TO_TIMEZONE": "UTC"}, ).isoformat(), "id": account_id, } return incident, incident_last_run def get_last_run_details(integration_params: dict, is_test: bool = False) -> dict: """ Extracts detail from the stored last_run variable or create them if needed :type integration_params: ``dict`` :param integration_params: The integration configuration parameters. :type is_test: ``bool`` :param is_test: A boolean indicating whether the command is being run in test mode. :return: Last run content. :rtype ``dict`` """ # Get the config settings fetch_first_time = integration_params.get("first_fetch", DEFAULT_FIRST_FETCH) fetch_entity_types = integration_params.get("fetch_entity_types", DEFAULT_FETCH_ENTITY_TYPES) # Get the last run value last_run = {} if is_test else demisto.getLastRun() demisto.debug(f"last run : {last_run}") output_last_run: dict = {} for entity_type in ENTITY_TYPES: if entity_type in fetch_entity_types: # This will return a relative TZaware datetime (in UTC) last_timestamp = dateparser.parse( fetch_first_time, # type: ignore settings={"TO_TIMEZONE": "UTC"}, ).isoformat() # type: ignore last_id = 0 empty_last_run: dict = {"last_timestamp": last_timestamp, "id": last_id, "last_created_events": []} output_last_run[entity_type] = empty_last_run if not last_run.get(entity_type): demisto.debug(f"Last run is not set for '{entity_type}'. Using value from config : {fetch_first_time}") # This will return a relative TZaware datetime (in UTC) demisto.debug(f"New last run for {entity_type}, {output_last_run[entity_type]}") else: output_last_run[entity_type].update(last_run.get(entity_type, {})) elif last_run.get(entity_type): demisto.debug(f"'{entity_type} present in last run but no more used, discarding.") return output_last_run def iso_date_to_vectra_start_time(iso_date: str, look_back: str = BACK_IN_TIME_SEARCH_IN_MINUTES): """ Converts an iso date into a Vectra timestamp used in search query. - params: - iso_date: The ISO date to convert - look_back: The look back time in minutes. - returns: A Vectra date timestamp. """ # This will return a relative TZaware datetime (in UTC) date = dateparser.parse(iso_date, settings={"TO_TIMEZONE": "UTC"}) # type: ignore if date: # We should return time in YYYY-MM-DDTHHMM format for Vectra Lucene query search ... start_datetime = date.strftime(r"%Y-%m-%dT%H%M") demisto.debug(f"Start datetime is : {start_datetime}") if look_back: look_back_int = arg_to_number(look_back, "look_back", True) if look_back_int < 0: # type: ignore raise ValueError(ERRORS["POSITIVE_VALUE"].format("look back")) demisto.debug(f"The look back is : {look_back_int} minutes. ") if look_back_int != 0: # Manipulate the date if we need to search backward # Timedelta is imported from CommonServerPython date = date - timedelta(minutes=look_back_int) # type: ignore backward_start_datetime = date.strftime("%Y-%m-%dT%H%M") # type: ignore demisto.debug( "Manipulated time as backward search. " f"Changed the Start time from : {start_datetime} to : {backward_start_datetime}" ) start_datetime = backward_start_datetime else: raise SystemError("Invalid ISO date") return start_datetime def unify_severity(severity: str | None) -> str: """ Force severity string to be consistent across endpoints - params: - severity: The severity string - returns: The unified severity string (First capitalized letter) """ if severity: output = severity.capitalize() else: output = "Unknown" return output def convert_outcome_category_raw2text(category: str | None) -> str | None: """ Convert outcome category from raw to human readable text - params: - category: The raw outcome category string - returns: The human readable outcome category string """ return OUTCOME_CATEGORIES.get(category) if category else None def convert_outcome_category_text2raw(category: str) -> str | None: """ Convert outcome category from human readable text to raw - params: - category: The human readable outcome category string - returns: The raw outcome category string """ # Inverting Key/Value category_text = {v: k for k, v in OUTCOME_CATEGORIES.items()} return category_text.get(category) if category else None def validate_positive_integer_arg(value: Any | None, arg_name: str, required: bool = False) -> bool: """ Validates whether the provided argument value is a valid positive integer. Args: value (int): The value to validate. arg_name (str): The name of the argument. required (bool): Flag indicating if the argument is required (default: False). Returns: bool: True if the value is a valid positive integer. Raises: ValueError: If the value is not a valid positive integer. """ if required and not value: raise ValueError(ERRORS["REQUIRED_ARGUMENT"].format(arg_name)) if value is not None and (not str(value).isdigit() or int(value) <= 0): raise ValueError(ERRORS["INVALID_INTEGER_VALUE"].format(arg_name)) return True def validate_group_list_command_args(args: dict[Any, Any]): """ Validates the arguments provided for the group list command. Args: args (dict[Any, Any]): The arguments dictionary. Raises: ValueError: If any of the arguments are invalid. """ group_type = args.get("group_type", "") if group_type and isinstance(group_type, str): group_type = group_type.lower() # Validate group_type value if group_type not in VALID_GROUP_TYPE: raise ValueError(ERRORS["INVALID_COMMAND_ARG_VALUE"].format("group_type", ", ".join(VALID_GROUP_TYPE))) importance = args.get("importance", "") # Validate importance value if importance and isinstance(importance, str) and importance.lower() not in VALID_IMPORTANCE_VALUE: raise ValueError(ERRORS["INVALID_COMMAND_ARG_VALUE"].format("importance", ", ".join(VALID_IMPORTANCE_VALUE))) # Validate account_names value account_names = argToList(args.get("account_names", "")) if account_names and group_type != "account": raise ValueError(ERRORS["INVALID_SUPPORT_FOR_ARG"].format("group_type", "account", "account_names")) # Validate domains value domains = argToList(args.get("domains", "")) if domains and group_type != "domain": raise ValueError(ERRORS["INVALID_SUPPORT_FOR_ARG"].format("group_type", "domain", "domains")) # Validate host_ids value host_ids = argToList(args.get("host_ids", "")) if host_ids and group_type != "host": raise ValueError(ERRORS["INVALID_SUPPORT_FOR_ARG"].format("group_type", "host", "host_ids")) for host_id in host_ids: host_id = arg_to_number(host_id, "host_ids") validate_positive_integer_arg(host_id, arg_name="host_ids") # Validate host_names value host_names = argToList(args.get("host_names", "")) if host_names and group_type != "host": raise ValueError(ERRORS["INVALID_SUPPORT_FOR_ARG"].format("group_type", "host", "host_names")) # Validate ips value ips = argToList(args.get("ips", "")) if ips and group_type != "ip": raise ValueError(ERRORS["INVALID_SUPPORT_FOR_ARG"].format("group_type", "ip", "ips")) def get_group_list_command_hr(groups: list): """ Converts a list of groups into a human-readable table format. Args: groups (Dict): The list of groups to convert. Returns: str: The human-readable table in Markdown format. """ hr_dict = [] # Process members data from group and make HR for groups for group in groups: # type: ignore group["group_id"] = group["id"] members: list = group.get("members") members_hr = None if members and isinstance(members, list): # If the members are simple list of strings, then join them with comma. if isinstance(members[0], str): members_hr = ", ".join([(str(member)) for member in members]) # If the members are list of dictionaries, then extract important field from that and join it with comma. elif isinstance(members[0], dict): members_list = [] for member in members: if member.get("uid"): members_list.append(str(member.get("uid"))) # type: ignore elif member.get("id"): members_list.append( # type: ignore f"[{member.get('id')}]({forge_entity_url(group.get('type'), member.get('id'))})" ) members_hr = ", ".join(members_list) hr_dict.append( { "Group ID": group.get("group_id"), "Name": group.get("name"), "Group Type": group.get("type"), "Description": group.get("description"), "Importance": group.get("importance"), "Members": members_hr, "Last Modified Timestamp": group.get("last_modified"), } ) # Prepare human-readable output table human_readable = tableToMarkdown( "Groups Table", hr_dict, ["Group ID", "Name", "Group Type", "Description", "Importance", "Members", "Last Modified Timestamp"], removeNull=True, ) return human_readable def validate_group_assign_and_unassign_command_args(args): """ Validate the arguments provided for assigning or unassigning members to/from a group. Args: args (Dict): A dictionary containing the arguments for the group assign and unassign command. Raises: ValueError: If the provided group_id is not a positive integer. ValueError: If members argument is missing. """ group_id = args.get("group_id") members = args.get("members") validate_positive_integer_arg(group_id, arg_name="group_id", required=True) if not members: raise ValueError(ERRORS["REQUIRED_ARGUMENT"].format("members")) def get_group_unassign_and_assign_command_hr(group: dict, changed_members: list, assign_flag: bool = False): """ Converts group into a human-readable table format. Args: group (Dict): The group to convert. changed_members (List): Removed/Added members from the group. assign_flag (bool): True for unassigning members, False for assigning members. Returns: str: The human-readable table in Markdown format. """ hr_dict = [] group["group_id"] = group["id"] members = group.get("members") members_hr = None if members and isinstance(members, list): # If the members are simple list of strings, then join them with comma. if isinstance(members[0], str): members_hr = ", ".join([(str(member)) for member in members]) # If the members are list of dictionaries, then extract important field from that and join it with comma. elif isinstance(members[0], dict): members_list = [] for member in members: if member.get("uid"): members_list.append(str(member.get("uid"))) # type: ignore elif member.get("id"): members_list.append( # type: ignore f"[{member.get('id')}]({forge_entity_url(str(group.get('type')), member.get('id'))})" ) members_hr = ", ".join(members_list) hr_dict.append( { "Group ID": group.get("group_id"), "Name": group.get("name"), "Group Type": group.get("type"), "Description": group.get("description"), "Members": members_hr, "Last Modified Timestamp": group.get("last_modified"), } ) if group.get("type").lower() == "account" and assign_flag is True: # type: ignore new_changed_members = [] ignored_members = [] accounts = [account.get("uid") for account in group.get("members", [])] for member in changed_members: if member in accounts: new_changed_members.append(member) else: ignored_members.append(member) if ignored_members: return_warning( f"The following account names were invalid: {', '.join(ignored_members)}", exit=len(ignored_members) == len(changed_members), ) changed_members = new_changed_members # Prepare human-readable output table change_action = "assigned to" if assign_flag else "unassigned from" human_readable = tableToMarkdown( f"Member(s) {', '.join(changed_members)} have been {change_action} the group.\n### Updated group details:", hr_dict, ["Group ID", "Name", "Group Type", "Description", "Members", "Last Modified Timestamp"], removeNull=True, ) return human_readable def validate_note_add_command_args(entity_id: int | None, note: str, entity_type: str): """ Validates the arguments provided for the note add command. Args: entity_id (int): The ID of the object. note (str): The note to add. Raises: ValueError: If any of the arguments are invalid. """ # Validate id value validate_positive_integer_arg(entity_id, arg_name=f"{entity_type}_id", required=True) # Validate note value if not note: raise ValueError(ERRORS["REQUIRED_ARGUMENT"].format("note")) def validate_note_update_command_args(entity_id: int | None, note_id: int | None, note: str, entity_type: str): """ Validates the arguments provided for the note update command. Args: entity_id (int): The ID of the object. note_id (int): The ID of the note to be updated. note (str): The note to update. Raises: ValueError: If any of the arguments are invalid. """ # Validate entity_id and note_id value validate_positive_integer_arg(entity_id, arg_name=f"{entity_type}_id", required=True) validate_positive_integer_arg(note_id, arg_name="note_id", required=True) # Validate note value if not note: raise ValueError(ERRORS["REQUIRED_ARGUMENT"].format("note")) def validate_note_remove_command_args(entity_id: int | None, note_id: int | None, entity_type: str): """ Validates the arguments provided for the note remove command. Args: entity_id (int): The ID of the object. note_id (int): The ID of the note to be removed. entity_type (str): The type from Account, Host or Detection. Raises: ValueError: If any of the arguments are invalid. """ # Validate entity_id value validate_positive_integer_arg(entity_id, arg_name=f"{entity_type}_id", required=True) # Validate note_id value validate_positive_integer_arg(note_id, arg_name="note_id", required=True) return True def get_list_notes_command_hr(notes: dict, entity_id: int | None, entity_type: str) -> str: # type: ignore """ Returns the human-readable output for the Account, Host or Detection notes. Args: notes (Dict): The assignment details dictionary. entity_id (Optional[int]): Account/Host/Detection ID. entity_type (str): Account, Host or Detection. Returns: str: The human-readable output. """ hr_dict = [] for note in notes: note.update({"note_id": note["id"]}) del note["id"] note.update({f"{entity_type}_id": entity_id}) hr_dict.append( { "Note ID": note.get("note_id"), "Note": note.get("note"), "Created By": note.get("created_by"), "Created Date": note.get("date_created"), "Modified By": note.get("modified_by"), "Modified Date": note.get("date_modified"), } ) # Prepare human-readable output table human_readable = tableToMarkdown( "Notes Table", hr_dict, ["Note ID", "Note", "Created By", "Created Date", "Modified By", "Modified Date"], removeNull=True ) return human_readable def get_mirroring(): """ Get the mirroring configuration parameters from the Demisto integration parameters. Returns: dict: A dictionary containing the mirroring configuration parameters. """ params = demisto.params() mirror_direction = params.get("mirror_direction", "None").strip() mirror_tags = params.get("note_tag", "").strip() return { "mirror_direction": MIRROR_DIRECTION.get(mirror_direction), "mirror_tags": mirror_tags, "mirror_instance": demisto.integrationInstance(), } def reopen_in_xsoar(entries: list, entity_id_type: list): """Reopen the XSOAR incident for the given entity. Args: entries (list): List of entries where the reopening entry will be appended. entity_id_type (list): Indicates the entity ID and type. """ demisto.debug(f"Reopening the incident with remote entity ID: {entity_id_type}.") entries.append({"Type": EntryType.NOTE, "Contents": {"dbotIncidentReopen": True}, "ContentsFormat": EntryFormat.JSON}) def add_notes_to_new_entries(notes: list, command_last_run_dt: datetime | None) -> list: """ Lists notes from vectra that to be added when mirrored on xsoar Args: notes (list): Notes from vectra platform command_last_run_dt (str): last time when command was ran on XSOAR Returns: list: list of notes to be added in XSOAR """ if not notes: return [] new_entry_notes: list[dict] = [] for note in notes: if "[Mirrored From XSOAR]" in note.get("note"): demisto.debug(f"Skipping the note {note.get('id')} as it is mirrored from XSOAR.") continue note_date_modified = arg_to_datetime(note.get("date_modified")) if note_date_modified and note_date_modified <= command_last_run_dt: # type: ignore demisto.debug(f"Skipping the note {note.get('id')} as it was modified earlier than the command last run timestamp.") continue else: note_date_created = arg_to_datetime(note.get("date_created"), arg_name="date_created", required=True) if note_date_created <= command_last_run_dt: # type: ignore demisto.debug(f"Skipping the note {note.get('id')} as it is older than the command last run timestamp.") continue if "\n" in note.get("note"): note_info = f"\n{note.get('note')}" else: note_info = note.get("note") new_entry_notes.append( { "Type": EntryType.NOTE, "Contents": f"[Mirrored From Vectra]\n" f"Added By: {note.get('created_by')}\n" f"Added At: {note.get('date_created')} UTC\n" f"Note: {note_info}", "ContentsFormat": EntryFormat.MARKDOWN, "Note": True, } ) return new_entry_notes def add_refetch_id_to_integration_context(entity_id: str, entity_type: str): """ Adds the entity ID and type to the integration context. Args: entity_id (str): The ID of the entity. entity_type (str): The type of the entity. """ if not entity_id or not entity_type: raise ValueError("Both 'entity_id' and 'entity_type' arguments are required.") if entity_type not in ENTITY_TYPES_FOR_MIRRORING: raise ValueError(ERRORS["INVALID_COMMAND_ARG_VALUE"].format("entity_type", ", ".join(ENTITY_TYPES_FOR_MIRRORING))) # Get current integration context integration_context = get_integration_context() entity_type = "Accounts" if entity_type == "account" else "Hosts" # Initialize refetch_ids if it doesn't exist if entity_type not in integration_context: integration_context[entity_type] = {} if "refetch_ids" not in integration_context[entity_type]: integration_context[entity_type]["refetch_ids"] = [] # Create the refetch entry refetch_entry = f"{entity_type}_{entity_id}" demisto.debug(f"Adding entity id to the integration context: {refetch_entry}") # Add to refetch list if not already present if refetch_entry not in integration_context[entity_type]["refetch_ids"]: integration_context[entity_type]["refetch_ids"].append(refetch_entry) # Update integration context set_integration_context(integration_context) demisto.debug(f"Updated {entity_type} ids list in the integration context: {integration_context[entity_type]['refetch_ids']}") def get_valid_and_dropped_tags(tags: list[str]) -> tuple[list[str], list[str]]: """ Return (valid_tags, dropped_tags) using TAG_REGEX.fullmatch(). Note: does not strip/mutate inputs. If you want trimming, do it before calling. """ valid: list[str] = [] invalid: list[str] = [] for t in tags: if TAGS_REGEX.fullmatch(t): valid.append(t) else: invalid.append(t) if invalid: demisto.debug(f"Dropping invalid tags which contains invalid characters: {invalid}") demisto.debug(f"Provided Valid tags(s): {valid}") return valid, invalid class VectraException(Exception): """ Custom Vectra Exception in case of Vectra API issue """ # #### #### # # ## COMMAND FUNCTIONS ## # # # def test_module(client: Client, integration_params: dict) -> str: """ Tests API connectivity and authentication. Returning 'ok' indicates that the integration works like it is supposed to. Connection to the service is successful. Raises exceptions if something goes wrong. - params: - client: The API Client - integration_params: All additional integration settings - returns: 'ok' if test passed, anything else if at least one test failed. """ try: last_timestamp = None if integration_params.get("isFetch"): fetch_incidents(client, integration_params, True) else: # Client class should raise the exceptions, but if the test fails # the exception text is printed to the Cortex XSOAR UI. client.search_detections(max_results=1, last_timestamp=last_timestamp) message = "ok" except ValueError as e: message = str(e) demisto.debug(message) except DemistoException as e: if "Invalid token" in str(e): message = "Authorization Error: make sure API Token is properly set" demisto.debug(message) elif "Error occurred while creating an authorization token" in str(e): message = "Authorization Error: make sure Client ID and Client Secret are properly set" demisto.debug(message) elif "Verify that the server URL parameter is correct" in str(e): message = "Verify that the Vectra Server FQDN or IP is correct and that you have access to the server from your host" demisto.debug(message) else: raise e return message def fetch_incidents(client: Client, integration_params: dict, is_test: bool = False): """ Fetches incidents based on the provided client and integration parameters. :param client: The client object used to communicate with the Vectra API. :param integration_params: A dictionary containing integration parameters like fetch entity types and queries. :param is_test: A boolean indicating whether the command is being run in test mode. :return: A tuple containing the new last run details and a list of fetched incidents. """ validate_fetch_incident_params(integration_params) fetch_entity_types = integration_params.get("fetch_entity_types", DEFAULT_FETCH_ENTITY_TYPES) look_back = integration_params.get("look_back", BACK_IN_TIME_SEARCH_IN_MINUTES) tags = argToList(integration_params.get("tags", "")) tags = [tag.strip() for tag in tags if tag.strip()] detection_category = integration_params.get("detection_category", "") detection_type = integration_params.get("detection_type", "").strip() create_multiple_incidents_by_timestamp = integration_params.get("create_multiple_incidents_by_timestamp", False) api_response: dict = {} # Get the last run and the last fetched value previous_last_run = get_last_run_details(integration_params, is_test) incidents = [] new_last_run: dict = previous_last_run # We split the number of incidents to create into the number of remaining endpoints to call remaining_fetch_types: set = fetch_entity_types max_fetch = arg_to_number(integration_params.get("max_fetch", DEFAULT_MAX_FETCH)) if max_fetch > MAX_RESULTS: # type: ignore if is_test: raise ValueError(ERRORS["INVALID_MAX_FETCH"].format(max_fetch)) demisto.debug( f"The value for the Max Fetch parameter is {max_fetch} which is greater than " f"{MAX_RESULTS}, so reducing it to {MAX_RESULTS}." ) max_fetch = MAX_RESULTS max_created_incidents: int = max_fetch // len(remaining_fetch_types) # type: ignore for entity_type in ENTITY_TYPES: entity_incidents: list = [] if entity_type not in fetch_entity_types: continue last_fetched_timestamp = previous_last_run[entity_type]["last_timestamp"] last_fetched_id = previous_last_run[entity_type]["id"] # Forced to use "get" as this field wasn't present in the first version of this integration last_created_events = previous_last_run[entity_type].get("last_created_events", []) # Retro-compat integration_context = get_integration_context() refetch_ids = integration_context.get(entity_type, {}).get("refetch_ids", []) # Remove refetch IDs from already_fetched and create the removed_ids list removed_ids = [] for refetch_id in refetch_ids: if refetch_id in last_created_events: removed_ids.append(refetch_id) last_created_events.remove(refetch_id) demisto.debug(f"Removed closed {entity_type} ids from last run checkpoint: {removed_ids}") # Initialize entity_type in integration_context if it doesn't exist if entity_type not in integration_context: integration_context[entity_type] = {} # Update integration context with empty refetch_ids integration_context[entity_type]["refetch_ids"] = [] # Set the updated integration context set_integration_context(integration_context) demisto.debug(f"{entity_type} - Last fetched incidentlast_timestamp : {last_fetched_timestamp} / ID : {last_fetched_id}") start_time = iso_date_to_vectra_start_time(last_fetched_timestamp, look_back) order_field = "last_detection_timestamp" last_detection_time, doc_modified_ts = start_time, None fetch_escalated_accounts_and_hosts = integration_params.get("fetch_escalated_accounts_and_hosts", False) if fetch_escalated_accounts_and_hosts: last_detection_time = None doc_modified_ts = start_time order_field = "_doc_modified_ts" new_last_run[entity_type] = previous_last_run.get(entity_type, {}) new_last_run[entity_type]["last_created_events"] = last_created_events if entity_type == "Accounts": accounts_fetch_query = build_search_query_for_tags( integration_params.get("accounts_fetch_query", ""), "linked_account", tags ) api_response = client.search_accounts( last_timestamp=last_detection_time, doc_modified_time=doc_modified_ts, search_query=accounts_fetch_query, order_field=order_field, ) elif entity_type == "Hosts": hosts_fetch_query = build_search_query_for_tags(integration_params.get("hosts_fetch_query", ""), "host", tags) api_response = client.search_hosts( last_timestamp=last_detection_time, doc_modified_time=doc_modified_ts, search_query=hosts_fetch_query, order_field=order_field, ) elif entity_type == "Detections": api_response = client.search_detections( last_timestamp=start_time, search_query=integration_params.get("detections_fetch_query"), # type: ignore ) if (api_response is None) or (api_response.get("count") is None): raise VectraException("API issue - Response is empty or invalid") if api_response.get("count") == 0: demisto.info(f"{entity_type} - No results") elif api_response.get("count", 0) > 0: demisto.debug(f"{entity_type} - {api_response.get('count')} objects fetched from Vectra") if api_response.get("results") is None: raise VectraException("API issue - Response is empty or invalid") # Due to backward search we need to avoid creating incidents of already ingested events api_results = api_response.get("results", []) ts_field = "last_detection_timestamp" if entity_type in ("Accounts", "Hosts") else "last_timestamp" for event in api_results: if len(entity_incidents) >= max_created_incidents: demisto.debug( f"{entity_type} - Maximum created incidents has been reached ({max_created_incidents})." f" Skipping other objects." ) break incident_uid = f"{entity_type}_{event.get('id')}" if (not create_multiple_incidents_by_timestamp) and incident_uid in last_created_events: demisto.debug( f"{entity_type} - Skipping object last_timestamp : {event.get('last_timestamp')} / ID : {event.get('id')}" ) continue if create_multiple_incidents_by_timestamp: event_id = str(event.get("id")) event_ts = event.get(ts_field) checkpoint_dict = new_last_run[entity_type].get("multiple_incidents_checkpoint", {}) or {} if event_id in checkpoint_dict and checkpoint_dict.get(event_id) == event_ts: continue incident_last_run = None if entity_type == "Accounts": incident, incident_last_run = account_to_incident( client, event, detection_category, detection_type, fetch_escalated_accounts_and_hosts ) elif entity_type == "Hosts": incident, incident_last_run = host_to_incident( client, event, detection_category, detection_type, fetch_escalated_accounts_and_hosts ) elif entity_type == "Detections": incident, incident_last_run = detection_to_incident(event) else: demisto.debug(f"The {entity_type=} didn't match any condition, can't create an incident, continue.") continue # Search this incident in the last_run, if it's in, skip it, if not create it # Create incident UID and search for it if incident_last_run is not None: demisto.debug( f"{entity_type} - New incident from object " f"last_timestamp : {incident_last_run.get('last_timestamp')} " f"/ ID : {incident_last_run.get('id')}" ) entity_incidents.append(incident) new_last_run[entity_type]["last_timestamp"] = incident_last_run.get("last_timestamp") new_last_run[entity_type]["id"] = incident_last_run.get("id") existing_multiple_incidents_checkpoint = ( new_last_run[entity_type].get("multiple_incidents_checkpoint", {}) or {} ) existing_multiple_incidents_checkpoint[str(incident_last_run.get("id"))] = event.get(ts_field) new_last_run[entity_type]["multiple_incidents_checkpoint"] = existing_multiple_incidents_checkpoint # We add this event in the list, as that's a new event we need to remember for next run if incident_uid not in last_created_events: last_created_events.append(incident_uid) if len(entity_incidents) > 0: demisto.info(f"{entity_type} - {len(entity_incidents)} incident(s) to create") incidents += entity_incidents else: demisto.debug(f"{entity_type} - No new incidents to create, keeping previous last_run data") new_last_run[entity_type] = previous_last_run[entity_type] # Update remaining list remaining_fetch_types.remove(entity_type) if len(remaining_fetch_types) > 0: max_created_incidents = (max_fetch - len(incidents)) // len(remaining_fetch_types) # type: ignore if is_test: demisto.debug(f"Setting last run to : {new_last_run}") return previous_last_run, [] demisto.info(f"{len(incidents)} total incident(s) to create.") return new_last_run, incidents def get_modified_remote_data_command(client: Client) -> GetModifiedRemoteDataResponse: """ Get modified remote data from the Vectra platform and prepare it for mirroring in XSOAR. Args: client (Client): An instance of the VectraClient class. Returns: GetModifiedRemoteDataResponse: List of incidents IDs which are modified since the last update. """ args = demisto.args() command_args = GetModifiedRemoteDataArgs(args) command_last_run_date = dateparser.parse( command_args.last_update, settings={"TIMEZONE": "UTC"}, ).strftime("%Y-%m-%dT%H%M") # type: ignore modified_entities_ids = [] demisto.debug(f"Last update date of get-modified-remote-data command is {command_last_run_date}.") for entity_type in ENTITY_TYPES_FOR_MIRRORING: entity_next_url = None page = 1 page_size = 500 while True: if entity_next_url: # Parse the URL parsed_url = urlparse(entity_next_url) # Extract the query parameters query_params = parse_qs(parsed_url.query) page = arg_to_number(query_params.get("page", [""])[0], arg_name="page") # type: ignore page_size = arg_to_number(query_params.get("page_size", [""])[0], arg_name="page_size") # type: ignore query_string = query_params.get("query_string", [""])[0] # type: ignore else: query_string = "_doc_modified_ts:>=" + command_last_run_date if entity_type == "host": response = client.search_hosts(search_query_only=query_string, page=page, max_results=page_size) else: response = client.search_accounts(search_query_only=query_string, page=page, max_results=page_size) entities = response.get("results", []) entity_next_url = response.get("next") if len(entities) == 0: break # Extra ID and type of the entities modified_entities_ids.extend([str(entity.get("id")) + f"-{entity_type}" for entity in entities]) # Mirroring limit if len(modified_entities_ids) > MAX_MIRRORING_LIMIT / 2: demisto.debug(f"Max mirroring limit reached for {entity_type}.") break # If there is no data on the next page if not entity_next_url: break # Filter out None values if there are any. modified_entities_ids: list[str] = list(filter(None, modified_entities_ids)) # type: ignore demisto.debug( f"Performing get-modified-remote-data command. Numbers Entity IDs to update in XSOAR: {len(modified_entities_ids)}" ) demisto.debug(f"Performing get-modified-remote-data command. Entity IDs to update in XSOAR: {modified_entities_ids}") # Filter out any duplicate incident IDs. updated_incident_ids = list(set(modified_entities_ids)) # At max 5,000 incidents should be updated. updated_incident_ids = updated_incident_ids[:5000] return GetModifiedRemoteDataResponse(modified_incident_ids=updated_incident_ids) def get_remote_data_command(client: Client, integration_params: dict = {}) -> GetRemoteDataResponse: """ Get remote data for a specific entity from the Vectra platform and prepare it for mirroring in XSOAR. Args: client (Client): An instance of the VectraClient class. integration_params (Dict): The integration parameters. Returns: GetRemoteDataResponse: An object containing the remote incident data and any new entries to return to XSOAR. """ detection_category = integration_params.get("detection_category", "") detection_type = integration_params.get("detection_type", "").strip() refetch_closed_incidents = argToBoolean(integration_params.get("refetch_closed_incidents", False)) new_entries_to_return: list[dict] = [] args = demisto.args() dbot_mirror_id: str = args.get("id") # type: ignore demisto.debug(f"dbot_mirror_id:{dbot_mirror_id}") entity_id_type = dbot_mirror_id.split("-") vectra_entity_id = entity_id_type[0] if entity_id_type else "" vectra_entity_type = entity_id_type[1] if entity_id_type else "" demisto.debug(f"vectra_entity_id:{vectra_entity_type}") demisto.debug(f"Getting update for remote {vectra_entity_id}.") command_last_run_dt = arg_to_datetime(args.get("lastUpdate"), arg_name="lastUpdate", required=True) command_last_run_timestamp = command_last_run_dt.strftime(DATE_FORMAT) # type: ignore demisto.debug( f"The time when the last time get-remote-data command is called for current incident is {command_last_run_timestamp}." ) # Retrieve the latest entity data from the Vectra platform. if vectra_entity_type == "account": remote_incident_data = client.get_account_by_account_id(account_id=vectra_entity_id) groups_response = client.list_group_request( group_type="account", account_names=[remote_incident_data.get("name")], ) # type: ignore remote_incident_data.update({"groups": groups_response.get("results", [])}) else: remote_incident_data = client.get_host_by_host_id(host_id=vectra_entity_id) if not remote_incident_data: return "Incident was not found." # type: ignore remote_incident_data["_vectra_specific"] = { "entity_type": vectra_entity_type, "UI_URL": forge_entity_url(vectra_entity_type, remote_incident_data.get("id")), } updated_severity_score = severity_string_to_int( scores_to_severity(remote_incident_data.get("threat"), remote_incident_data.get("certainty")) ) remote_incident_data.update({"calculated_severity": updated_severity_score}) # Get detection set. detection_set = remote_incident_data.get("detection_set", []) # Collect the detections if the detection set is not empty. detections = [] if len(detection_set) != 0: if vectra_entity_type == "account": search_query = build_search_query_for_detections(vectra_entity_id, "account", detection_category, detection_type) api_response = client.search_detections(state="active", search_query=search_query) if (api_response is None) or (api_response.get("count") is None): raise VectraException("API issue - Response is empty or invalid") if api_response.get("count") == 0: demisto.info(f"Found 0 detection(s) for the account with the ID: {vectra_entity_id}.") elif api_response.get("count", 0) > 0: demisto.info(f"Found {api_response.get('count')} detection(s) for the account with the ID: {vectra_entity_id}.") if api_response.get("results") is None: raise VectraException("API issue - Response is empty or invalid") # Due to backward search we need to avoid creating incidents of already ingested events detections = api_response.get("results", []) for detection in detections: detection["url"] = forge_entity_url("detection", detection.get("id")) else: detections_data = client.list_detections_by_host_id(vectra_entity_id, "active", detection_category, detection_type) detections = detections_data.get("results", []) for detection in detections: detection["url"] = forge_entity_url("detection", detection.get("id")) demisto.debug(f"Found {len(detections)} detection(s) for the host with the ID: {vectra_entity_id}.") # Add detection details to the entity. remote_incident_data.update({"detection_details": detections}) assignment_details = remote_incident_data.get("assignment", {}) if not assignment_details: past_assignments = remote_incident_data.get("past_assignments", [{}]) if past_assignments and isinstance(past_assignments, list): assignment_details = past_assignments[0] if assignment_details: if not assignment_details.get("resolved_by"): assignment_details["resolved_by"] = {"username": ""} assignment_details["outcome"] = {"title": ""} assignment_details["date_resolved"] = "" else: assignment_details = EMPTY_ASSIGNMENT remote_incident_data.update({"assignment_details": assignment_details}) if detections and not refetch_closed_incidents: reopen_in_xsoar(new_entries_to_return, entity_id_type) notes = remote_incident_data.get("notes", []) new_entry_notes = add_notes_to_new_entries(notes, command_last_run_dt) new_entries_to_return.extend(new_entry_notes) demisto.debug(f"remote_incident_data:{remote_incident_data}") return GetRemoteDataResponse(remote_incident_data, new_entries_to_return) def update_remote_system_command(client: Client) -> str: """ Update a remote system based on changes in the XSOAR incident. Args: client (Client): An instance of the VectraClient class. Returns: str: The ID of the updated remote entity. """ args = demisto.args() parsed_args = UpdateRemoteSystemArgs(args) # Get remote incident ID remote_entity_id = parsed_args.remote_incident_id demisto.debug(f"Remote Incident ID: {remote_entity_id}") delta = parsed_args.delta or {} demisto.debug(f"Delta: {delta}") # Get XSOAR incident id data = parsed_args.data or {} xsoar_incident_id = data.get("id", "") demisto.debug(f"XSOAR Incident ID: {xsoar_incident_id}") new_entries = parsed_args.entries or [] xsoar_tags: list = delta.get("tags") or [] mirror_entity_id = remote_entity_id.split("-")[0] remote_entity_type = remote_entity_id.split("-")[1] params = demisto.params() refetch_closed_incidents = argToBoolean(params.get("refetch_closed_incidents", False)) # For notes if new_entries: for entry in new_entries: entry_id = entry.get("id") demisto.debug(f"Sending the entry with ID: {entry_id} and Type: {entry.get('type')}") # Get note content and user entry_content = re.sub(r"([^\n])\n", r"\1\n\n", entry.get("contents", "")) entry_user = entry.get("user", "dbot") or "dbot" note_str = ( f"[Mirrored From XSOAR] XSOAR Incident ID: {xsoar_incident_id}\n\nNote: {entry_content}\n\nAdded By: {entry_user}" ) # API request for adding notes client.add_note_request(entity_id=mirror_entity_id, entity_type=remote_entity_type, note=note_str) # For tags res = client.list_entity_tags_request(entity_id=mirror_entity_id, entity_type=remote_entity_type) vectra_tags = res.get("tags") or [] if xsoar_tags or (not xsoar_tags and vectra_tags and "tags" in delta): demisto.debug(f"Sending the tags: {xsoar_tags}") # Drop invalid tags that do not fully match the allowed pattern if xsoar_tags: valid_tags, dropped_tags = get_valid_and_dropped_tags(xsoar_tags) xsoar_tags = valid_tags client.update_entity_tags_request(entity_id=mirror_entity_id, entity_type=remote_entity_type, tag_list=xsoar_tags) # For closing notes if the XSOAR incident is closed. send_close_notes( client, data, xsoar_incident_id, remote_entity_id, remote_entity_type, mirror_entity_id, delta, refetch_closed_incidents, parsed_args, ) return remote_entity_id def send_close_notes( client: Client, data: dict, xsoar_incident_id: str, remote_entity_id: str, remote_entity_type: str, mirror_entity_id: int, delta: dict, refetch_closed_incidents: bool, parsed_args: UpdateRemoteSystemArgs, ): """ Send close notes to Vectra and also remove the assignment when the XSOAR incident is reopened. Args: data (dict): A dictionary of data from Vectra. xsoar_incident_id (str): The ID of the XSOAR incident. delta (dict): A dictionary of changes in the XSOAR incident. refetch_closed_incidents (bool): A boolean to determine whether to refetch closed incidents. """ delta_keys = delta.keys() closing_user_id = delta.get("closingUserId") if "closingUserId" in delta_keys and parsed_args.incident_changed: # For Closing notes if parsed_args.inc_status == IncidentStatus.DONE: if refetch_closed_incidents: add_refetch_id_to_integration_context(str(mirror_entity_id), remote_entity_type) close_notes = data.get("closeNotes", "") close_reason = data.get("closeReason", "") close_user_id = data.get("closingUserId", "") closing_note = ( f"[Mirrored From XSOAR] XSOAR Incident ID: {xsoar_incident_id}\n\n" f"Close Reason: {close_reason}\n\n" f"Closed By: {close_user_id}\n\n" f"Close Notes: {close_notes}" ) demisto.debug(f"Closing Comment: {closing_note}") client.add_note_request(entity_id=mirror_entity_id, entity_type=remote_entity_type, note=closing_note) # Remove assignment in Vectra if incident is reopened. elif parsed_args.inc_status == IncidentStatus.ACTIVE and closing_user_id == "": api_response = {} if "account" in remote_entity_type: api_response = client.list_assignments_request(accounts=mirror_entity_id, page_size=1) # type: ignore elif "host" in remote_entity_type: api_response = client.list_assignments_request(hosts=mirror_entity_id, page_size=1) # type: ignore assignment_details = api_response.get("results", []) assignment = assignment_details[0] if assignment_details else {} if assignment: assignment_id = assignment.get("id", "") if not assignment.get("resolved_by"): demisto.debug( f"Removing assignment with the ID: {assignment_id} for the incident having" f" remote entity ID: {remote_entity_id} as incident in XSOAR is reopened." ) client.delete_assignment(assignment_id) def vectra_search_accounts_command(client: Client, **kwargs) -> CommandResults: """ Returns several Account objects matching the search criteria passed as arguments - params: - client: Vectra Client - kwargs: The different possible search query arguments - returns CommandResults to be used in War Room """ api_response = client.search_accounts(**kwargs) count = api_response.get("count") if count is None: raise VectraException("API issue - Response is empty or invalid") accounts_data = [] if count == 0: readable_output = "Cannot find any Account." else: if api_response.get("results") is None: raise VectraException("API issue - Response is empty or invalid") api_results = api_response.get("results", []) for account in api_results: accounts_data.append(extract_account_data(account)) readable_output_keys = ["ID", "Username", "Severity", "URL"] readable_output = tableToMarkdown( name=f"Accounts table (Showing max {MAX_RESULTS} entries)", t=accounts_data, headers=readable_output_keys, url_keys=["URL"], date_fields=["AssignedDate", "LastDetectionTimestamp"], ) command_result = CommandResults( readable_output=readable_output, outputs_prefix="Vectra.Account", outputs_key_field="ID", outputs=accounts_data, raw_response=api_response, ) return command_result def vectra_search_detections_command(client: Client, **kwargs) -> CommandResults: """ Returns several Detection objects matching the search criteria passed as arguments - params: - client: Vectra Client - kwargs: The different possible search query arguments - returns CommandResults to be used in War Room """ api_response = client.search_detections(**kwargs) count = api_response.get("count") if count is None: raise VectraException("API issue - Response is empty or invalid") detections_data = [] if count == 0: readable_output = "Cannot find any Detection." else: if api_response.get("results") is None: raise VectraException("API issue - Response is empty or invalid") api_results = api_response.get("results", []) # Define which fields we want to exclude from the context output # detection_context_excluded_fields = [] # Context Keys # context_keys = [] for detection in api_results: detection_data = extract_detection_data(detection) # detection_data = {k: detection_data[k] for k in detection_data if k not in detection_context_excluded_fields} detections_data.append(detection_data) readable_output_keys = ["ID", "Name", "Severity", "LastTimestamp", "Category", "URL"] readable_output = tableToMarkdown( name=f"Detections table (Showing max {MAX_RESULTS} entries)", t=detections_data, headers=readable_output_keys, url_keys=["URL"], date_fields=["AssignedDate", "FirstTimestamp", "LastTimestamp"], ) command_result = CommandResults( readable_output=readable_output, outputs_prefix="Vectra.Detection", outputs_key_field="ID", outputs=detections_data, raw_response=api_response, ) return command_result def vectra_search_hosts_command(client: Client, **kwargs) -> CommandResults: """ Returns several Host objects matching the search criteria passed as arguments - params: - client: Vectra Client - kwargs: The different possible search query arguments - returns CommandResults to be used in War Room """ api_response = client.search_hosts(**kwargs) count = api_response.get("count") if count is None: raise VectraException("API issue - Response is empty or invalid") hosts_data = [] if count == 0: readable_output = "Cannot find any Host." else: if api_response.get("results") is None: raise VectraException("API issue - Response is empty or invalid") api_results = api_response.get("results", []) for host in api_results: hosts_data.append(extract_host_data(host)) readable_output_keys = ["ID", "Hostname", "Severity", "LastDetectionTimestamp", "URL"] readable_output = tableToMarkdown( name=f"Hosts table (Showing max {MAX_RESULTS} entries)", t=hosts_data, headers=readable_output_keys, url_keys=["URL"], date_fields=["AssignedDate", "LastDetectionTimestamp"], ) command_result = CommandResults( readable_output=readable_output, outputs_prefix="Vectra.Host", outputs_key_field="ID", outputs=hosts_data, raw_response=api_response, ) return command_result def vectra_search_assignments_command(client: Client, **kwargs) -> CommandResults: """ Returns several Assignment objects matching the search criteria passed as arguments - params: - client: Vectra Client - kwargs: The different possible search query arguments - returns CommandResults to be used in War Room """ api_response = client.search_assignments(**kwargs) count = api_response.get("count") if count is None: raise VectraException("API issue - Response is empty or invalid") assignments_data = [] if count == 0: readable_output = "Cannot find any Assignments." else: if api_response.get("results") is None: raise VectraException("API issue - Response is empty or invalid") api_results = api_response.get("results", []) for assignment in api_results: assignments_data.append(extract_assignment_data(assignment)) readable_output_keys = ["ID", "IsResolved", "AssignedTo", "AccountID", "HostID"] readable_output = tableToMarkdown( name=f"Assignments table (Showing max {MAX_RESULTS} entries)", t=assignments_data, headers=readable_output_keys, date_fields=["AssignedDate", "ResolvedDate"], ) command_result = CommandResults( readable_output=readable_output, outputs_prefix="Vectra.Assignment", outputs_key_field="ID", outputs=assignments_data, raw_response=api_response, ) return command_result def vectra_search_outcomes_command(client: Client, **kwargs) -> CommandResults: """ Returns several Assignment outcome objects matching the search criteria passed as arguments - params: - client: Vectra Client - kwargs: The different possible search query arguments - returns CommandResults to be used in War Room """ api_response = client.search_outcomes(**kwargs) count = api_response.get("count") if count is None: raise VectraException("API issue - Response is empty or invalid") outcomes_data = [] if count == 0: readable_output = "Cannot find any Outcomes." else: if api_response.get("results") is None: raise VectraException("API issue - Response is empty or invalid") api_results = api_response.get("results", []) for outcome in api_results: outcomes_data.append(extract_outcome_data(outcome)) readable_output_keys = ["ID", "Title", "Category", "IsBuiltIn"] readable_output = tableToMarkdown( name=f"Outcomes table (Showing max {MAX_RESULTS} entries)", t=outcomes_data, headers=readable_output_keys ) command_result = CommandResults( readable_output=readable_output, outputs_prefix="Vectra.Outcome", outputs_key_field="ID", outputs=outcomes_data, raw_response=api_response, ) return command_result def vectra_search_users_command(client: Client, **kwargs) -> CommandResults: """ Returns several Vectra Users objects matching the search criteria passed as arguments - params: - client: Vectra Client - kwargs: The different possible search query arguments - returns CommandResults to be used in War Room """ api_response = client.search_users(**kwargs) count = api_response.get("count") if count is None: raise VectraException("API issue - Response is empty or invalid") users_data = [] if count == 0: readable_output = "Cannot find any Vectra Users." else: if api_response.get("results") is None: raise VectraException("API issue - Response is empty or invalid") api_results = api_response.get("results", []) for assignment in api_results: users_data.append(extract_user_data(assignment)) readable_output_keys = ["ID", "Role", "Type", "Username", "LastLoginDate"] readable_output = tableToMarkdown( name=f"Vectra Users table (Showing max {MAX_RESULTS} entries)", t=users_data, headers=readable_output_keys, date_fields=["LastLoginDate"], ) command_result = CommandResults( readable_output=readable_output, outputs_prefix="Vectra.User", outputs_key_field="ID", outputs=users_data, raw_response=api_response, ) return command_result def vectra_get_account_by_id_command(client: Client, id: str) -> CommandResults: """ Gets Account details using its ID - params: - client: Vectra Client - id: The Account ID - returns CommandResults to be used in War Room """ # Check args if not id: raise VectraException('"id" not specified') search_query: str = f"account.id:{id}" api_response = client.search_accounts(search_query_only=search_query) count = api_response.get("count") if count is None: raise VectraException("API issue - Response is empty or invalid") if count > 1: raise VectraException("Multiple Accounts found") account_data = None if count == 0: readable_output = f'Cannot find Account with ID "{id}".' else: if api_response.get("results") is None: raise VectraException("API issue - Response is empty or invalid") api_results = api_response.get("results", []) account_data = extract_account_data(api_results[0]) readable_output = tableToMarkdown( name=f"Account ID {id} details table", t=account_data, url_keys=["URL"], date_fields=["LastDetectionTimestamp"] ) command_result = CommandResults( readable_output=readable_output, outputs_prefix="Vectra.Account", outputs_key_field="ID", outputs=account_data, raw_response=api_response, ) return command_result def vectra_get_detection_by_id_command(client: Client, id: str) -> CommandResults: """ Gets Detection details using its ID - params: - client: Vectra Client - id: The Detection ID - returns CommandResults to be used in War Room """ # Check args if not id: raise VectraException('"id" not specified') search_query: str = f"detection.id:{id}" api_response = client.search_detections(search_query_only=search_query) count = api_response.get("count") if count is None: raise VectraException("API issue - Response is empty or invalid") if count > 1: raise VectraException("Multiple Detections found") detection_data = None if count == 0: readable_output = f'Cannot find Detection with ID "{id}".' else: if api_response.get("results") is None: raise VectraException("API issue - Response is empty or invalid") api_results = api_response.get("results", []) detection_data = extract_detection_data(api_results[0]) readable_output = tableToMarkdown( name=f"Detection ID '{id}' details table", t=detection_data, url_keys=["URL"], date_fields=["FirstTimestamp", "LastTimestamp"], ) command_result = CommandResults( readable_output=readable_output, outputs_prefix="Vectra.Detection", outputs_key_field="ID", outputs=detection_data, raw_response=api_response, ) return command_result def vectra_get_host_by_id_command(client: Client, id: str) -> CommandResults: """ Gets Host details using its ID - params: - client: Vectra Client - id: The Host ID - returns CommandResults to be used in War Room """ # Check args if not id: raise VectraException('"id" not specified') search_query: str = f"host.id:{id}" api_response = client.search_hosts(search_query_only=search_query) count = api_response.get("count") if count is None: raise VectraException("API issue - Response is empty or invalid") if count > 1: raise VectraException("Multiple Hosts found") host_data = None if count == 0: readable_output = f'Cannot find Host with ID "{id}".' else: if api_response.get("results") is None: raise VectraException("API issue - Response is empty or invalid") api_results = api_response.get("results", []) host_data = extract_host_data(api_results[0]) readable_output = tableToMarkdown( name=f"Host ID {id} details table", t=host_data, url_keys=["URL"], date_fields=["LastDetectionTimestamp"], ) command_result = CommandResults( readable_output=readable_output, outputs_prefix="Vectra.Host", outputs_key_field="ID", outputs=host_data, raw_response=api_response, ) return command_result def get_detection_pcap_file_command(client: Client, id: str): """ Downloads a PCAP fileassociated to a detection - params: - client: Vectra Client - id: The Detection ID - returns: A commandResult to use in the War Room """ if not id: raise VectraException('"id" not specified') api_response = client.get_pcap_by_detection_id(id=id) # 404 API error will be raised by the Client class filename = f"detection-{id}.pcap" file_content = api_response.content pcap_file = fileResult(filename, file_content) return pcap_file def mark_detection_as_fixed_command(client: Client, id: str, fixed: str) -> CommandResults: """ Toggles a detection status as : fixed / Not fixed - params: - client: Vectra Client - id: The Detection ID - fixed: The Detection future state """ if (id is None) or (id == ""): raise VectraException('"id" not specified') fixed_as_bool = str2bool(fixed) if fixed_as_bool is None: raise VectraException('"fixed" not specified') api_response = client.markasfixed_by_detection_id(id=id, fixed=fixed_as_bool) # 404 API error will be raised by the Client class command_result = CommandResults( readable_output=f'Detection "{id}" successfully {"marked" if fixed_as_bool else "unmarked"} as fixed.', raw_response=api_response, ) return command_result def vectra_get_assignment_by_id_command(client: Client, id: str) -> CommandResults: """ GetsAssignment details using its ID - params: - client: Vectra Client - id: The Assignment ID - returns CommandResults to be used in War Room """ # Check args if not id: raise VectraException('"id" not specified') api_response = client.search_assignments(id=id) assignment_data = None # Assignment doesn't follow classic describe behavior obtained_assignment = api_response.get("assignment") if obtained_assignment is None: readable_output = f'Cannot find Assignment with ID "{id}".' else: assignment_data = extract_assignment_data(obtained_assignment) readable_output = tableToMarkdown( name=f"Assignment ID {id} details table", t=assignment_data, date_fields=["AssignedDate", "ResolvedDate"] ) command_result = CommandResults( readable_output=readable_output, outputs_prefix="Vectra.Assignment", outputs_key_field="ID", outputs=assignment_data, raw_response=api_response, ) return command_result def vectra_assignment_assign_command( client: Client, assignee_id: str = None, # type: ignore account_id: str = None, host_id: str = None, # type: ignore assignment_id: str = None, ) -> CommandResults: # type: ignore """ Assign or reassign an Account/Host - params: - client: Vectra Client - assignee_id: The Vectra User ID who want to assign to - account_id: The Account ID - host_id: The Host ID - assignment_id: The existing assignment ID associated with the targeted Entity, if there is any - returns CommandResults to be used in War Room """ # Check args if not assignee_id: raise VectraException('"assignee_id" not specified') if ( ((assignment_id is None) and (account_id is None) and (host_id is None)) or (account_id and host_id) or (assignment_id and (account_id or host_id)) ): raise VectraException('You must specify one of "assignment_id", "account_id" or "host_id"') if assignment_id is None: api_response = client.update_assignment(assignee_id=assignee_id, account_id=account_id, host_id=host_id) else: api_response = client.update_assignment(assignee_id=assignee_id, assignment_id=assignment_id) # 40x API error will be raised by the Client class obtained_assignment = api_response.get("assignment") assignment_data = extract_assignment_data(obtained_assignment) readable_output = tableToMarkdown(name="Assignment details table", t=assignment_data) command_result = CommandResults( readable_output=readable_output, outputs_prefix="Vectra.Assignment", outputs_key_field="ID", outputs=assignment_data, raw_response=api_response, ) return command_result def vectra_assignment_resolve_command( client: Client, assignment_id: str = None, outcome_id: str = None, # type: ignore note: str = None, detections_filter: str = None, # type: ignore filter_rule_name: str = None, detections_list: str = None, ): # type: ignore """ Resolve an existing assignment - params: - client: Vectra Client - assignment_id: Assignment ID - outcome_id: The Outcome ID - detections_filter: Filter mode to use ('None' or 'Filter Rule') [Default: None] - filter_rule_name: Filter rule name (when detections_filter equals 'Filter Rule') - detections_list: List of the Detections to filter - returns CommandResults to be used in War Room """ # Check args if not assignment_id: raise VectraException('"assignment_id" not specified') if not outcome_id: raise VectraException('"outcome_id" not specified') if detections_filter == "Filter Rule": if not filter_rule_name: raise VectraException('"filter_rule_name" not specified') if not detections_list: raise VectraException('"detections_list" not specified') api_response = client.resolve_assignment( assignment_id=assignment_id, outcome_id=outcome_id, note=note, rule_name=filter_rule_name, detections_list=detections_list, ) else: api_response = client.resolve_assignment(assignment_id=assignment_id, outcome_id=outcome_id, note=note) # 40x API error will be raised by the Client class obtained_assignment = api_response.get("assignment") assignment_data = extract_assignment_data(obtained_assignment) readable_output = tableToMarkdown(name="Assignment details table", t=assignment_data) command_result = CommandResults( readable_output=readable_output, outputs_prefix="Vectra.Assignment", outputs_key_field="ID", outputs=assignment_data, raw_response=api_response, ) return command_result def vectra_get_outcome_by_id_command(client: Client, id: str) -> CommandResults: """ Gets Outcome details using its ID - params: - client: Vectra Client - id: The Outcome ID - returns CommandResults to be used in War Room """ # Check args if not id: raise VectraException('"id" not specified') api_response = client.search_outcomes(id=id) outcome_data = None obtained_id = api_response.get("id") if obtained_id is None: readable_output = f'Cannot find Outcome with ID "{id}".' else: outcome_data = extract_outcome_data(api_response) readable_output = tableToMarkdown(name=f"Outcome ID {id} details table", t=outcome_data) command_result = CommandResults( readable_output=readable_output, outputs_prefix="Vectra.Outcome", outputs_key_field="ID", outputs=outcome_data, raw_response=api_response, ) return command_result def vectra_outcome_create_command(client: Client, category: str, title: str) -> CommandResults: """ Creates a new Outcome - params: - client: Vectra Client - category: The Outcome category (one of "BTP,MTP,FP") - title: A custom title for this new outcome - returns CommandResults to be used in War Room """ # Check args if not category: raise VectraException('"category" not specified') if not title: raise VectraException('"title" not specified') api_response = client.create_outcome(category=category, title=title) # 40x API error will be raised by the Client class outcome_data = extract_outcome_data(api_response) readable_output = tableToMarkdown(name="Newly created Outcome details table", t=outcome_data) command_result = CommandResults( readable_output=readable_output, outputs_prefix="Vectra.Outcome", outputs_key_field="ID", outputs=outcome_data, raw_response=api_response, ) return command_result def vectra_get_user_by_id_command(client: Client, id: str) -> CommandResults: """ Gets Vectra User details using its ID - params: - client: Vectra Client - id: The User ID - returns CommandResults to be used in War Room """ # Check args if not id: raise VectraException('"id" not specified') api_response = client.search_users(id=id) user_data = None obtained_id = api_response.get("id") if obtained_id is None: readable_output = f'Cannot find Vectra User with ID "{id}".' else: user_data = extract_user_data(api_response) readable_output = tableToMarkdown(name=f"Vectra User ID {id} details table", t=user_data, date_fields=["LastLoginDate"]) command_result = CommandResults( readable_output=readable_output, outputs_prefix="Vectra.User", outputs_key_field="ID", outputs=user_data, raw_response=api_response, ) return command_result def add_tags_command(client: Client, type: str, id: str, tags: str) -> CommandResults: """ Adds several tags to an account/host/detection - params: - client: Vectra Client - type: The object to work with ("account", "host" or "detection") - id: The id ID the account/host/detection - tags: The tags list (comma separated) """ if not type: raise VectraException('"type" not specified') if not id: raise VectraException('"id" not specified') if not tags: raise VectraException('"tags" not specified') api_response = client.add_tags(id=id, type=type, tags=tags.split(",")) # 404 API error will be raised by the Client class command_result = CommandResults(readable_output=f'Tags "{tags}" successfully added.', raw_response=api_response) return command_result def del_tags_command(client: Client, type: str, id: str, tags: str) -> CommandResults: """ Removes several tags from an account/host/detection - params: - client: Vectra Client - type: The object to work with ("account", "host" or "detection") - id: The ID of the account/host/detection - tags: The tags list (comma separated) """ if not type: raise VectraException('"type" not specified') if not id: raise VectraException('"id" not specified') if not tags: raise VectraException('"tags" not specified') api_response = client.del_tags(id=id, type=type, tags=tags.split(",")) # 404 API error will be raised by the Client class command_result = CommandResults(readable_output=f'Tags "{tags}" successfully deleted.', raw_response=api_response) return command_result def tag_list_command(client: Client, entity_type: str, args: dict) -> CommandResults: """ List all tags of specific Account, Host or Detection. Args: client : An instance of the Client class. entity_type: The type Account, Host or Detection args: dictionary of arguments. Returns: CommandResults: The command results containing the outputs, readable output, raw response, and outputs key field. """ entity_id = arg_to_number(args.get("id"), arg_name="id", required=True) validate_positive_integer_arg(entity_id, arg_name="id", required=True) existing_tag_res = client.list_tags_request(entity_id=entity_id, entity_type=entity_type) # type: ignore raw_res = deepcopy(existing_tag_res) existing_tag_res_status = existing_tag_res.get("status", "") if ( not existing_tag_res_status or not isinstance(existing_tag_res_status, str) or existing_tag_res_status.lower() != "success" ): message = "Something went wrong." if existing_tag_res.get("message"): message += f" Message: {existing_tag_res.get('message')}." raise VectraException(message) tags_resp = existing_tag_res.get("tags", []) human_readable = f"##### No tags were found for the given {entity_type} ID." if tags_resp and isinstance(tags_resp, list): tags_resp = [tag.strip() for tag in tags_resp if isinstance(tag, str) and tag.strip()] if tags_resp: tags_resp = f"**{'**, **'.join(tags_resp)}**" human_readable = f"##### List of tags: {tags_resp}" existing_tag_res["ID"] = entity_id existing_tag_res["Tags"] = existing_tag_res["tags"] del existing_tag_res["status"] del existing_tag_res["tags"] del existing_tag_res["tag_id"] return CommandResults( outputs_prefix=f"Vectra.{entity_type.capitalize()}", outputs=createContext(remove_empty_elements(existing_tag_res)), readable_output=human_readable, raw_response=raw_res, outputs_key_field="ID", ) def note_add_command(client: Client, entity_type: str, args: dict) -> CommandResults: """ Adds a note to an account/host/detection in Vectra API. Args: client : An instance of the Client class. entity_type (str): The type of the object (account, host or detection). Returns: CommandResults: The command results containing the outputs, readable output, raw response, and outputs key field. """ entity_id = arg_to_number(args.get(f"{entity_type}_id"), arg_name=f"{entity_type}_id", required=True) note = args.get("note") validate_note_add_command_args(entity_id=entity_id, note=note, entity_type=entity_type) # type: ignore # Call Vectra API to add note notes = client.add_note_request(entity_id=entity_id, entity_type=entity_type, note=note) # type: ignore notes_raw_response = deepcopy(notes) output_prefix = OUTPUT_PREFIXES[f"{entity_type.upper()}_NOTES"] if notes: notes["note_id"] = notes.get("id") del notes["id"] notes.update({f"{entity_type}_id": entity_id}) human_readable = f"##### The note has been successfully added to the {entity_type}." human_readable += f"\nReturned Note ID: **{notes['note_id']}**" return CommandResults( outputs_prefix=output_prefix, outputs=createContext(remove_empty_elements(notes)), readable_output=human_readable, raw_response=notes_raw_response, outputs_key_field=NOTE_OUTPUT_KEY_FIELD, ) def note_update_command(client: Client, entity_type: str, args: dict) -> CommandResults: """ Updates a note to an Account/Host/Detection in Vectra API. Args: client: An instance of the Client class. entity_type (str): The type of the object (account, host or detection). Returns: CommandResults: The command results containing the outputs, readable output, raw response, and outputs key field. """ entity_id = arg_to_number(args.get(f"{entity_type}_id"), arg_name=f"{entity_type}_id", required=True) note_id = arg_to_number(args.get("note_id"), arg_name="note_id", required=True) note = args.get("note") validate_note_update_command_args(entity_id=entity_id, note_id=note_id, note=note, entity_type=entity_type) # type: ignore # Call Vectra API to update note notes = client.update_note_request( entity_id=entity_id, entity_type=entity_type, # type: ignore note=note, note_id=note_id, ) # type: ignore notes_raw_response = deepcopy(notes) output_prefix = OUTPUT_PREFIXES[f"{entity_type.upper()}_NOTES"] if notes: notes["note_id"] = notes["id"] del notes["id"] notes.update({f"{entity_type}_id": entity_id}) human_readable = f"##### The note has been successfully updated in the {entity_type}." return CommandResults( outputs_prefix=output_prefix, outputs=createContext(remove_empty_elements(notes)), readable_output=human_readable, raw_response=notes_raw_response, outputs_key_field=NOTE_OUTPUT_KEY_FIELD, ) def note_remove_command(client: Client, entity_type: str, args: dict) -> CommandResults: """ Removes a note from an Account/Host/Detection in Vectra API. Args: client (VectraClient): An instance of the VectraClient class. Returns: CommandResults: The command results containing the outputs, readable output. """ entity_id = arg_to_number(args.get(f"{entity_type}_id"), arg_name=f"{entity_type}_id", required=True) note_id = arg_to_number(args.get("note_id"), arg_name="note_id", required=True) validate_note_remove_command_args(entity_id=entity_id, note_id=note_id, entity_type=entity_type) # Call Vectra API to remove note response = client.remove_note_request(entity_id=entity_id, entity_type=entity_type, note_id=note_id) # type: ignore if response.status_code == 204: human_readable = f"##### The note has been successfully removed from the {entity_type}." else: human_readable = f"Something went wrong. API Response: {response.text}" return CommandResults(readable_output=human_readable, raw_response=response.text) def note_list_command(client: Client, entity_type: str, args: dict) -> CommandResults: # type: ignore """ List notes of specific Account, Host or Detection. Args: client: An instance of the Client class. entity_type: The type Account, Host or Detection Returns: CommandResults: The command results containing the outputs, readable output, raw response, and outputs key field. """ entity_id = arg_to_number(args.get(f"{entity_type}_id"), arg_name=f"{entity_type}_id", required=True) validate_positive_integer_arg(entity_id, arg_name=f"{entity_type}_id", required=True) notes = client.list_note_request(entity_id=entity_id, entity_type=entity_type) # type: ignore notes_raw_response = deepcopy(notes) notes = remove_empty_elements(notes) if notes: human_readable = get_list_notes_command_hr(notes, entity_id, entity_type) # type: ignore context = [createContext(note) for note in notes] output_prefix = OUTPUT_PREFIXES[f"{entity_type.upper()}_NOTES"] # type: ignore return CommandResults( outputs_prefix=output_prefix, outputs=context, readable_output=human_readable, raw_response=notes_raw_response, outputs_key_field=NOTE_OUTPUT_KEY_FIELD, ) return CommandResults( outputs={}, raw_response=notes_raw_response, readable_output=f"Couldn't find any notes for provided {entity_type}." ) def markall_detections_asfixed_command( client: Client, type: str, account_id: str = None, # type: ignore host_id: str = None, ) -> CommandResults: # type: ignore """ Marks all active detections of an account/host as fixed. - param: - client: Vectra Client - type: The object to work with ("account" or "host") - return: CommandResults to be used in War Room. """ if type == "account": validate_positive_integer_arg(account_id, "account_id", True) entity_data = client.get_account_by_account_id(account_id=str(account_id)) else: validate_positive_integer_arg(host_id, "host_id", True) entity_data = client.get_host_by_host_id(host_id=str(host_id)) detections_ids = [ str(detection.get("detection_id")) for detection in entity_data.get("detection_summaries") if detection.get("state") == "active" ] if detections_ids: api_response = client.markasfixed_by_detection_ids(ids_list=detections_ids) # 404 API error will be raised by the Client class command_result = CommandResults( readable_output=f"The active detections of the provided {type} have been successfully marked as fixed.", raw_response=api_response, ) else: command_result = CommandResults(readable_output="There are no active detections present.", raw_response={}) return command_result def markall_detections_asclosed_command( client: Client, entity_type: str, args: dict[str, Any], ) -> CommandResults: """ Marks all active detections of an account/host as closed. - param: - client: Vectra Client - entity_type: The object to work with ("account" or "host") - args: The arguments - return: CommandResults to be used in War Room. """ entity_id = args.get(f"{entity_type}_id") validate_positive_integer_arg(entity_id, arg_name=f"{entity_type}_id", required=True) close_reason = args.get("close_reason", "").lower() if not close_reason: raise ValueError(ERRORS["REQUIRED_ARGUMENT"].format("close_reason")) # Validate close_reason if close_reason not in VALID_CLOSE_REASON: raise ValueError('Invalid close_reason. Must be "benign" or "remediated".') # Get account data if entity_type == "account": entity_data = client.get_account_by_account_id(account_id=str(entity_id)) else: entity_data = client.get_host_by_host_id(host_id=str(entity_id)) # Extract active detection IDs detections_ids = [ str(detection.get("detection_id")) for detection in entity_data.get("detection_summaries") if detection.get("state") == "active" ] if detections_ids: api_response = client.close_detections_by_ids(ids_list=detections_ids, reason=close_reason) if api_response.get("_meta", {}).get("level", "").lower() != "success": res_message = api_response.get("_meta", {}).get("message", "") message = "Something went wrong." if res_message: message += f" Message: {res_message}." raise DemistoException(message) # 404 API error will be raised by the Client class hr_output = f"##### The active detections of the provided {entity_type} have been successfully closed as {close_reason}." command_result = CommandResults( readable_output=hr_output, raw_response=api_response, ) else: command_result = CommandResults(readable_output="##### There are no active detections present.", raw_response={}) return command_result def vectra_group_list_command(client: Client, args: dict[str, Any]): """ Retrieves a list of groups. Args: client (Client): An instance of the Client class. args (Dict[str, Any]): The command arguments provided by the user. Returns: CommandResults: The command results containing the outputs, readable output, raw response, and outputs key field. """ validate_group_list_command_args(args) # Get function arguments group_type = args.get("group_type", "") if group_type: group_type = group_type.lower() importance = args.get("importance", "") if importance: importance = importance.lower() account_names = argToList(args.get("account_names", "")) domains = argToList(args.get("domains", "")) host_ids = argToList(args.get("host_ids", "")) host_names = argToList(args.get("host_names", "")) ips = argToList(args.get("ips", "")) description = args.get("description", "") last_modified_timestamp = arg_to_datetime(args.get("last_modified_timestamp"), arg_name="last_modified_timestamp") last_modified_by = args.get("last_modified_by", "") group_name = args.get("group_name", "") response = client.list_group_request( group_type=group_type, account_names=account_names, domains=domains, host_ids=host_ids, host_names=host_names, importance=importance, ips=ips, description=description, last_modified_timestamp=last_modified_timestamp, last_modified_by=last_modified_by, group_name=group_name, ) # type: ignore count = response.get("count") if count == 0: return CommandResults( outputs={}, readable_output="##### Couldn't find any matching groups for provided filters.", raw_response=response ) groups = response.get("results") # Prepare context data human_readable = get_group_list_command_hr(groups) # type: ignore context = [createContext(group) for group in remove_empty_elements(groups)] # type: ignore return CommandResults( outputs_prefix="Vectra.Group", outputs=context, readable_output=human_readable, raw_response=groups, outputs_key_field="group_id", ) def vectra_group_unassign_command(client: Client, args: dict[str, Any]): """ Unassign members in Group. Args: client (Client): An instance of the Client class. args (Dict[str, Any]): The command arguments. Returns: CommandResults: The command results. """ validate_group_assign_and_unassign_command_args(args) group_id = args.get("group_id") members = args.get("members") # Call to get group details group = client.get_group_request(group_id=group_id) # type: ignore group_type = group.get("type") updated_members = group_members = group.get("members") members_list = argToList(members) removed_members = [] if group_type.lower() == "ip" or group_type.lower() == "domain": # type: ignore for member in members_list: if member in group_members: # type: ignore removed_members.append(member) updated_members.remove(member) # type: ignore elif group_type.lower() == "account": # type: ignore uids = [i.get("uid") for i in group_members] # type: ignore for member in members_list: if member in uids: removed_members.append(member) uids.remove(member) updated_members = uids elif group_type.lower() == "host": # type: ignore ids = [str(i.get("id")) for i in group_members] # type: ignore for member in members_list: if member in ids: removed_members.append(member) ids.remove(member) updated_members = ids if not removed_members: return CommandResults(readable_output=f"##### Member(s) {', '.join(members_list)} do not exist in the group.") # Call Vectra API to unassign members in group res = client.update_group_members_request(group_id=group_id, members=updated_members) # type: ignore updated_group = remove_empty_elements(res) human_readable = get_group_unassign_and_assign_command_hr( group=updated_group, changed_members=removed_members, assign_flag=False ) return CommandResults( outputs_prefix="Vectra.Group", outputs=createContext(updated_group), readable_output=human_readable, raw_response=updated_group, outputs_key_field="group_id", ) def vectra_group_assign_command(client: Client, args: dict[str, Any]): """ Assign members in Group. Args: client (Client): An instance of the Client class. args (Dict[str, Any]): The command arguments. Returns: CommandResults: The command results. """ validate_group_assign_and_unassign_command_args(args) group_id = args.get("group_id") members = args.get("members") # Call to get group details group = client.get_group_request(group_id=group_id) # type: ignore group_type = group.get("type") updated_members = group_members = group.get("members") members_list = argToList(members) added_members = [] if group_type.lower() == "ip" or group_type.lower() == "domain": # type: ignore for member in members_list: if member not in group_members: # type: ignore added_members.append(member) updated_members.append(member) # type: ignore elif group_type.lower() == "account": # type: ignore uids = [i.get("uid") for i in group_members] # type: ignore for member in members_list: if member not in uids: added_members.append(member) uids.append(member) updated_members = uids elif group_type.lower() == "host": # type: ignore ids = [str(i.get("id")) for i in group_members] # type: ignore for member in members_list: if member not in ids: added_members.append(member) ids.append(member) updated_members = ids if not added_members: return CommandResults(readable_output=f"##### Member(s) {', '.join(members_list)} are already in the group.") # Call Vectra API to assign members in group res = client.update_group_members_request(group_id=group_id, members=updated_members) # type: ignore updated_group = remove_empty_elements(res) human_readable = get_group_unassign_and_assign_command_hr( group=updated_group, changed_members=added_members, assign_flag=True ) return CommandResults( outputs_prefix="Vectra.Group", outputs=createContext(updated_group), readable_output=human_readable, raw_response=updated_group, outputs_key_field="group_id", ) def mark_detections_asclosed_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Mark the detection as closed by providing ID of detections and close reason in the argument. - params: - client: Vectra Client - args: Command arguments """ detection_ids = argToList(args.get("detection_ids")) detection_ids = [detection_id.strip() for detection_id in detection_ids if detection_id.strip()] # Validate detection ids if not detection_ids: raise ValueError(ERRORS["REQUIRED_ARGUMENT"].format("detection_ids")) all(validate_positive_integer_arg(detection_id, arg_name="detection_ids") for detection_id in detection_ids) close_reason = args.get("close_reason", "").lower() if not close_reason: raise ValueError(ERRORS["REQUIRED_ARGUMENT"].format("close_reason")) # Validate close_reason if close_reason not in VALID_CLOSE_REASON: raise ValueError("Invalid close_reason. Must be 'benign' or 'remediated'.") api_response = client.close_detections_by_ids(ids_list=detection_ids, reason=close_reason) if api_response.get("_meta", {}).get("level", "").lower() == "success": readable_output = f"##### The provided detection IDs have been successfully closed as {close_reason}." else: res_message = api_response.get("_meta", {}).get("message", "") message = "Something went wrong." if res_message: message += f" Message: {res_message}." raise DemistoException(message) command_result = CommandResults(readable_output=readable_output, raw_response=api_response) return command_result def mark_detections_asopen_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Open detection with provided detection IDs. Args: client (Client): An instance of the Client class. args (Dict[str, Any]): The command arguments. Raises: ValueError: If detection_ids argument is missing or empty. Returns: CommandResults: The command results. """ # Get function arguments detection_ids = argToList(args.get("detection_ids")) detection_ids = [detection_id.strip() for detection_id in detection_ids if detection_id.strip()] # Validate detection_ids if not detection_ids: raise ValueError(ERRORS["REQUIRED_ARGUMENT"].format("detection_ids")) all(validate_positive_integer_arg(detection_id, arg_name="detection_ids") for detection_id in detection_ids) # Call Vectra API to open detections api_response = client.open_detections_by_ids(ids_list=detection_ids) if api_response.get("_meta", {}).get("level", "").lower() == "success": readable_output = "##### The provided detection IDs have been successfully re-opened." else: res_message = api_response.get("_meta", {}).get("message", "") message = "Something went wrong." if res_message: message += f" Message: {res_message}." raise DemistoException(message) command_result = CommandResults(readable_output=readable_output, raw_response=api_response) return command_result # #### #### # # ## MAIN FUNCTION ## # def main() -> None: # pragma: no cover # Set some settings as global (to use them inside some functions) global global_UI_URL integration_params = remove_space_from_args(demisto.params()) remove_nulls_from_dictionary(integration_params) command = demisto.command() kwargs = remove_space_from_args(demisto.args()) remove_nulls_from_dictionary(kwargs) server_fqdn: str | None = integration_params.get("server_fqdn") if not server_fqdn: # Should be impossible thx to UI required settings control raise DemistoException("Missing integration setting : 'Server FQDN'") auth_type = integration_params.get("authentication_type", "API Token").lower() use_oauth = auth_type == "oauth 2.0" credentials: dict | None = integration_params.get("credentials") oauth_credentials: dict | None = integration_params.get("oauth_credentials") # Handle different authentication types api_token: str | None = None client_id: str | None = None client_secret: str | None = None if use_oauth: # OAuth 2.0 authentication if not oauth_credentials: raise DemistoException( "Missing integration setting : 'OAuth 2.0 Credentials' are required for OAuth 2.0 authentication" ) client_id = oauth_credentials.get("identifier", "").strip() client_secret = oauth_credentials.get("password", "").strip() if not client_id or not client_secret: raise DemistoException( "Missing integration setting : 'Client ID' and 'Client Secret' are required for OAuth 2.0 authentication" ) else: # API Token authentication (default) if not credentials: raise DemistoException("Missing integration setting : 'Credentials' or 'API token'") api_token = credentials.get("password", "").strip() if (api_token is None) or (api_token == ""): raise DemistoException("Missing integration setting : 'Credentials password' or 'API token'") # Setting default settings for fetch mode if integration_params.get("isFetch"): if integration_params.get("first_fetch") == "": integration_params["first_fetch"] = DEFAULT_FIRST_FETCH demisto.debug(f"First fetch timestamp not set, setting to default '{DEFAULT_FIRST_FETCH}'") if integration_params.get("fetch_entity_types") == []: integration_params["fetch_entity_types"] = DEFAULT_FETCH_ENTITY_TYPES demisto.debug(f"Fetch entity types not set, setting to default '{DEFAULT_FETCH_ENTITY_TYPES}'") if integration_params.get("max_fetch") == "": integration_params["max_fetch"] = DEFAULT_MAX_FETCH demisto.debug(f"Max incidents per fetch not set, setting to default '{DEFAULT_MAX_FETCH}'") verify_certificate: bool = not integration_params.get("insecure", False) use_proxy: bool = integration_params.get("use_proxy", False) global_UI_URL = urljoin("https://", server_fqdn) api_base_url = urljoin("https://", urljoin(server_fqdn, API_VERSION_URL)) demisto.info(f"Command being called is {command}") try: # Create client with appropriate authentication client = Client( base_url=api_base_url, verify=verify_certificate, proxy=use_proxy, use_oauth=use_oauth, api_token=api_token, client_id=client_id, client_secret=client_secret, ) if command == "test-module": # This is the call made when pressing the integration Test button. results = test_module(client, integration_params) return_results(results) elif command == "fetch-incidents": # Get new incidents to create if any from Vectra API next_run, incidents = fetch_incidents(client, integration_params) # Add incidents in the SOAR platform demisto.incidents(incidents) if next_run: demisto.info(f"Setting last run to : {next_run}") demisto.setLastRun(next_run) demisto.info("fetch-incidents action done.") elif command == "get-modified-remote-data": return_results(get_modified_remote_data_command(client)) # type: ignore elif command == "get-remote-data": return_results(get_remote_data_command(client, integration_params)) # type: ignore elif command == "update-remote-system": return_results(update_remote_system_command(client)) elif command == "vectra-search-accounts": return_results(vectra_search_accounts_command(client, **kwargs)) elif command == "vectra-search-hosts": return_results(vectra_search_hosts_command(client, **kwargs)) elif command == "vectra-search-detections": return_results(vectra_search_detections_command(client, **kwargs)) elif command == "vectra-search-assignments": return_results(vectra_search_assignments_command(client, **kwargs)) elif command == "vectra-search-outcomes": return_results(vectra_search_outcomes_command(client, **kwargs)) elif command == "vectra-search-users": return_results(vectra_search_users_command(client, **kwargs)) # ## Accounts centric commands elif command == "vectra-account-describe": return_results(vectra_get_account_by_id_command(client, **kwargs)) elif command == "vectra-account-add-tags": return_results(add_tags_command(client, type="account", **kwargs)) elif command == "vectra-account-del-tags": return_results(del_tags_command(client, type="account", **kwargs)) elif command == "vectra-account-tag-list": return_results(tag_list_command(client, entity_type="account", args=kwargs)) elif command == "vectra-account-note-add": return_results(note_add_command(client, entity_type="account", args=kwargs)) elif command == "vectra-account-note-update": return_results(note_update_command(client, entity_type="account", args=kwargs)) elif command == "vectra-account-note-remove": return_results(note_remove_command(client, entity_type="account", args=kwargs)) elif command == "vectra-account-note-list": return_results(note_list_command(client, entity_type="account", args=kwargs)) elif command == "vectra-account-markall-detections-asfixed": return_results(markall_detections_asfixed_command(client, type="account", **kwargs)) elif command == "vectra-account-markall-detections-asclosed": return_results(markall_detections_asclosed_command(client, entity_type="account", args=kwargs)) # ## Hosts centric commands elif command == "vectra-host-describe": return_results(vectra_get_host_by_id_command(client, **kwargs)) elif command == "vectra-host-add-tags": return_results(add_tags_command(client, type="host", **kwargs)) elif command == "vectra-host-del-tags": return_results(del_tags_command(client, type="host", **kwargs)) elif command == "vectra-host-tag-list": return_results(tag_list_command(client, entity_type="host", args=kwargs)) elif command == "vectra-host-note-add": return_results(note_add_command(client, entity_type="host", args=kwargs)) elif command == "vectra-host-note-update": return_results(note_update_command(client, entity_type="host", args=kwargs)) elif command == "vectra-host-note-remove": return_results(note_remove_command(client, entity_type="host", args=kwargs)) elif command == "vectra-host-note-list": return_results(note_list_command(client, entity_type="host", args=kwargs)) elif command == "vectra-host-markall-detections-asfixed": return_results(markall_detections_asfixed_command(client, type="host", **kwargs)) elif command == "vectra-host-markall-detections-asclosed": return_results(markall_detections_asclosed_command(client, entity_type="host", args=kwargs)) # ## Detections centric commands elif command == "vectra-detection-describe": return_results(vectra_get_detection_by_id_command(client, **kwargs)) elif command == "vectra-detection-get-pcap": return_results(get_detection_pcap_file_command(client, **kwargs)) elif command == "vectra-detection-markasfixed": return_results(mark_detection_as_fixed_command(client, **kwargs)) elif command == "vectra-detection-add-tags": return_results(add_tags_command(client, type="detection", **kwargs)) elif command == "vectra-detection-del-tags": return_results(del_tags_command(client, type="detection", **kwargs)) elif command == "vectra-detection-tag-list": return_results(tag_list_command(client, entity_type="detection", args=kwargs)) elif command == "vectra-detection-note-add": return_results(note_add_command(client, entity_type="detection", args=kwargs)) elif command == "vectra-detection-note-update": return_results(note_update_command(client, entity_type="detection", args=kwargs)) elif command == "vectra-detection-note-remove": return_results(note_remove_command(client, entity_type="detection", args=kwargs)) elif command == "vectra-detection-note-list": return_results(note_list_command(client, entity_type="detection", args=kwargs)) elif command == "vectra-detections-mark-asclosed": return_results(mark_detections_asclosed_command(client, args=kwargs)) elif command == "vectra-detections-mark-asopen": return_results(mark_detections_asopen_command(client, args=kwargs)) # ## Assignments / Assignment outcomes commands elif command == "vectra-assignment-describe": return_results(vectra_get_assignment_by_id_command(client, **kwargs)) elif command == "vectra-assignment-assign": return_results(vectra_assignment_assign_command(client, **kwargs)) elif command == "vectra-assignment-resolve": return_results(vectra_assignment_resolve_command(client, **kwargs)) elif command == "vectra-outcome-describe": return_results(vectra_get_outcome_by_id_command(client, **kwargs)) elif command == "vectra-outcome-create": return_results(vectra_outcome_create_command(client, **kwargs)) elif command == "vectra-user-describe": return_results(vectra_get_user_by_id_command(client, **kwargs)) # ## Groups / Groups outcomes commands elif command == "vectra-group-list": return_results(vectra_group_list_command(client, kwargs)) elif command == "vectra-group-assign": return_results(vectra_group_assign_command(client, kwargs)) elif command == "vectra-group-unassign": return_results(vectra_group_unassign_command(client, kwargs)) else: raise NotImplementedError # Log exceptions and return errors except Exception as e: return_error(f"Failed to execute {command} command.\nError:\n{e!s}") # #### #### # # ## ENTRY POINT ## # if __name__ in ("__main__", "__builtin__", "builtins"): # pragma: no cover main()