Vega
Vega integration for fetching alerts and incidents from the Vega platform.
Analytics & SIEM · Vega
Details
| ID | Vega |
|---|---|
| Provider | Vega |
| Category | Analytics & SIEM |
| From Version | 6.10.0 |
| Docker Image | demisto/python3:3.12.13.10404775 |
| Supported Modules | Agentix XSIAM |
README
Vega integration for fetching alerts and incidents from the Vega platform.
This is the default integration for this content pack when configured by the Data Onboarder in Cortex XSIAM.
Configure Vega in Cortex
| Parameter | Description | Required |
|---|---|---|
| Base URL | The Base URL of the Vega API. | True |
| Access Key ID | The Access Key ID used to authenticate with the Vega API. | True |
| Access Key | The Access Key used to authenticate with the Vega API. | True |
| Fetch incidents | False | |
| Incidents Fetch Interval | True | |
| Maximum incidents per fetch | The Maximum number of Vega alerts and incidents to fetch per cycle, combined. Valid range is 1-50. Invalid values default to 50 during fetch. | True |
| Fetch Lookback (minutes) | The number of minutes the query window is shifted backwards on each fetch cycle to catch alerts and incidents that were indexed late on the Vega side. Deduplication prevents re- ingestion. Valid range is 1-60. | True |
| Vega Entities to fetch | The Vega entities to fetch as Cortex XSOAR incidents. | True |
| Backfill Days | The number of days before today to fetch alerts and incidents on the first run. Use 0 for today only. Valid range is 0–365. | True |
| Enable Cortex XSOAR to Vega mirroring | Whether to enable Cortex XSOAR to Vega mirroring. When enabled, changes made in Cortex XSOAR investigations are mirrored to Vega for status, verdict, verdict reasoning, severity, and comments. Requires the Vega Outgoing Mapper on this instance. When disabled, Vega to Cortex XSOAR mirroring remains enabled. | False |
| Outgoing fields to mirror | The investigation fields that are mirrored from Cortex XSOAR to Vega when outgoing mirroring is enabled. If empty, all fields are mirrored. War Room comments are included when Comments is selected. Any custom values entered outside the available options are ignored. | False |
| Alert Severities to fetch | The severities by which to filter alerts. If empty, all severities are fetched. Any custom values entered outside the available options are ignored and will not affect the fetch cycle. | False |
| Alert Statuses to fetch | The statuses by which to filter alerts. If empty, all statuses are fetched. Any custom values entered outside the available options are ignored and will not affect the fetch cycle. | False |
| Alert Verdicts to fetch | The verdicts by which to filter alerts. If empty, all verdicts are fetched. Any custom values entered outside the available options are ignored and will not affect the fetch cycle. | False |
| Has related incidents | The filter for alerts based on whether they have related incidents. Select Yes to fetch alerts with related incidents, No to fetch alerts without related incidents, or both to fetch all alerts. At least one option must be selected. Filter alerts by whether they have related incidents. Select Yes to fetch alerts with related incidents, No to fetch alerts without related incidents, or both to fetch all alerts. At least one option must be selected. | True |
| Incident Severities to fetch | The severities by which to filter incidents. If empty, all severities are fetched. Any custom values entered outside the available options are ignored and will not affect the fetch cycle. Filter incidents by severity. If empty, all severities are fetched. Any custom values entered outside the available options are ignored and will not affect the fetch cycle. | False |
| Incident Statuses to fetch | The statuses by which to filter incidents. If empty, all statuses are fetched. Any custom values entered outside the available options are ignored and will not affect the fetch cycle. Filter incidents by status. If empty, all statuses are fetched. Any custom values entered outside the available options are ignored and will not affect the fetch cycle. | False |
| Incident Verdicts to fetch | The verdicts by which to filter incidents. If empty, all verdicts are fetched. Any custom values entered outside the available options are ignored and will not affect the fetch cycle. Filter incidents by verdict. If empty, all verdicts are fetched. Any custom values entered outside the available options are ignored and will not affect the fetch cycle. | False |
| Incident type | False | |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
vega-get-alert-events
Fetch all aggregated alert events for a Vega alert using internal API pagination, then return the requested display page as a markdown table and CustomFields for the Alert Events layout section.
Base Command
vega-get-alert-events
Input
| Argument Name | Description | Required |
|---|---|---|
| alert_id | The Vega alert API id (UUID). When omitted, resolves the alert id from the current Vega Alert incident. | Optional |
| limit | The number of alert events to display per page. Also used as the Vega API batch size when fetching all events. Default is 200. | Optional |
| offset | The pagination offset for alert events. Default is 0. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vega.AlertEvents.AlertId | String | Vega alert ID. |
| Vega.AlertEvents.Total | Number | Total number of alert events reported by Vega. |
| Vega.AlertEvents.Offset | Number | Pagination offset used for the current page. |
| Vega.AlertEvents.Limit | Number | Page size used for the current fetch. |
| Vega.AlertEvents.Count | Number | Number of alert events returned in the current page. |
| Vega.AlertEvents.HasAlertEvents | Boolean | Whether the alert returned real alert events instead of aggregated parse-field summary rows. |
| Vega.AlertEvents.Cached | Boolean | Whether the response was served from cached incident data. |
| Vega.AlertEvents.Events | Unknown | Enriched alert events for the current page. JSON fields are parsed and fields._raw contents are promoted to top-level keys under fields. |
| Vega.AlertEvents.CustomFields | Unknown | Incident custom fields to persist for the Alert Events layout section. |
vega-set-detections-state
Set the state for one or more Vega detections.
Base Command
vega-set-detections-state
Input
| Argument Name | Description | Required |
|---|---|---|
| ids | A comma-separated list of Vega detection IDs to update. | Required |
| state | The target detection state. Possible values are: ENABLED, DISABLED, TEST_MODE. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Vega.DetectionsState.State | String | The state applied to the detections. |
| Vega.DetectionsState.IDs | String | Detection IDs updated by Vega. |
| Vega.DetectionsState.Count | Number | Number of detection IDs updated. |
vega-update-detections
Update severity, status, state, and/or tags for one or more Vega detections using the updateDetections GraphQL mutation.
Base Command
vega-update-detections
Input
| Argument Name | Description | Required |
|---|---|---|
| detection_id | A comma-separated list of Vega detection IDs to update. | Required |
| severity | The target Vega detection severity. Possible values are: LOW, MEDIUM, HIGH, CRITICAL. | Optional |
| state | The target Vega detection state. Possible values are: ENABLED, DISABLED, TEST_MODE. | Optional |
| tags | A comma-separated list of tags to apply to the Vega detection. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vega.Detection.ID | String | Updated Vega detection ID. |
| Vega.Detection.Name | String | Updated Vega detection name. |
| Vega.Detection.Severity | String | Updated Vega detection severity. |
| Vega.Detection.Status | String | Updated Vega detection status. |
| Vega.Detection.State | String | Updated Vega detection state. |
| Vega.Detection.Tags | String | Updated Vega detection tags. |
| Vega.Detection.ValidationStatus | String | Vega validation status for the detection update. |
vega-update-alert
Immediately update Vega alert status, severity, verdict, verdict reasoning, assignees, and/or comment on the Vega platform and sync the open Cortex XSOAR investigation when run from a Vega Alert investigation.
Base Command
vega-update-alert
Input
| Argument Name | Description | Required |
|---|---|---|
| alert_ids | A comma-separated list of Vega alert IDs to update. Accepts a comma-separated list or repeated values (for example, alert_ids=alert-1,alert-2). Use this to update alerts directly from the war room without opening an investigation. When omitted, the alert ID is resolved from the current Vega Alert investigation. One or more Vega alert IDs to update. Accepts a comma-separated list or repeated values (for example, alert_ids=alert-1,alert-2). Use this to update alerts directly from the war room without opening an investigation. When omitted, the alert ID is resolved from the current Vega Alert investigation. | Optional |
| status | The target Vega alert status. Possible values are: OPEN, IN PROGRESS, PEER REVIEW, RESOLVED. | Optional |
| verdict | The target Vega alert verdict. Possible values are: MALICIOUS, SUSPICIOUS, BENIGN, INCONCLUSIVE, NA. | Optional |
| severity | The target Vega alert severity. Possible values are: LOW, MEDIUM, HIGH, CRITICAL. | Optional |
| verdict_reasoning | The target Vega alert verdict reasoning. | Optional |
| comment | The comment to add on the Vega alert. | Optional |
| assignees | A comma-separated list of Vega user IDs to assign to the alert. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vega.Alert.id | String | Updated Vega alert ID. |
| Vega.Alert.status | String | Updated Vega alert status. |
| Vega.Alert.severity | String | Updated Vega alert severity. |
| Vega.Alert.verdict | String | Updated Vega alert verdict. |
| Vega.Alert.assignee | String | Updated Vega alert assignee email, display name, or user ID. |
vega-update-incident
Immediately update Vega incident status, verdict, verdict reasoning, severity, assignee emails, and/or comment on the Vega platform and sync the open Cortex XSOAR investigation when run from a Vega Incident investigation.
Base Command
vega-update-incident
Input
| Argument Name | Description | Required |
|---|---|---|
| incident_ids | A comma-separated list of Vega incident IDs to update. Accepts a comma-separated list or repeated values (for example, incident_ids=inc-1,inc-2). Use this to update incidents directly from the war room without opening an investigation. When omitted, the incident ID is resolved from the current Vega Incident investigation. One or more Vega incident IDs to update. Accepts a comma-separated list or repeated values (for example, incident_ids=inc-1,inc-2). Use this to update incidents directly from the war room without opening an investigation. When omitted, the incident ID is resolved from the current Vega Incident investigation. | Optional |
| status | The target Vega incident status. Possible values are: NEW, INVESTIGATING, ON HOLD, EXTERNAL ESCALATION, RESOLVED, REOPENED, REVIEW RECOMMENDED, RESPONSE REQUIRED, UNDER REVIEW. | Optional |
| verdict | The target Vega incident verdict. Possible values are: MALICIOUS, SUSPICIOUS, BENIGN, INCONCLUSIVE, NA. | Optional |
| severity | The target Vega incident severity. Possible values are: LOW, MEDIUM, HIGH, CRITICAL. | Optional |
| verdict_reasoning | The target Vega incident verdict reasoning. | Optional |
| comment | The comment to add on the Vega incident. | Optional |
| assignee_emails | A comma-separated list of email addresses to assign to the Vega incident. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Vega.Incident.id | String | Updated Vega incident ID. |
| Vega.Incident.status | String | Updated Vega incident status. |
| Vega.Incident.verdict | String | Updated Vega incident verdict. |
| Vega.Incident.severity | String | Updated Vega incident severity. |
| Vega.Incident.assignee | String | Updated Vega incident assignee email, display name, or user ID. |
get-remote-data
Gets remote data from a remote Vega alert or incident. Used for debugging incoming mirroring.
Base Command
get-remote-data
Input
| Argument Name | Description | Required |
|---|---|---|
| id | The remote Vega alert or incident ID. | Required |
| lastUpdate | The UTC timestamp in seconds (e.g., 1672531200). The incident is only updated if it was modified after the last update time. | Optional |
Context Output
There is no context output for this command.
get-modified-remote-data
Gets Vega alert and incident IDs modified since the last update time. Used for debugging incoming mirroring.
Base Command
get-modified-remote-data
Input
| Argument Name | Description | Required |
|---|---|---|
| lastUpdate | The UTC timestamp in seconds (e.g., 1672531200). Returns Vega alert and incident IDs updated since this time. | Required |
Context Output
There is no context output for this command.
update-remote-system
Pushes Cortex XSOAR investigation changes to Vega when outgoing mirroring is enabled.
Base Command
update-remote-system
Input
There are no input arguments for this command.
Context Output
There is no context output for this command.
get-mapping-fields
Returns the outgoing mirroring fields for Vega Alert and Vega Incident investigations.
Base Command
get-mapping-fields
Input
There are no input arguments for this command.
Context Output
There is no context output for this command.
Incident Mirroring
You can enable incident mirroring between Cortex XSOAR incidents and Vega corresponding events (available from Cortex XSOAR version 6.0.0).
To set up the mirroring:
- Enable Fetching incidents in your instance configuration.
Newly fetched incidents will be mirrored in the chosen direction. However, this selection does not affect existing incidents.
Important Note: To ensure the mirroring works as expected, mappers are required, both for incoming and outgoing, to map the expected fields in Cortex XSOAR and Vega.
Configuration parameters
url— Base URL (required)access_key_id— Access Key ID (required)access_key— Access Key (required)isFetch— Fetch incidentsincidentFetchInterval— Incidents Fetch Interval (required)max_fetch— Maximum incidents per fetch (required)lookback_minutes— Fetch Lookback (minutes) (required)vega_entities— Vega Entities to fetch (required)backfill_days— Backfill Days (required)autoclosure— Enable Cortex XSOAR to Vega mirroringoutgoing_mirror_fields— Outgoing fields to mirroralert_severities— Alert Severities to fetchalert_statuses— Alert Statuses to fetchalert_verdicts— Alert Verdicts to fetchalert_has_related_incidents— Has related incidents (required)incident_severities— Incident Severities to fetchincident_statuses— Incident Statuses to fetchincident_verdicts— Incident Verdicts to fetchincidentType— Incident typeinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (9)
-
get-mapping-fieldsReturns the outgoing mirroring fields for Vega Alert and Vega Incident investigations.
-
get-modified-remote-dataGets Vega alert and incident IDs modified since the last update time. Used for debugging incoming mirroring.
-
get-remote-dataGets remote data from a remote Vega alert or incident. Used for debugging incoming mirroring.
-
update-remote-systemPushes Cortex XSOAR investigation changes to Vega when outgoing mirroring is enabled.
-
vega-get-alert-eventsFetch all aggregated alert events for a Vega alert using internal API pagination, then return the requested display page as a markdown table and CustomFields for the Alert Events layout section.
-
vega-set-detections-stateSet the state for one or more Vega detections.
-
vega-update-alertImmediately update Vega alert status, severity, verdict, verdict reasoning, assignees, and/or comment on the Vega platform and sync the open Cortex XSOAR investigation when run from a Vega Alert investigation.
-
vega-update-detectionsUpdate severity, status, state, and/or tags for one or more Vega detections using the updateDetections GraphQL mutation.
-
vega-update-incidentImmediately update Vega incident status, verdict, verdict reasoning, severity, assignee emails, and/or comment on the Vega platform and sync the open Cortex XSOAR investigation when run from a Vega Incident investigation.
import json from datetime import datetime, timedelta, UTC import requests import demistomock as demisto from CommonServerPython import * from CommonServerUserPython import * import pytest from Vega import ( ALERT_EVENTS_NOT_AVAILABLE_MARKDOWN, _alert_events_command_results, _enrich_alert_event, _enrich_alert_events, _event_has_bad_alert_events_shape, _events_have_bad_alert_events_shape, _expand_flat_raw_fields, _format_alert_events_markdown, _format_mitre_attack, _promote_raw_into_alert_event_fields, Client, GET_ALERT_MIRROR_QUERY, GET_INCIDENT_MIRROR_QUERY, _suppress_noisy_http_integration_logs, _build_fetch_filter_fingerprint, _build_vega_alert_custom_fields, _build_vega_incident_custom_fields, _fetch_paginated_entities, _is_retryable_http_error, _format_bullet_list, _format_key_findings_html, _format_raw_entity_for_xsoar, _format_recommended_actions_for_grid, VEGA_NO_RECOMMENDED_ACTIONS_DISPLAY, _format_timeline_events_html, _format_vega_comments_html, _is_empty_vega_comment_text, _build_effective_alert_update_args, _build_effective_incident_update_args, _build_direct_alert_update_payload, _build_direct_incident_update_payload, _resolve_incident_status_for_update, _normalize_vega_severity_for_display, MIRROR_ENTITY_SUFFIX_ALERT, MIRROR_ENTITY_SUFFIX_INCIDENT, VEGA_ALERT_STATUS_FIELD, VEGA_ALERT_SEVERITY_FIELD, VEGA_VERDICT_FIELD, VEGA_INCIDENT_STATUS_FIELD, _normalize_vega_status_for_display, _normalize_entity_id, _normalize_verdict_reasoning_for_display, _extract_verdict_reasoning_from_entity, _mirror_entity_type_from_args, _entity_type_from_field_keys, _entity_type_from_mirror_payload, _parse_alert_events_results, _resolve_fetch_from_time, alert_to_incident, build_alert_events_custom_fields, fetch_alert_events_command, fetch_alert_events_page, fetch_incidents_command, _fetch_alert_events_for_ingest, set_detections_state_command, update_detections_command, incident_to_xsoar_incident, parse_backfill_days, load_current_incident, resolve_alert_id_from_incident, resolve_incident_id_from_incident, update_alert_command, update_incident_command, _build_comment_war_room_entry, _get_mirroring_fields, _is_xsoar_to_vega_mirroring_enabled, get_modified_remote_data_command, get_remote_data_command, update_remote_system_command, get_mapping_fields_command, _build_incoming_status_sync_entries, _entity_updated_after, _entity_matches_remote_id, _resolve_remote_entity, _normalize_incident_api_entity, _build_mirror_sync_object, _extract_vega_verdict_from_entity, _resolve_mirror_updated_from, _poll_entity_is_alert, _mirror_entity_suffix_from_poll_entity, _resolve_mirror_incident_lookup_filters, _resolve_mirror_entity_lookup_filters, _resolve_mirror_updated_to, _normalize_mirror_field_value, _mirror_field_value, _mirror_field_changed_in_delta, _build_outgoing_alert_mirror_update, _collect_outgoing_entry_comments, _outgoing_mirror_comment_value, VEGA_NEW_COMMENT_FIELD, VEGA_NEW_COMMENT_LAYOUT_DEFAULT, VEGA_MIRROR_TAG_FROM_VEGA, VEGA_MIRROR_TAG_TO_VEGA, RATE_LIMIT_INITIAL_WAIT_SECONDS, validate_backfill_days, validate_max_fetch, _resolve_max_fetch, MAX_FETCH_ERROR, filter_alert_severities, filter_alert_statuses, filter_alert_verdicts, filter_incident_severities, filter_incident_statuses, filter_incident_verdicts, resolve_has_related_incidents, TEST_CONNECTION_ACCESS_KEY_ERROR, TEST_CONNECTION_ACCESS_KEY_ID_ERROR, TEST_CONNECTION_BASE_URL_ERROR, TEST_CONNECTION_URL_ERROR, test_module as vega_test_module, main as vega_main, ) _VEGA_API_HOST = "api" + ".vega.com" BASE_URL = f"https://{_VEGA_API_HOST}" MOCK_JWT_RESPONSE = { "session_jwt": "mock-jwt-token", "session_max_age": 1999999999, # Far in the future "error": "", } def test_test_module(requests_mock, mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch.object(demisto, "info") requests_mock.post(f"{BASE_URL}/api/v1/login_machine", json=MOCK_JWT_RESPONSE) requests_mock.post( f"{BASE_URL}/api/v1/query", json={"data": {"getAccessKey": {"id": "mock-key-id", "roles": ["security admin"]}}}, ) client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) assert vega_test_module(client, backfill_days=30, max_fetch=50, lookback_minutes=5) == "ok" def test_test_module_unauthorized(requests_mock, mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch.object(demisto, "info") requests_mock.post(f"{BASE_URL}/api/v1/login_machine", json=MOCK_JWT_RESPONSE) requests_mock.post( f"{BASE_URL}/api/v1/query", json={"data": {"getAccessKey": {"id": "mock-key-id", "roles": ["Viewer"]}}}, ) client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) assert ( vega_test_module(client, backfill_days=30, max_fetch=50, lookback_minutes=5) == "You do not have required access to fetch incidents." ) def test_test_module_incorrect_access_key_id(requests_mock, mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch.object(demisto, "info") requests_mock.post(f"{BASE_URL}/api/v1/login_machine", json=MOCK_JWT_RESPONSE) requests_mock.post( f"{BASE_URL}/api/v1/query", json={ "errors": [ { "message": "Internal Server Error", "extensions": { "error_code": "E000000000", "error_code_name": "INTERNAL_SERVER_ERROR", "extra_args": None, "trace_id": 8786647935177050492, }, } ], "data": {"getAccessKey": None}, }, ) client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) assert vega_test_module(client, backfill_days=30, max_fetch=50, lookback_minutes=5).startswith( TEST_CONNECTION_ACCESS_KEY_ID_ERROR ) def test_test_module_incorrect_access_key(requests_mock, mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch.object(demisto, "info") requests_mock.post(f"{BASE_URL}/api/v1/login_machine", status_code=500) client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="wrong-key", access_key_id="test-key-id", ) assert vega_test_module(client, backfill_days=30, max_fetch=50, lookback_minutes=5) == TEST_CONNECTION_ACCESS_KEY_ERROR def test_test_module_connection_error(requests_mock, mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch.object(demisto, "info") requests_mock.post( f"{BASE_URL}/api/v1/login_machine", exc=requests.exceptions.ConnectionError("Failed to establish a new connection"), ) client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) assert vega_test_module(client, backfill_days=30, max_fetch=50, lookback_minutes=5) == TEST_CONNECTION_URL_ERROR def test_test_module_wrong_url_not_found(requests_mock, mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch.object(demisto, "info") requests_mock.post(f"{BASE_URL}/api/v1/login_machine", status_code=404, text="Not Found") client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) assert vega_test_module(client, backfill_days=30, max_fetch=50, lookback_minutes=5) == TEST_CONNECTION_BASE_URL_ERROR def test_test_module_whitespace_base_url(mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch.object(demisto, "info") client = Client( base_url=" ", verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) assert vega_test_module(client, backfill_days=30, max_fetch=50, lookback_minutes=5) == TEST_CONNECTION_BASE_URL_ERROR def test_main_test_module_requires_backfill_days(mocker): mocker.patch.object( demisto, "params", return_value={ "access_key": {"password": "key"}, "access_key_id": {"password": "id"}, "url": BASE_URL, "vega_entities": ["Alerts", "Incidents"], "max_fetch": "50", "lookback_minutes": "5", }, ) mocker.patch.object(demisto, "command", return_value="test-module") mock_return_results = mocker.patch("Vega.return_results") mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch.object(demisto, "info") vega_main() mock_return_results.assert_called_once_with("backfill_days must be an integer between 0 and 365.") def test_main_test_module_rejects_invalid_max_fetch(mocker): mocker.patch.object( demisto, "params", return_value={ "access_key": {"password": "key"}, "access_key_id": {"password": "id"}, "url": BASE_URL, "vega_entities": ["Alerts", "Incidents"], "backfill_days": "30", "max_fetch": "100", "lookback_minutes": "5", }, ) mocker.patch.object(demisto, "command", return_value="test-module") mock_return_results = mocker.patch("Vega.return_results") mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch.object(demisto, "info") vega_main() mock_return_results.assert_called_once_with(MAX_FETCH_ERROR) def test_test_module_rejects_invalid_max_fetch(mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch.object(demisto, "info") client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) assert vega_test_module(client, backfill_days=30, max_fetch=0, lookback_minutes=5) == MAX_FETCH_ERROR assert vega_test_module(client, backfill_days=30, max_fetch=100, lookback_minutes=5) == MAX_FETCH_ERROR assert vega_test_module(client, backfill_days=30, max_fetch="abc", lookback_minutes=5) == 'Invalid number: "max_fetch"="abc"' def test_validate_max_fetch_accepts_valid_range(): validate_max_fetch(1) validate_max_fetch(50) validate_max_fetch("25") def test_validate_max_fetch_rejects_invalid_values(): with pytest.raises(ValueError, match=MAX_FETCH_ERROR): validate_max_fetch(0) with pytest.raises(ValueError, match=MAX_FETCH_ERROR): validate_max_fetch(51) with pytest.raises(ValueError, match='Invalid number: "max_fetch"="not-a-number"'): validate_max_fetch("not-a-number") with pytest.raises(ValueError, match=MAX_FETCH_ERROR): validate_max_fetch(None) def test_resolve_max_fetch_defaults_invalid_values(): assert _resolve_max_fetch(None) == 50 assert _resolve_max_fetch("0") == 50 assert _resolve_max_fetch("100") == 50 assert _resolve_max_fetch("abc") == 50 assert _resolve_max_fetch("25") == 25 assert _resolve_max_fetch("50") == 50 def test_url_normalization(): # Test cases for URL normalization: (input_url, expected_normalized_url) test_cases = [ (BASE_URL, f"{BASE_URL}/api/v1/"), (f"{BASE_URL}/", f"{BASE_URL}/api/v1/"), (f"{BASE_URL}/api/v1", f"{BASE_URL}/api/v1/"), (f"{BASE_URL}/api/v1/", f"{BASE_URL}/api/v1/"), (f"{BASE_URL}/API/V1", f"{BASE_URL}/API/V1/"), (f"{BASE_URL}/API/v1/", f"{BASE_URL}/API/v1/"), (f" {BASE_URL} ", f"{BASE_URL}/api/v1/"), ] for input_url, expected in test_cases: client = Client( base_url=input_url, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) assert client._base_url == expected FIRST_FETCH_TIME = "2026-01-01T00:00:00Z" BACKFILL_DAYS = "30" TIMESTAMP_T1 = "2026-06-01T10:00:00Z" TIMESTAMP_T2 = "2026-06-01T11:00:00Z" CURRENT_TIME_CURSOR = "2026-06-04T17:00:00Z" def test_normalize_entity_id_coerces_numeric_ids(): assert _normalize_entity_id({"id": 12345}) == "12345" assert _normalize_entity_id({"id": "12345"}) == "12345" def test_fetch_incidents_command_dedup_numeric_id_at_boundary(mocker): mocker.patch.object(demisto, "debug") mock_client = mocker.Mock() numeric_id = 987654321 mock_client.get_alerts.return_value = { "alerts": [ { "id": numeric_id, "name": "Numeric ID Alert", "severity": "LOW", "createdAt": TIMESTAMP_T1, }, ], "total": 1, "limit": 200, "offset": 0, } mock_client.get_incidents.return_value = { "incidents": [], "total": 0, "limit": 200, "offset": 0, } last_run = { "alerts_last_fetch": TIMESTAMP_T1, "alerts_last_ids": [str(numeric_id)], } next_run, incidents = fetch_incidents_command( client=mock_client, last_run=last_run, fetch_alerts=True, fetch_incidents=False, alert_severities=None, alert_statuses=None, alert_verdicts=None, has_related_incidents=None, incident_severities=None, incident_statuses=None, incident_verdicts=None, first_fetch_time=FIRST_FETCH_TIME, ) assert incidents == [] assert set(next_run["alerts_last_ids"]) == {str(numeric_id)} assert "alerts_seen_ids" not in next_run def test_resolve_fetch_from_time_uses_backfill_when_cursor_not_anchored(): last_run = {"incidents_last_fetch": CURRENT_TIME_CURSOR} assert ( _resolve_fetch_from_time( last_run, "incidents_last_fetch", FIRST_FETCH_TIME, ) == FIRST_FETCH_TIME ) def test_resolve_fetch_from_time_uses_stored_cursor_when_present(): last_run = { "incidents_last_fetch": CURRENT_TIME_CURSOR, "incidents_fetch_config": _build_fetch_filter_fingerprint(None, None, None), } assert ( _resolve_fetch_from_time( last_run, "incidents_last_fetch", FIRST_FETCH_TIME, ) == CURRENT_TIME_CURSOR ) def test_resolve_fetch_from_time_keeps_cursor_when_fetch_filters_change(): previous_config = _build_fetch_filter_fingerprint(["HIGH"], None, None) current_config = _build_fetch_filter_fingerprint(["HIGH", "MEDIUM"], None, None) last_run = { "alerts_last_fetch": CURRENT_TIME_CURSOR, "alerts_fetch_config": previous_config, } assert ( _resolve_fetch_from_time( last_run, "alerts_last_fetch", FIRST_FETCH_TIME, ) == CURRENT_TIME_CURSOR ) assert current_config != previous_config def test_fetch_paginated_entities_multiple_pages(mocker): page_one = { "alerts": [{"id": "1", "createdAt": TIMESTAMP_T1}], "total": 2, "limit": 1, "offset": 0, } page_two = { "alerts": [{"id": "2", "createdAt": TIMESTAMP_T2}], "total": 2, "limit": 1, "offset": 1, } mock_get_alerts = mocker.Mock(side_effect=[page_one, page_two]) results, next_offset = _fetch_paginated_entities( mock_get_alerts, entities_key="alerts", from_time=FIRST_FETCH_TIME, ) assert len(results) == 2 assert next_offset is None assert results[0]["id"] == "1" assert results[1]["id"] == "2" assert mock_get_alerts.call_count == 2 assert mock_get_alerts.call_args_list[0].kwargs["offset"] == 0 assert mock_get_alerts.call_args_list[1].kwargs["offset"] == 1 def test_fetch_paginated_entities_fetches_beyond_single_page(mocker): """Verify pagination continues until total is reached when the API returns multiple pages.""" page_one = { "alerts": [{"id": str(i), "createdAt": TIMESTAMP_T1} for i in range(100)], "total": 250, "limit": 100, "offset": 0, } page_two = { "alerts": [{"id": str(i), "createdAt": TIMESTAMP_T2} for i in range(100, 200)], "total": 250, "limit": 100, "offset": 100, } page_three = { "alerts": [{"id": str(i), "createdAt": TIMESTAMP_T2} for i in range(200, 250)], "total": 250, "limit": 100, "offset": 200, } mock_get_alerts = mocker.Mock(side_effect=[page_one, page_two, page_three]) results, next_offset = _fetch_paginated_entities( mock_get_alerts, entities_key="alerts", from_time=FIRST_FETCH_TIME, ) assert len(results) == 250 assert next_offset is None assert mock_get_alerts.call_count == 3 assert mock_get_alerts.call_args_list[0].kwargs["limit"] == 100 assert mock_get_alerts.call_args_list[0].kwargs["offset"] == 0 assert mock_get_alerts.call_args_list[2].kwargs["offset"] == 200 def test_fetch_incidents_command_incidents_first_then_alerts(mocker): mocker.patch.object(demisto, "debug") mock_client = mocker.Mock() mock_client.get_incident_timeline.return_value = {"events": []} def make_incident_page(offset: int, count: int, total: int = 152): return { "incidents": [ { "id": f"inc-{index}", "name": f"Inc {index}", "severity": "LOW", "createdAt": TIMESTAMP_T1, } for index in range(offset, offset + count) ], "total": total, "limit": count, "offset": offset, } mock_client.get_incidents.side_effect = [ make_incident_page(0, 50), make_incident_page(50, 50), make_incident_page(100, 50), make_incident_page(150, 2), ] mock_client.get_alerts.return_value = { "alerts": [ { "id": f"alert-{index}", "name": f"Alert {index}", "severity": "LOW", "createdAt": TIMESTAMP_T2, } for index in range(48) ], "total": 200, "limit": 48, "offset": 0, } last_run: dict = {} total_created = 0 for run_index in range(3): last_run, incidents = fetch_incidents_command( client=mock_client, last_run=last_run, fetch_alerts=True, fetch_incidents=True, alert_severities=None, alert_statuses=None, alert_verdicts=None, has_related_incidents=None, incident_severities=None, incident_statuses=None, incident_verdicts=None, first_fetch_time=FIRST_FETCH_TIME, max_fetch=50, ) assert len(incidents) == 50 assert last_run["incidents_offset"] == (run_index + 1) * 50 assert mock_client.get_alerts.call_count == 0 total_created += len(incidents) last_run, incidents = fetch_incidents_command( client=mock_client, last_run=last_run, fetch_alerts=True, fetch_incidents=True, alert_severities=None, alert_statuses=None, alert_verdicts=None, has_related_incidents=None, incident_severities=None, incident_statuses=None, incident_verdicts=None, first_fetch_time=FIRST_FETCH_TIME, max_fetch=50, ) assert len(incidents) == 50 assert "incidents_offset" not in last_run assert mock_client.get_alerts.call_count == 1 assert mock_client.get_alerts.call_args.kwargs["limit"] == 48 total_created += len(incidents) assert total_created == 200 def test_fetch_incidents_command_resumes_alert_pagination(mocker): mocker.patch.object(demisto, "debug") mock_client = mocker.Mock() mock_client.get_incident_timeline.return_value = {"events": []} mock_client.get_incidents.side_effect = [ { "incidents": [ { "id": f"inc-{index}", "name": f"Inc {index}", "severity": "LOW", "createdAt": TIMESTAMP_T1, } for index in range(8) ], "total": 8, "limit": 8, "offset": 0, }, {"incidents": [], "total": 8, "limit": 50, "offset": 0}, {"incidents": [], "total": 8, "limit": 50, "offset": 0}, ] mock_client.get_alerts.side_effect = [ { "alerts": [ { "id": f"alert-{index}", "name": f"Alert {index}", "severity": "LOW", "createdAt": TIMESTAMP_T2, } for index in range(42) ], "total": 100, "limit": 42, "offset": 0, }, { "alerts": [ { "id": f"alert-{index}", "name": f"Alert {index}", "severity": "LOW", "createdAt": TIMESTAMP_T2, } for index in range(42, 92) ], "total": 100, "limit": 50, "offset": 42, }, { "alerts": [ { "id": f"alert-{index}", "name": f"Alert {index}", "severity": "LOW", "createdAt": TIMESTAMP_T2, } for index in range(92, 100) ], "total": 100, "limit": 8, "offset": 92, }, ] first_run, first_incidents = fetch_incidents_command( client=mock_client, last_run={}, fetch_alerts=True, fetch_incidents=True, alert_severities=None, alert_statuses=None, alert_verdicts=None, has_related_incidents=None, incident_severities=None, incident_statuses=None, incident_verdicts=None, first_fetch_time=FIRST_FETCH_TIME, max_fetch=50, ) assert len(first_incidents) == 50 assert first_run["alerts_offset"] == 42 assert first_run["alerts_last_fetch"] == TIMESTAMP_T2 assert "alert-0" in first_run["alerts_last_ids"] second_run, second_incidents = fetch_incidents_command( client=mock_client, last_run=first_run, fetch_alerts=True, fetch_incidents=True, alert_severities=None, alert_statuses=None, alert_verdicts=None, has_related_incidents=None, incident_severities=None, incident_statuses=None, incident_verdicts=None, first_fetch_time=FIRST_FETCH_TIME, max_fetch=50, ) assert len(second_incidents) == 50 assert second_run["alerts_offset"] == 92 assert mock_client.get_alerts.call_args.kwargs["offset"] == 42 third_run, third_incidents = fetch_incidents_command( client=mock_client, last_run=second_run, fetch_alerts=True, fetch_incidents=True, alert_severities=None, alert_statuses=None, alert_verdicts=None, has_related_incidents=None, incident_severities=None, incident_statuses=None, incident_verdicts=None, first_fetch_time=FIRST_FETCH_TIME, max_fetch=50, ) assert len(third_incidents) == 8 assert "alerts_offset" not in third_run assert len(first_incidents) + len(second_incidents) + len(third_incidents) == 108 def test_fetch_incidents_command_no_duplicates_across_pagination_runs(mocker): mocker.patch.object(demisto, "debug") mock_client = mocker.Mock() mock_client.get_incident_timeline.return_value = {"events": []} mock_client.get_incidents.side_effect = [ { "incidents": [ { "id": f"inc-{index}", "name": f"Inc {index}", "severity": "LOW", "createdAt": TIMESTAMP_T1, } for index in range(50) ], "total": 80, "limit": 50, "offset": 0, }, { "incidents": [ { "id": f"inc-{index}", "name": f"Inc {index}", "severity": "LOW", "createdAt": TIMESTAMP_T1, } for index in range(50, 80) ], "total": 80, "limit": 30, "offset": 50, }, {"incidents": [], "total": 80, "limit": 50, "offset": 0}, ] mock_client.get_alerts.return_value = { "alerts": [], "total": 0, "limit": 50, "offset": 0, } first_run, first_incidents = fetch_incidents_command( client=mock_client, last_run={}, fetch_alerts=True, fetch_incidents=True, alert_severities=None, alert_statuses=None, alert_verdicts=None, has_related_incidents=None, incident_severities=None, incident_statuses=None, incident_verdicts=None, first_fetch_time=FIRST_FETCH_TIME, max_fetch=50, ) second_run, second_incidents = fetch_incidents_command( client=mock_client, last_run=first_run, fetch_alerts=True, fetch_incidents=True, alert_severities=None, alert_statuses=None, alert_verdicts=None, has_related_incidents=None, incident_severities=None, incident_statuses=None, incident_verdicts=None, first_fetch_time=FIRST_FETCH_TIME, max_fetch=50, ) third_run, third_incidents = fetch_incidents_command( client=mock_client, last_run=second_run, fetch_alerts=True, fetch_incidents=True, alert_severities=None, alert_statuses=None, alert_verdicts=None, has_related_incidents=None, incident_severities=None, incident_statuses=None, incident_verdicts=None, first_fetch_time=FIRST_FETCH_TIME, max_fetch=50, ) assert len(first_incidents) == 50 assert len(second_incidents) == 30 assert len(third_incidents) == 0 assert {incident["name"] for incident in first_incidents + second_incidents} == {f"Inc {index}" for index in range(80)} assert mock_client.get_incidents.call_count == 3 def test_fetch_incidents_command_uses_stored_cursor_when_present(mocker): mocker.patch.object(demisto, "debug") mock_client = mocker.Mock() mock_client.get_incidents.return_value = { "incidents": [], "total": 0, "limit": 200, "offset": 0, } mock_client.get_alerts.return_value = { "alerts": [], "total": 0, "limit": 200, "offset": 0, } last_run = { "incidents_last_fetch": CURRENT_TIME_CURSOR, "incidents_fetch_config": _build_fetch_filter_fingerprint(None, None, None), } fetch_incidents_command( client=mock_client, last_run=last_run, fetch_alerts=False, fetch_incidents=True, alert_severities=None, alert_statuses=None, alert_verdicts=None, has_related_incidents=None, incident_severities=None, incident_statuses=None, incident_verdicts=None, first_fetch_time=FIRST_FETCH_TIME, ) assert mock_client.get_incidents.call_args.kwargs["from_time"] == "2026-06-04T16:55:00Z" def test_parse_backfill_days_today(mocker): fixed_now = datetime(2026, 6, 2, 15, 30, 0, tzinfo=UTC) mocker.patch("Vega.datetime", wraps=datetime) mocker.patch("Vega.datetime.now", return_value=fixed_now) assert parse_backfill_days(0) == "2026-06-02T00:00:00Z" def test_parse_backfill_days_days(mocker): fixed_now = datetime(2026, 6, 2, 15, 30, 0, tzinfo=UTC) mocker.patch("Vega.datetime", wraps=datetime) mocker.patch("Vega.datetime.now", return_value=fixed_now) assert parse_backfill_days(7) == "2026-05-26T00:00:00Z" def test_parse_backfill_days_defaults(mocker): fixed_now = datetime(2026, 6, 2, 15, 30, 0, tzinfo=UTC) mocker.patch("Vega.datetime", wraps=datetime) mocker.patch("Vega.datetime.now", return_value=fixed_now) assert parse_backfill_days(None) == "2026-05-03T00:00:00Z" def test_filter_alert_statuses_maps_display_and_ignores_invalid(): assert filter_alert_statuses(["OPEN", "IN PROGRESS", "PEER REVIEW", "RESOLVED"]) == [ "OPEN", "IN_PROGRESS", "PEER_REVIEW", "RESOLVED", ] assert filter_alert_statuses(["IN_PROGRESS", "open"]) == ["IN_PROGRESS", "OPEN"] assert filter_alert_statuses(["OPEN", "not-a-status", ""]) == ["OPEN"] assert filter_alert_statuses(["garbage"]) is None assert filter_alert_statuses(None) is None def test_filter_incident_statuses_maps_display_and_ignores_invalid(): assert filter_incident_statuses(["NEW", "ON HOLD", "UNDER REVIEW"]) == [ "NEW", "ON_HOLD", "UNDER_REVIEW", ] assert filter_incident_statuses(["EXTERNAL_ESCALATION", "review recommended"]) == [ "EXTERNAL_ESCALATION", "REVIEW_RECOMMENDED", ] assert filter_incident_statuses(["NEW", "invalid"]) == ["NEW"] assert filter_incident_statuses([]) is None def test_filter_severities_accepts_valid_and_ignores_invalid(): assert filter_alert_severities(["LOW", "HIGH", "critical"]) == [ "LOW", "HIGH", "CRITICAL", ] assert filter_incident_severities(["MEDIUM", "invalid", ""]) == ["MEDIUM"] assert filter_alert_severities(["garbage"]) is None assert filter_incident_severities(None) is None def test_filter_verdicts_accepts_valid_and_ignores_invalid(): assert filter_alert_verdicts(["MALICIOUS", "N/A", "benign"]) == [ "MALICIOUS", "NA", "BENIGN", ] assert filter_incident_verdicts(["SUSPICIOUS", "INCONCLUSIVE", "not-a-verdict"]) == [ "SUSPICIOUS", "INCONCLUSIVE", ] assert filter_alert_verdicts([]) is None def test_resolve_has_related_incidents(): assert resolve_has_related_incidents(["Yes"]) is True assert resolve_has_related_incidents(["No"]) is False assert resolve_has_related_incidents(["Yes", "No"]) is None assert resolve_has_related_incidents([]) is None assert resolve_has_related_incidents(None) is None def test_get_alerts_includes_has_related_incidents_when_set(requests_mock, mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch.object(demisto, "info") requests_mock.post(f"{BASE_URL}/api/v1/login_machine", json=MOCK_JWT_RESPONSE) requests_mock.post( f"{BASE_URL}/api/v1/query", json={"data": {"getAlerts": {"alerts": [], "total": 0}}}, ) client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) client.get_alerts(has_related_incidents=True) request_json = requests_mock.request_history[-1].json() assert request_json["variables"]["hasRelatedIncidents"] is True def test_get_alerts_omits_has_related_incidents_when_unset(requests_mock, mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch.object(demisto, "info") requests_mock.post(f"{BASE_URL}/api/v1/login_machine", json=MOCK_JWT_RESPONSE) requests_mock.post( f"{BASE_URL}/api/v1/query", json={"data": {"getAlerts": {"alerts": [], "total": 0}}}, ) client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) client.get_alerts() request_json = requests_mock.request_history[-1].json() assert "hasRelatedIncidents" not in request_json["variables"] def test_normalize_vega_status_for_display_maps_api_values(): assert _normalize_vega_status_for_display("IN_PROGRESS", "alert") == "IN PROGRESS" assert _normalize_vega_status_for_display("PEER_REVIEW", "alert") == "PEER REVIEW" assert _normalize_vega_status_for_display("OPEN", "alert") == "OPEN" assert _normalize_vega_status_for_display("ON_HOLD", "incident") == "ON HOLD" assert _normalize_vega_status_for_display("EXTERNAL_ESCALATION", "incident") == "EXTERNAL ESCALATION" assert _normalize_vega_status_for_display("IN PROGRESS", "alert") == "IN PROGRESS" def test_format_raw_entity_for_xsoar_normalizes_status_for_dropdown(): alert = {"vegaEntityType": "Vega Alert", "status": "IN_PROGRESS"} _format_raw_entity_for_xsoar(alert) assert alert["status"] == "IN PROGRESS" incident = {"vegaEntityType": "Vega Incident", "status": "UNDER_REVIEW"} _format_raw_entity_for_xsoar(incident) assert incident["status"] == "UNDER REVIEW" def test_validate_backfill_days_rejects_out_of_range(): with pytest.raises(ValueError, match="between 0 and 365"): validate_backfill_days(500) with pytest.raises(ValueError, match="between 0 and 365"): validate_backfill_days(-5) with pytest.raises(ValueError, match="must be an integer"): validate_backfill_days("not-a-number") def test_parse_backfill_days_parses_decimal_string(): assert parse_backfill_days("30.0") == parse_backfill_days(30) def test_parse_backfill_days_defaults_when_none(): result = parse_backfill_days(None) assert result.endswith("T00:00:00Z") parsed = datetime.strptime(result, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC) today_start = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0) assert (today_start - parsed).days == 30 def test_format_bullet_list(): assert _format_bullet_list(["CloudTrail", "VPC Flow Logs"]) == "• CloudTrail\n• VPC Flow Logs" assert _format_bullet_list([]) == [] assert _format_bullet_list(None) is None assert _format_bullet_list("already formatted") == "already formatted" def test_format_recommended_actions_for_grid_empty_shows_placeholder(): assert _format_recommended_actions_for_grid([]) == [{"name": VEGA_NO_RECOMMENDED_ACTIONS_DISPLAY}] assert _format_recommended_actions_for_grid(None) == [{"name": VEGA_NO_RECOMMENDED_ACTIONS_DISPLAY}] def test_format_recommended_actions_for_grid_adds_description_newline(): actions = [ {"name": "Revoke sessions", "description": "Revoke active sessions", "actionKey": "revoke_user_sessions"}, {"name": "Reset password", "description": "Reset the user password\n", "actionKey": "reset_user_password"}, ] formatted = _format_recommended_actions_for_grid(actions) assert formatted[0]["description"] == "Revoke active sessions\n" assert formatted[1]["description"] == "Reset the user password\n" def test_format_raw_entity_for_xsoar_empty_recommended_actions(): incident = { "vegaEntityType": "Vega Incident", "recommendedActions": [], } _format_raw_entity_for_xsoar(incident) assert incident["recommendedActions"] == [{"name": VEGA_NO_RECOMMENDED_ACTIONS_DISPLAY}] def test_format_key_findings_html_dark_theme_layout(): findings = [ "Suspicious activity from 10.0.0.1", "Domain test-observable contacted by host", ] assets = ["10.0.0.1"] observables = ["test-observable"] result = _format_key_findings_html(findings, assets, observables) assert "background:#000000" in result assert "Key findings</div>" in result assert "See Investigation" not in result assert "border-radius:999px" in result assert "10.0.0.1" in result assert "test-observable" in result assert ">1</div>" in result assert ">2</div>" in result assert "border-bottom:1px solid #333333" in result def test_format_key_findings_html_empty_state(): result = _format_key_findings_html([], [], []) assert "No key findings are available" in result assert "background:#000000" in result def test_format_raw_entity_for_xsoar_alert(): alert = { "id": "alert-1", "name": "Test Alert", "vegaEntityType": "Vega Alert", "dataSources": ["CloudTrail", "GuardDuty"], } _format_raw_entity_for_xsoar(alert) assert alert["dataSources"] == "• CloudTrail\n• GuardDuty" assert alert["detectionDescription"] == "N/A" assert alert["detectionQuery"] == "N/A" assert alert["verdictReasoning"] == "N/A" assert "vegaAlertId" not in alert assert set(alert.keys()) == { "id", "name", "vegaEntityType", "dataSources", "detectionDescription", "detectionQuery", "verdictReasoning", } def test_format_raw_entity_for_xsoar_alert_preserves_vega_alert_id(): alert = { "id": "019e1b27-513c-7dd0-a9ca-db2105bdddc4", "vegaAlertId": "VEGA-3409", "vegaEntityType": "Vega Alert", } _format_raw_entity_for_xsoar(alert) assert alert["id"] == "019e1b27-513c-7dd0-a9ca-db2105bdddc4" assert alert["vegaAlertId"] == "VEGA-3409" def test_format_raw_entity_for_xsoar_alert_detection_fields(): alert = { "id": "alert-1", "vegaEntityType": "Vega Alert", "detectionDescription": " ", "detectionQuery": "SELECT * FROM events", } _format_raw_entity_for_xsoar(alert) assert alert["detectionDescription"] == "N/A" assert alert["detectionQuery"] == "```sql\nSELECT * FROM events\n```" def test_format_raw_entity_for_xsoar_alert_empty_detection_fields(): alert = { "id": "alert-1", "vegaEntityType": "Vega Alert", "detectionDescription": None, "detectionQuery": "", } _format_raw_entity_for_xsoar(alert) assert alert["detectionDescription"] == "N/A" assert alert["detectionQuery"] == "N/A" def test_format_mitre_attack(): assert _format_mitre_attack(None) is None assert _format_mitre_attack({}) is None assert ( _format_mitre_attack( { "mitreTactics": ["Discovery"], "mitreTechniques": ["Cloud Infrastructure Discovery"], } ) == "• Discovery\n• Cloud Infrastructure Discovery" ) assert _format_mitre_attack({"mitreTactics": "Discovery", "mitreTechniques": "T1526"}) == "• Discovery\n• T1526" def test_format_raw_entity_for_xsoar_mitre_attack(): alert = { "id": "alert-1", "mitre": {"mitreTactics": ["Discovery"], "mitreTechniques": ["T1526"]}, } _format_raw_entity_for_xsoar(alert) assert alert["vegaMitreAttack"] == "• Discovery\n• T1526" def test_format_mitre_attack_object_items(): mitre = { "mitreTactics": [{"name": "Discovery", "id": "TA0007"}], "mitreTechniques": [{"techniqueName": "Cloud Infrastructure Discovery", "techniqueId": "T1526"}], } assert _format_mitre_attack(mitre) == "• Discovery\n• Cloud Infrastructure Discovery" def test_alert_to_incident_sets_vega_mitre_attack(): alert = { "id": "alert-1", "name": "Test Alert", "severity": "HIGH", "createdAt": TIMESTAMP_T1, "mitre": {"mitreTactics": ["Discovery"], "mitreTechniques": ["T1526"]}, } xsoar_incident = alert_to_incident(alert) raw = json.loads(xsoar_incident["rawJSON"]) assert raw["vegaMitreAttack"] == "• Discovery\n• T1526" assert xsoar_incident["CustomFields"]["vegamitreattack"] == "• Discovery\n• T1526" assert xsoar_incident["CustomFields"]["vegacreatedat"] == TIMESTAMP_T1 def test_alert_to_incident_fetches_alert_events_when_client_provided(mocker): mock_client = mocker.Mock(spec=Client) mock_client.get_alert_events.return_value = { "total": 1, "results": [ { "actor.user.uid": "arn:aws:iam::890123456789:root", "timeframe": "2026-05-12 00:50:00.000", } ], } alert = { "id": "alert-1", "name": "Test Alert", "severity": "HIGH", "createdAt": TIMESTAMP_T1, } xsoar_incident = alert_to_incident(alert, client=mock_client) raw = json.loads(xsoar_incident["rawJSON"]) assert len(raw["alertEvents"]) == 1 assert "Alert Events (1)" in xsoar_incident["CustomFields"]["vegaalertevents"] assert xsoar_incident["CustomFields"]["vegaalerteventsloadedfor"] == "alert-1" assert "_alertEventsCustomFields" not in raw def test_alert_to_incident_skips_alert_events_when_client_fetch_fails(mocker): mocker.patch.object(demisto, "debug") mock_client = mocker.Mock(spec=Client) mock_client.get_alert_events.side_effect = DemistoException("Gateway Timeout") alert = { "id": "alert-1", "name": "Test Alert", "severity": "HIGH", "createdAt": TIMESTAMP_T1, } xsoar_incident = alert_to_incident(alert, client=mock_client) assert xsoar_incident["CustomFields"]["vegaalerteventsloadedfor"] == "alert-1" assert "No alert events found" in xsoar_incident["CustomFields"]["vegaalertevents"] assert json.loads(xsoar_incident["rawJSON"])["alertEvents"] == [] def test_format_raw_entity_for_xsoar_incident(): incident = { "id": "inc-1", "dataSources": ["CloudTrail"], "assets": ["i-12345"], "observables": ["10.0.0.1"], "incidentFindings": ["Instance i-12345 connected to 10.0.0.1"], } _format_raw_entity_for_xsoar(incident) assert incident["dataSources"] == "• CloudTrail" assert incident["assets"] == "• i-12345" assert incident["observables"] == "• 10.0.0.1" assert "vegaIncidentFindings" in incident assert "background:#000000" in incident["vegaIncidentFindings"] assert "i-12345" in incident["vegaIncidentFindings"] assert "10.0.0.1" in incident["vegaIncidentFindings"] def test_alert_to_incident_formats_raw_json(mocker): mocker.patch.object(demisto, "params", return_value={"autoclosure": "false"}) mocker.patch.object(demisto, "integrationInstance", return_value="Vega_instance_1") alert = { "id": "alert-1", "name": "Test Alert", "severity": "HIGH", "createdAt": TIMESTAMP_T1, "dataSources": ["CloudTrail"], } xsoar_incident = alert_to_incident(alert, integration_url="https://api.vega.io") raw = json.loads(xsoar_incident["rawJSON"]) assert raw["dataSources"] == "• CloudTrail" assert raw["vegaEntityType"] == "Vega Alert" assert raw["link"] == "https://app.vega.io/incidents/alerts/investigation/alert-1" assert raw["detectionDescription"] == "N/A" assert raw["detectionQuery"] == "N/A" assert raw["verdictReasoning"] == "N/A" assert "vegaAlertId" not in raw assert set(raw.keys()) == { "id", "name", "severity", "createdAt", "dataSources", "vegaEntityType", "link", "detectionDescription", "detectionQuery", "verdictReasoning", "mirror_instance", "mirror_direction", "mirror_id", } assert raw["mirror_id"] == "alert:alert-1" assert xsoar_incident["dbotMirrorId"] == "alert:alert-1" def test_incident_to_xsoar_incident_formats_raw_json(): incident = { "id": "inc-1", "name": "Test Incident", "severity": "LOW", "createdAt": TIMESTAMP_T1, "assets": ["host-1"], "observables": ["host-1"], "incidentFindings": ["Activity detected on host-1"], } xsoar_incident = incident_to_xsoar_incident(incident) raw = json.loads(xsoar_incident["rawJSON"]) assert raw["assets"] == "• host-1" assert raw["observables"] == "• host-1" assert "vegaIncidentFindings" in raw assert "Activity detected on" in raw["vegaIncidentFindings"] assert "host-1" in raw["vegaIncidentFindings"] assert xsoar_incident["dbotMirrorId"] == "incident:inc-1" assert xsoar_incident["CustomFields"]["vegaincidentfindings"] assert xsoar_incident["CustomFields"]["vegacreatedat"] == TIMESTAMP_T1 assert "link" not in raw def test_is_empty_vega_comment_text(): assert _is_empty_vega_comment_text(None) is True assert _is_empty_vega_comment_text("") is True assert _is_empty_vega_comment_text("[{}]") is True assert _is_empty_vega_comment_text("[]") is True assert _is_empty_vega_comment_text("status to investigation and verdict to benign") is False def test_format_vega_comments_html_filters_empty_comments(): comments = [ { "text": "[{}]", "addedBy": "K3E1sZgbbNR2v3DpC3QCStodL1ay", "addedAt": "2026-06-12T05:01:20.379Z", }, { "text": "status to investigation and verdict to benign", "addedBy": "K3E1sZgbbNR2v3DpC3QCStodL1ay", "addedAt": "2026-06-12T11:27:06Z", }, { "text": "[{}]", "addedBy": "K3E1sZgbbNR2v3DpC3QCStodL1ay", "addedAt": "2026-06-12T05:00:43.95Z", }, ] html = _format_vega_comments_html(comments) assert "status to investigation and verdict to benign" in html assert "[{}]" not in html assert "background:#000000" in html assert "added a comment" in html assert "Unknown" in html assert "2026-06-12T11:27:06Z" in html def test_format_raw_entity_for_xsoar_builds_vega_comments_html(): incident = { "id": "inc-1", "vegaEntityType": "Vega Incident", "comments": [ { "text": "[{}]", "addedBy": "machine-user", "addedAt": "2026-06-12T05:01:20.379Z", }, { "text": "Reviewed in XSOAR", "addedBy": "Analyst One", "addedAt": "2026-06-12T11:27:06Z", }, ], } _format_raw_entity_for_xsoar(incident) assert "vegaComments" in incident assert "Reviewed in XSOAR" in incident["vegaComments"] assert "[{}]" not in incident["vegaComments"] def test_format_raw_entity_for_xsoar_builds_vega_alert_comments_html(): alert = { "id": "alert-1", "vegaEntityType": "Vega Alert", "comments": [ { "text": "[{}]", "addedBy": "machine-user", "addedAt": "2026-06-12T05:01:20.379Z", }, { "text": "Escalated for review", "addedBy": "Analyst Two", "addedAt": "2026-06-12T12:00:00Z", }, ], } _format_raw_entity_for_xsoar(alert) assert "vegaComments" in alert assert "Escalated for review" in alert["vegaComments"] assert "[{}]" not in alert["vegaComments"] def test_format_timeline_events_html_dark_theme_layout(): timeline = [ { "id": "evt-1", "timestamp": "2026-04-28T01:30:00Z", "summary": "SSM enumeration detected.", "entities": [], "dataSources": [{"vendor": "AWS", "displayName": "CloudTrail"}], "alert": { "id": "alert-1", "displayName": "AWS SSM Enumeration", "severity": 3, }, }, { "id": "evt-2", "timestamp": "2026-04-28T02:00:00Z", "summary": "Authorized scanner context.", "entities": [ { "type": "ASSET", "category": "USERNAME", "value": "arn:aws:sts::890123456789:assumed-role/WizAccess-Role/wiz-scanner-session", } ], "dataSources": [{"vendor": "Wiz", "displayName": "Wiz Issues"}], "alert": None, }, ] formatted = _format_timeline_events_html(timeline) assert "background:#000000" in formatted assert "color:#ffffff" in formatted assert "Timeline</div>" in formatted assert "2026-04-28 01:30:00" in formatted assert "AWS SSM Enumeration" in formatted assert "AWS · CloudTrail" in formatted assert "Wiz · Wiz Issues" in formatted assert "Severity: High" in formatted assert "SSM enumeration detected." in formatted assert "arn:aws:sts::890123456789:assumed-role/WizAccess-Role/wiz-scanner-session" in formatted assert formatted.count("align-items:stretch") == 2 assert "border-radius:50%" not in formatted def test_incident_to_xsoar_incident_includes_timeline_events(): timeline = [ { "id": "evt-1", "timestamp": "2026-04-28T01:30:00Z", "summary": "Test event.", "entities": [], "dataSources": [], "alert": None, } ] incident = { "id": "inc-1", "name": "Test Incident", "severity": "LOW", "createdAt": TIMESTAMP_T1, } xsoar_incident = incident_to_xsoar_incident(incident, timeline_events=timeline) raw = json.loads(xsoar_incident["rawJSON"]) assert raw["timelineEvents"] == timeline assert "vegaTimelineEvents" in raw assert xsoar_incident["CustomFields"]["vegatimelineevents"] assert "Test event." in xsoar_incident["CustomFields"]["vegatimelineevents"] def test_fetch_incidents_command_fetches_timeline_details(mocker): mocker.patch.object(demisto, "debug") mock_client = mocker.Mock() mock_client.get_incidents.return_value = { "incidents": [ { "id": "inc-1", "name": "Inc 1", "severity": "LOW", "createdAt": TIMESTAMP_T2, } ], "total": 1, "limit": 200, "offset": 0, } mock_client.get_incident_timeline.return_value = { "events": [ { "id": "evt-1", "timestamp": TIMESTAMP_T2, "summary": "Timeline summary.", "assets": [], "observables": [], "dataSources": [], "alert": None, } ], } mock_client.get_alerts.return_value = { "alerts": [], "total": 0, "limit": 200, "offset": 0, } _, incidents = fetch_incidents_command( client=mock_client, last_run={}, fetch_alerts=False, fetch_incidents=True, alert_severities=None, alert_statuses=None, alert_verdicts=None, has_related_incidents=None, incident_severities=None, incident_statuses=None, incident_verdicts=None, first_fetch_time=FIRST_FETCH_TIME, ) assert len(incidents) == 1 mock_client.get_incident_timeline.assert_called_once_with("inc-1") raw = json.loads(incidents[0]["rawJSON"]) assert raw["timelineEvents"][0]["summary"] == "Timeline summary." def test_format_raw_entity_for_xsoar_prefers_key_findings(): incident = { "incidentFindings": ["List finding"], "keyFindings": ["Detail finding"], "assets": [], "observables": [], } _format_raw_entity_for_xsoar(incident) assert incident["assets"] == "No assets present." assert incident["observables"] == "No observables present." assert "Detail finding" in incident["vegaIncidentFindings"] assert "List finding" not in incident["vegaIncidentFindings"] def test_alert_to_incident_normalizes_api_link(): alert = { "id": "alert-1", "name": "Test Alert", "severity": "HIGH", "createdAt": TIMESTAMP_T1, "link": "https://api.vega.io/incidents/alerts/alert-1", } raw = json.loads(alert_to_incident(alert)["rawJSON"]) assert raw["link"] == "https://app.vega.io/incidents/alerts/alert-1" def test_incident_to_xsoar_incident_normalizes_api_link(): incident_id = "019e1b27-6d49-7ea1-a9d2-f2fe9227738f" incident = { "id": incident_id, "name": "Test Incident", "severity": "LOW", "createdAt": TIMESTAMP_T1, "link": f"https://api.vega.io/incidents/list/{incident_id}", } raw = json.loads(incident_to_xsoar_incident(incident)["rawJSON"]) assert raw["link"] == f"https://app.vega.io/incidents/list/{incident_id}" def test_normalize_verdict_reasoning_null_to_na(): assert _normalize_verdict_reasoning_for_display({"verdictReasoning": None}) == "N/A" assert _normalize_verdict_reasoning_for_display({}) == "N/A" assert _normalize_verdict_reasoning_for_display({"verdictReasoning": " "}) == "N/A" def test_normalize_verdict_reasoning_displays_string(): assert _normalize_verdict_reasoning_for_display({"verdictReasoning": "Confirmed malicious activity"}) == ( "Confirmed malicious activity" ) def test_extract_verdict_reasoning_treats_na_placeholder_as_missing(): assert _extract_verdict_reasoning_from_entity({"verdictReasoning": "N/A"}) is None assert _extract_verdict_reasoning_from_entity({"verdictReasoning": "n/a"}) is None assert ( _extract_verdict_reasoning_from_entity( { "verdictReasoning": "N/A", "userVerdict": {"value": "BENIGN", "reasoning": "Reviewed by analyst"}, } ) is None ) def test_extract_verdict_reasoning_ignores_user_verdict_and_nested_verdict(): assert ( _extract_verdict_reasoning_from_entity({"userVerdict": {"value": "BENIGN", "reasoning": "Reviewed by analyst"}}) is None ) assert ( _extract_verdict_reasoning_from_entity( { "verdict": { "value": "SUSPICIOUS", "reasoning": "Multiple failed logins observed", } } ) is None ) assert _extract_verdict_reasoning_from_entity({"incidentSummary": "Incident summary text"}) is None def test_normalize_verdict_reasoning_from_nested_verdict_dict(): raw = { "verdict": { "value": "SUSPICIOUS", "reasoning": "Multiple failed logins observed", } } assert _normalize_verdict_reasoning_for_display(raw) == "N/A" def test_parse_alert_events_results_handles_json_string(): payload = json.dumps( [ { "actor": {"user": {"uid": "arn:aws:iam::123:root"}}, "timeframe": "2026-05-12 00:40:00.000", "event_count": 23, } ] ) parsed = _parse_alert_events_results(payload) assert len(parsed) == 1 assert parsed[0]["event_count"] == 23 def test_event_has_bad_alert_events_shape_detects_cid_or_eid(): assert _event_has_bad_alert_events_shape({"cid": "12345678901234567890123456789012", "eid": "118"}) is True assert _event_has_bad_alert_events_shape({"cid": "12345678901234567890123456789012"}) is True assert _event_has_bad_alert_events_shape({"eid": "118"}) is True def test_event_has_bad_alert_events_shape_allows_normal_rows(): summary_row = { "actor.user.uid": "arn:aws:iam::890123456789:root", "event_count": "23", "unique_events_count": "6", "timeframe": "2026-05-12 00:40:00.000", } parse_field_row = { "catalog": "amazoneksaudit", "timestamp": "2026-03-25 17:26:18.000", "fields": json.dumps({"operation": "create"}), } assert _event_has_bad_alert_events_shape(summary_row) is False assert _event_has_bad_alert_events_shape(parse_field_row) is False assert _events_have_bad_alert_events_shape([summary_row, parse_field_row]) is False def test_events_have_bad_alert_events_shape_when_any_row_has_cid(): vendor_row = { "cid": "12345678901234567890123456789012", "EventType": "Event_ExternalApiEvent", } good_row = {"event_count": "23", "unique_events_count": "6"} assert _events_have_bad_alert_events_shape([vendor_row]) is True assert _events_have_bad_alert_events_shape([good_row, vendor_row]) is True assert _events_have_bad_alert_events_shape([good_row]) is False def test_format_alert_events_markdown_table_layout(): actor_arn = "arn:aws:iam::890123456789:root" alert_events = [ { "actor.user.uid": actor_arn, "event_count": "23", "regions_count": "6", "timeframe": "2026-05-12 00:40:00.000", "unique_events": "[DescribeInstances DescribeVolumes]", "unique_events_count": "6", } ] formatted = _format_alert_events_markdown(alert_events, total=16, offset=0, page_size=50) assert "Alert Events (16)" in formatted assert "actor.user.uid" in formatted assert "timeframe" in formatted assert "event_count" in formatted assert "unique_events_count" in formatted assert "regions_count" in formatted assert actor_arn in formatted assert "<div" not in formatted def test_format_alert_events_markdown_handles_dynamic_eks_shape(): fields_payload = { "cluster": {"name": "eks-prod-cluster"}, "operation": "create", "actor": { "user": { "uid": "aws-iam-authenticator:890123456789:AIDASDRANJTZJUR47VREC", "name": "arn:aws:iam::890123456789:user/james.collins", } }, "request": {"uri": "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings"}, "status_code": "201", } alert_events = [ { "catalog": "amazoneksaudit", "class": "Network Activity", "data_source": "amazon_eks_events", "fields": json.dumps(fields_payload), "index_timestamp": "2026-03-25 14:12:43.000", "raw": json.dumps({"auditID": "72cf9493-079d-4d73-872b-e1f4f0a099a8", "verb": "create"}), "source": "EKS", "storage": "AWS S3", "timestamp": "2026-03-25 17:26:18.000", } ] formatted = _format_alert_events_markdown(alert_events, total=1) assert "timestamp" in formatted assert "source" in formatted assert "catalog" in formatted assert "actor.user.uid" in formatted assert "operation" in formatted assert "request.uri" in formatted assert "eks-prod-cluster" in formatted assert "aws-iam-authenticator:890123456789:AIDASDRANJTZJUR47VREC" in formatted assert "raw" in formatted def test_expand_flat_raw_fields_expands_dotted_and_array_keys(): expanded = _expand_flat_raw_fields( { "date_year": "2026", "vendorInformation.provider": "ASC", "securityResources{}.resourceType": "attacked", "userStates{}.logonIp": "10.0.0.1", "userStates{}.userPrincipalName": "user@example.com", } ) assert expanded["date_year"] == "2026" assert expanded["vendorInformation"] == {"provider": "ASC"} assert expanded["securityResources"] == [{"resourceType": "attacked"}] assert expanded["userStates"] == [{"logonIp": "10.0.0.1", "userPrincipalName": "user@example.com"}] def test_promote_raw_into_fields_keeps_schema_and_promotes_splunk_raw(): fields = { "app_uid": None, "http_response": {"code": None}, "request": {"uri": None}, "auth_type": None, "risk_score": "medium", "_raw": json.dumps( { "date_year": "2026", "createdDateTime": "2026-07-21T14:23:19.063Z", "vendorInformation.provider": "ASC", "securityResources{}.resourceType": "attacked", "userStates{}.logonIp": "10.0.0.1", "userStates{}.userPrincipalName": "user@example.com", "splunk_server": "idx-example.splunkcloud.com", "risk_score": "should-not-overwrite", } ), } promoted = _promote_raw_into_alert_event_fields(fields) assert promoted["risk_score"] == "medium" assert promoted["app_uid"] is None assert promoted["date_year"] == "2026" assert promoted["createdDateTime"] == "2026-07-21T14:23:19.063Z" assert promoted["vendorInformation"] == {"provider": "ASC"} assert promoted["securityResources"] == [{"resourceType": "attacked"}] assert promoted["userStates"] == [{"logonIp": "10.0.0.1", "userPrincipalName": "user@example.com"}] assert promoted["splunk_server"] == "idx-example.splunkcloud.com" assert isinstance(promoted["_raw"], str) assert "date_year" in promoted["_raw"] def test_enrich_alert_event_promotes_nested_eks_raw_object(): raw_event = { "_index_timestamp": 1774447926000, "account_id": "890123456789", "auditID": "c6dcb49b-90fd-4ec8-ac88-a58990cf7dc4", "cluster_name": "eks-prod-cluster", "verb": "get", "user": { "uid": "8a75b43c-5278-4abb-91e1-eea07ad04ea6", "username": "system:serviceaccount:stratus-red-team-np-name-fatnmkvw:stratus-red-team-np-sa", }, "sourceIPs": ["10.0.0.1"], } fields_payload = { "container": {"uid": None, "name": None}, "cluster": {"name": "eks-prod-cluster"}, "request": { "data": None, "uri": "/api/v1/nodes/ip-192-168-20-125.ec2.internal/proxy/runningpods/", }, "status_code": "200", "operation": "get", "account": {"uid": "890123456789"}, "_raw": raw_event, } event = { "catalog": "amazoneksaudit", "class": "Network Activity", "data_source": "amazon_eks_events", "fields": json.dumps(fields_payload), "raw": json.dumps(raw_event), "source": "EKS", "storage": "AWS S3", "timestamp": "2026-03-24 17:26:20.000", } enriched = _enrich_alert_event(event) fields = enriched["fields"] assert isinstance(fields, dict) assert fields["cluster"] == {"name": "eks-prod-cluster"} assert fields["status_code"] == "200" assert fields["auditID"] == "c6dcb49b-90fd-4ec8-ac88-a58990cf7dc4" assert fields["verb"] == "get" assert fields["account_id"] == "890123456789" assert fields["user"]["username"].startswith("system:serviceaccount:") assert fields["sourceIPs"] == ["10.0.0.1"] assert fields["_raw"] == raw_event assert enriched["raw"] == json.dumps(raw_event) def test_enrich_alert_event_noop_without_fields_or_raw(): event = { "actor.user.uid": "arn:aws:iam::890123456789:root", "event_count": "23", "timeframe": "2026-05-12 00:40:00.000", } assert _enrich_alert_event(event) == event def test_enrich_alert_events_leaves_summary_rows_unchanged(): events = [ { "actor.user.uid": "arn:aws:iam::890123456789:root", "event_count": "23", "unique_events_count": "6", } ] assert _enrich_alert_events(events) == events def test_fetch_alert_events_page_enriches_fields_from_raw(mocker): mock_client = mocker.Mock(spec=Client) mock_client.get_alert_events.return_value = { "total": 1, "results": [ { "catalog": "splunk_cloud__sandbox", "data_source": "microsoft_graph_events", "fields": json.dumps( { "app_uid": None, "risk_score": "medium", "_raw": { "date_year": "2026", "vendorInformation.provider": "ASC", "securityResources{}.resourceType": "attacked", }, } ), "raw": '{"date_year":"2026"}', "source": "Graph", "timestamp": "2026-07-21 14:23:55.887", } ], } events, total = fetch_alert_events_page(mock_client, "alert-1") assert total == 1 assert events[0]["fields"]["date_year"] == "2026" assert events[0]["fields"]["vendorInformation"] == {"provider": "ASC"} assert events[0]["fields"]["securityResources"] == [{"resourceType": "attacked"}] assert events[0]["fields"]["risk_score"] == "medium" assert events[0]["fields"]["app_uid"] is None def test_fetch_alert_events_command_outputs_enriched_events(mocker): mocker.patch( "Vega.load_current_incident", return_value={"CustomFields": {"vegaalertid": "alert-1"}}, ) mock_client = mocker.Mock(spec=Client) mock_client.get_alert_events.return_value = { "total": 1, "results": [ { "catalog": "amazoneksaudit", "fields": json.dumps( { "operation": "get", "_raw": {"auditID": "abc-123", "verb": "get"}, } ), "source": "EKS", "timestamp": "2026-03-24 17:26:20.000", } ], } result = fetch_alert_events_command(mock_client, {"alert_id": "alert-1"}) assert result.outputs["Count"] == 1 assert result.outputs["Events"][0]["fields"]["auditID"] == "abc-123" assert result.outputs["Events"][0]["fields"]["operation"] == "get" assert "auditID" in result.readable_output or "operation" in result.readable_output def test_alert_to_incident_stores_enriched_alert_events(mocker): mock_client = mocker.Mock(spec=Client) mock_client.get_alert_events.return_value = { "total": 1, "results": [ { "catalog": "splunk_cloud__sandbox", "fields": json.dumps( { "risk_score": "medium", "_raw": '{"createdDateTime":"2026-07-21T14:23:19.063Z","vendorInformation.provider":"ASC"}', } ), "source": "Graph", } ], } alert = { "id": "alert-1", "name": "Test Alert", "severity": "HIGH", "createdAt": TIMESTAMP_T1, } xsoar_incident = alert_to_incident(alert, client=mock_client) raw = json.loads(xsoar_incident["rawJSON"]) fields = raw["alertEvents"][0]["fields"] assert fields["createdDateTime"] == "2026-07-21T14:23:19.063Z" assert fields["vendorInformation"] == {"provider": "ASC"} assert fields["risk_score"] == "medium" def test_build_alert_events_custom_fields(): fields = build_alert_events_custom_fields("alert-1", "### Alert Events (16)", 16, offset=50) assert fields["vegaalerteventsloadedfor"] == "alert-1" assert fields["vegaalertevents"] == "### Alert Events (16)" assert fields["vegaalerteventstotal"] == 16 assert fields["vegaalerteventsoffset"] == 50 def test_load_current_incident_returns_incident_context(mocker): mocker.patch( "Vega.demisto.incident", return_value={ "id": "123", "type": "Vega Alert", "CustomFields": {"vegaalertid": "alert-from-context"}, }, ) incident = load_current_incident() assert incident["CustomFields"]["vegaalertid"] == "alert-from-context" def test_load_current_incident_handles_demisto_incident_failure(mocker): mocker.patch( "Vega.demisto.incident", side_effect=TypeError("'NoneType' object is not subscriptable"), ) mocker.patch("Vega.demisto.incidents", return_value=[]) mocker.patch.object(demisto, "debug") incident = load_current_incident() assert incident == {} def test_resolve_alert_id_from_incident_uses_raw_json(): incident = { "type": "Vega Alert", "CustomFields": {}, "rawJSON": json.dumps({"id": "alert-raw", "vegaEntityType": "Vega Alert"}), } assert resolve_alert_id_from_incident({}, incident) == "alert-raw" def test_resolve_alert_id_from_incident_uses_alertid_custom_field(): incident = { "type": "Vega Alert", "CustomFields": { "alertid": "019e1b27-513c-7dd0-a9ca-db2105bdddc4", "vegaalertid": "VEGA-3409", }, "rawJSON": json.dumps({"id": "fallback-id", "vegaEntityType": "Vega Alert"}), } assert resolve_alert_id_from_incident({}, incident) == "019e1b27-513c-7dd0-a9ca-db2105bdddc4" def test_resolve_alert_id_from_incident_ignores_display_vegaalertid(): incident = { "type": "Vega Alert", "CustomFields": {"vegaalertid": "VEGA-3409"}, "rawJSON": json.dumps( { "id": "019e1b27-513c-7dd0-a9ca-db2105bdddc4", "vegaAlertId": "VEGA-3409", "vegaEntityType": "Vega Alert", } ), } assert resolve_alert_id_from_incident({}, incident) == "019e1b27-513c-7dd0-a9ca-db2105bdddc4" def test_build_vega_alert_custom_fields_sets_mitre_attack_and_alert_id(): fields = _build_vega_alert_custom_fields({"id": "alert-1", "vegaMitreAttack": "T1059"}) assert fields["alertid"] == "alert-1" assert "vegaalertid" not in fields assert fields["vegamitreattack"] == "T1059" assert fields[VEGA_NEW_COMMENT_FIELD] == VEGA_NEW_COMMENT_LAYOUT_DEFAULT def test_build_vega_incident_custom_fields_sets_layout_default_comment(): fields = _build_vega_incident_custom_fields({"id": "inc-1"}) assert fields["vegaincidentid"] == "inc-1" assert fields[VEGA_NEW_COMMENT_FIELD] == VEGA_NEW_COMMENT_LAYOUT_DEFAULT def test_outgoing_mirror_comment_value_skips_layout_default(): assert _outgoing_mirror_comment_value(VEGA_NEW_COMMENT_LAYOUT_DEFAULT) is None assert _outgoing_mirror_comment_value("Reviewed in XSOAR") == "Reviewed in XSOAR" def test_fetch_alert_events_page(mocker): mock_client = mocker.Mock(spec=Client) mock_client.get_alert_events.return_value = { "total": 2, "limit": 50, "offset": 0, "results": [ { "timestamp": "2026-05-12 00:40:00.000", "source": "AWS CloudTrail", "catalog": "awscloudtrail", }, { "timestamp": "2026-05-12 00:50:00.000", "source": "AWS CloudTrail", "catalog": "awscloudtrail", }, ], } events, total = fetch_alert_events_page(mock_client, "alert-1", limit=50, offset=0) assert total == 2 assert len(events) == 2 mock_client.get_alert_events.assert_called_once_with("alert-1", limit=50, offset=0) def test_fetch_alert_events_for_ingest_returns_not_available_for_bad_shape(mocker): mock_client = mocker.Mock(spec=Client) mock_client.get_alert_events.return_value = { "total": 1, "results": [{"cid": "123", "eid": "118", "Name": "Access from IP with bad reputation"}], } events, custom_fields = _fetch_alert_events_for_ingest(mock_client, "alert-1") assert events == [] assert "No alert events found" in custom_fields["vegaalertevents"] assert custom_fields["vegaalerteventsloadedfor"] == "alert-1" def test_alert_events_command_results_use_markdown_readable_output(): result = _alert_events_command_results("### Alert Events (1)\n| actor.user.uid |", {"AlertId": "alert-1"}) entry = result.to_context() assert entry["HumanReadable"] == "### Alert Events (1)\n| actor.user.uid |" assert "<div" not in str(entry.get("HumanReadable", "")) def test_fetch_alert_events_command_fetches_all_and_slices_page(mocker): mocker.patch( "Vega.load_current_incident", return_value={"CustomFields": {"vegaalertid": "alert-1"}}, ) mock_client = mocker.Mock(spec=Client) alert_events_page_responses = [ { "total": 3, "results": [ { "actor.user.uid": "arn:aws:iam::890123456789:root", "event_count": "1", "timeframe": "2026-05-12 00:40:00.000", "unique_events_count": "1", }, { "actor.user.uid": "arn:aws:iam::890123456789:root", "event_count": "2", "timeframe": "2026-05-12 00:50:00.000", "unique_events_count": "2", }, ], }, { "total": 3, "results": [ { "actor.user.uid": "arn:aws:iam::890123456789:root", "event_count": "3", "timeframe": "2026-05-12 01:00:00.000", "unique_events_count": "3", } ], }, ] mock_client.get_alert_events.side_effect = alert_events_page_responses * 2 first_page = fetch_alert_events_command( mock_client, {"alert_id": "alert-1", "limit": "2", "offset": "0"}, ) second_page = fetch_alert_events_command( mock_client, {"alert_id": "alert-1", "limit": "2", "offset": "2"}, ) assert first_page.outputs["Total"] == 3 assert first_page.outputs["Count"] == 2 assert first_page.outputs["Offset"] == 0 assert first_page.outputs["HasAlertEvents"] is True assert second_page.outputs["Count"] == 1 assert second_page.outputs["Offset"] == 2 assert mock_client.get_alert_events.call_count == 4 def test_fetch_alert_events_command_returns_not_available_for_vendor_parse_fields( mocker, ): mocker.patch( "Vega.load_current_incident", return_value={"CustomFields": {"vegaalertid": "alert-1"}}, ) mock_client = mocker.Mock(spec=Client) mock_client.get_alert_events.return_value = { "total": 1, "results": [ { "cid": "12345678901234567890123456789012", "eid": "118", "Name": "Access from IP with bad reputation", "EventType": "Event_ExternalApiEvent", "ExternalApiType": "Event_IdpDetectionSummaryEvent", "MitreAttack": [{"Tactic": "Initial Access", "TechniqueID": "T1078"}], "SourceVendors": "CrowdStrike", "SourceProducts": "Falcon Identity Protection", "timestamp": 1774165347000, } ], } result = fetch_alert_events_command(mock_client, {"alert_id": "alert-1"}) assert result.readable_output == ALERT_EVENTS_NOT_AVAILABLE_MARKDOWN assert result.outputs["Total"] == 0 assert result.outputs["Count"] == 0 assert result.outputs["HasAlertEvents"] is False assert "does not have alert events" not in result.outputs["CustomFields"]["vegaalertevents"] assert "No alert events found" in result.outputs["CustomFields"]["vegaalertevents"] mock_client.get_alert_events.assert_called_once() def test_set_detections_state_command(mocker): mock_client = mocker.Mock(spec=Client) mock_client.set_detections_state.return_value = {"ids": ["det-1", "det-2"]} result = set_detections_state_command( mock_client, {"ids": ["det-1", "det-2"], "state": "ENABLED"}, ) mock_client.set_detections_state.assert_called_once_with(["det-1", "det-2"], "ENABLED") assert result.outputs["State"] == "ENABLED" assert result.outputs["IDs"] == ["det-1", "det-2"] assert result.outputs["Count"] == 2 assert "Updated detection state to ENABLED" in result.readable_output def test_set_detections_state_command_requires_ids(mocker): mock_client = mocker.Mock(spec=Client) with pytest.raises(DemistoException, match="ids is required"): set_detections_state_command(mock_client, {"state": "ENABLED"}) def test_set_detections_state_command_requires_valid_state(mocker): mock_client = mocker.Mock(spec=Client) with pytest.raises(DemistoException, match="state must be one of"): set_detections_state_command(mock_client, {"ids": ["det-1"], "state": "INVALID"}) def test_set_detections_state_command_test_mode(mocker): mock_client = mocker.Mock(spec=Client) mock_client.set_detections_state.return_value = {"ids": ["det-1"]} result = set_detections_state_command(mock_client, {"ids": ["det-1"], "state": "TEST_MODE"}) mock_client.set_detections_state.assert_called_once_with(["det-1"], "TEST_MODE") assert result.outputs["State"] == "TEST_MODE" def test_update_detections_command_single_id(mocker): mock_client = mocker.Mock(spec=Client) mock_client.update_detections.return_value = { "results": [ { "status": "VALID", "name": "Detection 1", "detection": { "id": "det-1", "name": "Detection 1", "severity": "HIGH", "status": "VISIBLE", }, } ], "summary": {"requested": 1, "valid": 1, "invalid": 0, "committed": True}, } result = update_detections_command( mock_client, {"detection_id": "det-1", "severity": "HIGH", "status": "VISIBLE"}, ) mock_client.update_detections.assert_called_once_with([{"detectionId": "det-1", "severity": "HIGH", "status": "VISIBLE"}]) assert result.outputs["ID"] == "det-1" assert result.outputs["Severity"] == "HIGH" assert result.outputs["Status"] == "VISIBLE" assert "Updated Vega Detections" in result.readable_output def test_update_detections_command_multiple_ids(mocker): mock_client = mocker.Mock(spec=Client) mock_client.update_detections.return_value = { "results": [ { "status": "VALID", "name": "Detection 1", "detection": { "id": "det-1", "name": "Detection 1", "severity": "LOW", "status": "HIDDEN", }, }, { "status": "VALID", "name": "Detection 2", "detection": { "id": "det-2", "name": "Detection 2", "severity": "LOW", "status": "HIDDEN", }, }, ], "summary": {"requested": 2, "valid": 2, "invalid": 0, "committed": True}, } result = update_detections_command( mock_client, {"detection_id": ["det-1", "det-2"], "severity": "low", "status": "hidden"}, ) mock_client.update_detections.assert_called_once_with( [ {"detectionId": "det-1", "severity": "LOW", "status": "HIDDEN"}, {"detectionId": "det-2", "severity": "LOW", "status": "HIDDEN"}, ] ) assert result.outputs[0]["ID"] == "det-1" assert result.outputs[1]["ID"] == "det-2" def test_update_detections_command_requires_detection_id(mocker): mock_client = mocker.Mock(spec=Client) with pytest.raises(DemistoException, match="detection_id is required"): update_detections_command(mock_client, {"severity": "HIGH"}) def test_update_detections_command_requires_update_fields(mocker): mock_client = mocker.Mock(spec=Client) with pytest.raises(DemistoException, match="At least one of severity, status, state, or tags"): update_detections_command(mock_client, {"detection_id": "det-1"}) def test_update_detections_command_with_state_and_tags(mocker): mock_client = mocker.Mock(spec=Client) mock_client.update_detections.return_value = { "results": [ { "status": "VALID", "name": "Detection 1", "detection": { "id": "det-1", "name": "Detection 1", "severity": "HIGH", "status": "VISIBLE", "state": "ENABLED", "tags": ["tag-a", "tag-b"], }, } ], "summary": {"requested": 1, "valid": 1, "invalid": 0, "committed": True}, } result = update_detections_command( mock_client, {"detection_id": "det-1", "state": "enabled", "tags": ["tag-a", "tag-b"]}, ) mock_client.update_detections.assert_called_once_with( [{"detectionId": "det-1", "state": "ENABLED", "tags": ["tag-a", "tag-b"]}] ) assert result.outputs["State"] == "ENABLED" assert result.outputs["Tags"] == ["tag-a", "tag-b"] def test_update_detections_command_invalid_state(mocker): mock_client = mocker.Mock(spec=Client) with pytest.raises(DemistoException, match="state must be one of"): update_detections_command(mock_client, {"detection_id": "det-1", "state": "INVALID"}) def test_update_detections_command_raises_on_api_errors(mocker): mock_client = mocker.Mock(spec=Client) mock_client.update_detections.return_value = { "results": [ { "status": "INVALID", "name": "Detection 1", "errors": [ { "code": "INVALID_VALUE", "message": "Invalid severity", "field": "severity", } ], } ], "summary": {"requested": 1, "valid": 0, "invalid": 1, "committed": False}, } with pytest.raises(DemistoException, match="Vega API error updating detections"): update_detections_command(mock_client, {"detection_id": "det-1", "severity": "HIGH"}) def test_graphql_request_retries_on_graphql_rate_limit(mocker): mocker.patch.object(demisto, "debug") sleep_mock = mocker.patch("Vega.time.sleep") client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) mocker.patch.object(client, "_authenticate", return_value="jwt-token") rate_limited_response = { "errors": [ { "message": "Rate limit exceeded. Please retry after a brief wait.", "extensions": { "code": "TooManyRequests", "error_code_name": "REQUEST_RATE_LIMITED", "retryAfter": 3, }, } ], "data": None, } success_response = {"data": {"getAlerts": {"alerts": [], "total": 0}}} http_mock = mocker.patch.object( client, "_http_request", side_effect=[rate_limited_response, rate_limited_response, success_response], ) response = client._graphql_request("query { getAlerts { alerts { id } } }") assert response == success_response assert http_mock.call_count == 3 assert sleep_mock.call_args_list[0].args[0] == 2 assert sleep_mock.call_args_list[1].args[0] == 4 assert client._rate_limit_wait_seconds == RATE_LIMIT_INITIAL_WAIT_SECONDS def test_client_http_request_retries_on_429(mocker): mocker.patch.object(demisto, "debug") sleep_mock = mocker.patch("Vega.time.sleep") client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) rate_limited = DemistoException("Too Many Requests") rate_limited.res = mocker.Mock(status_code=429) success_response = {"data": {"getAlertsEvents": {"total": 0, "results": []}}} super_mock = mocker.patch( "Vega.BaseClient._http_request", side_effect=[rate_limited, rate_limited, success_response], ) response = client._http_request(method="POST", url_suffix="query", resp_type="json") assert response == success_response assert super_mock.call_count == 3 assert sleep_mock.call_args_list[0].args[0] == 2 assert sleep_mock.call_args_list[1].args[0] == 4 def test_client_http_request_retries_on_504(mocker): mocker.patch.object(demisto, "debug") sleep_mock = mocker.patch("Vega.time.sleep") client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) gateway_timeout = DemistoException("Gateway Timeout") gateway_timeout.res = mocker.Mock(status_code=504) success_response = {"data": {"getAlerts": {"alerts": [], "total": 0}}} super_mock = mocker.patch( "Vega.BaseClient._http_request", side_effect=[gateway_timeout, gateway_timeout, success_response], ) response = client._http_request(method="POST", url_suffix="query", resp_type="json") assert response == success_response assert super_mock.call_count == 3 assert sleep_mock.call_args_list[0].args[0] == 2 assert sleep_mock.call_args_list[1].args[0] == 4 def test_is_retryable_http_error_detects_gateway_timeout(): exc = DemistoException("Gateway Timeout") exc.res = type("Response", (), {"status_code": 504})() assert _is_retryable_http_error(exc) is True def test_fetch_incidents_command_skips_alerts_on_transient_error(mocker): mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "error") mock_client = mocker.Mock(spec=Client) gateway_timeout = DemistoException("Gateway Timeout") gateway_timeout.res = mocker.Mock(status_code=504) mock_client.get_alerts.side_effect = gateway_timeout mock_client.get_incidents.return_value = { "incidents": [ { "id": "inc-1", "name": "Incident 1", "severity": "HIGH", "createdAt": TIMESTAMP_T1, } ], "total": 1, "limit": 100, "offset": 0, } next_run, incidents = fetch_incidents_command( client=mock_client, last_run={}, fetch_alerts=True, fetch_incidents=True, alert_severities=None, alert_statuses=None, alert_verdicts=None, has_related_incidents=None, incident_severities=None, incident_statuses=None, incident_verdicts=None, first_fetch_time=FIRST_FETCH_TIME, ) assert len(incidents) == 1 assert incidents[0]["type"] == "Vega Incident" assert "alerts_last_fetch" not in next_run assert "incidents_last_fetch" in next_run demisto.error.assert_called_once() def test_build_effective_incident_update_args_no_args_uses_custom_fields(): incident = { "CustomFields": { VEGA_INCIDENT_STATUS_FIELD: "INVESTIGATING", "vegaverdict": "BENIGN", } } effective_args = _build_effective_incident_update_args({}, incident) assert effective_args["status"] == "INVESTIGATING" assert effective_args["verdict"] == "BENIGN" def test_update_incident_command_no_args_uses_layout_fields(mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch( "Vega.load_current_incident", return_value={ "type": "Vega Incident", "CustomFields": { "vegaincidentid": "inc-1", VEGA_INCIDENT_STATUS_FIELD: "UNDER REVIEW", "vegaverdict": "SUSPICIOUS", }, }, ) mock_client = mocker.Mock(spec=Client) mock_client.update_incidents.return_value = { "incidents": [{"incidentId": "inc-1", "status": "UNDER_REVIEW", "verdict": "SUSPICIOUS"}] } mock_client.get_incident_by_id.return_value = { "id": "inc-1", "status": "UNDER_REVIEW", "verdict": "SUSPICIOUS", } update_incident_command(mock_client, {}) mock_client.update_incidents.assert_called_once_with( { "incidentIds": ["inc-1"], "status": "UNDER_REVIEW", "verdict": {"value": "SUSPICIOUS", "reasoning": ""}, } ) def test_update_alert_command_status_only_does_not_send_verdict(mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch( "Vega.load_current_incident", return_value={"CustomFields": {"vegaverdict": "NA", "vegastatus": "OPEN"}}, ) mock_client = mocker.Mock(spec=Client) mock_client.update_alerts.return_value = {"alerts": [{"id": "alert-1", "status": "IN_PROGRESS", "verdict": "BENIGN"}]} update_alert_command(mock_client, {"alert_ids": "alert-1", "status": "IN PROGRESS"}) mock_client.update_alerts.assert_called_once_with({"alertIds": ["alert-1"], "status": "IN_PROGRESS"}) def test_build_effective_alert_update_args_field_change_updates_status_only(): effective_args = _build_effective_alert_update_args( {"old": "OPEN", "new": "IN PROGRESS"}, {"CustomFields": {"vegaverdict": "NA"}}, ) assert effective_args["status"] == "IN PROGRESS" assert "verdict" not in effective_args def test_build_effective_alert_update_args_field_change_updates_verdict_only(): effective_args = _build_effective_alert_update_args( {"old": "NA", "new": "BENIGN"}, {"CustomFields": {"vegastatus": "OPEN"}}, ) assert effective_args["verdict"] == "BENIGN" assert "status" not in effective_args def test_build_effective_alert_update_args_field_change_updates_severity_only(): effective_args = _build_effective_alert_update_args( {"old": "LOW", "new": "HIGH"}, {"CustomFields": {"vegastatus": "OPEN", "vegaverdict": "NA"}}, ) assert effective_args["severity"] == "HIGH" assert "status" not in effective_args assert "verdict" not in effective_args def test_build_effective_alert_update_args_field_change_updates_verdict_reasoning_only(): effective_args = _build_effective_alert_update_args( {"old": "Old reasoning", "new": "Confirmed malicious activity"}, {"CustomFields": {"vegastatus": "OPEN", "vegaverdict": "MALICIOUS"}}, ) assert effective_args["verdict_reasoning"] == "Confirmed malicious activity" assert "status" not in effective_args assert "verdict" not in effective_args def test_update_alert_command_updates_multiple_alerts(mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch("Vega.load_current_incident", return_value={}) mock_client = mocker.Mock(spec=Client) mock_client.update_alerts.return_value = { "alerts": [ {"id": "alert-1", "status": "RESOLVED", "verdict": "MALICIOUS"}, {"id": "alert-2", "status": "RESOLVED", "verdict": "MALICIOUS"}, ] } mock_client.get_alert_by_id.return_value = {} result = update_alert_command( mock_client, { "alert_ids": ["alert-1", "alert-2"], "status": "RESOLVED", "verdict": "MALICIOUS", }, ) mock_client.update_alerts.assert_called_once_with( { "alertIds": ["alert-1", "alert-2"], "status": "RESOLVED", "verdict": "MALICIOUS", } ) assert result.outputs[0]["id"] == "alert-1" assert result.outputs[1]["id"] == "alert-2" assert "Updated Vega Alerts" in result.readable_output def test_update_alert_command_accepts_comma_separated_alert_ids(mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch("Vega.load_current_incident", return_value={}) mock_client = mocker.Mock(spec=Client) mock_client.update_alerts.return_value = { "alerts": [ {"id": "alert-1", "status": "RESOLVED", "verdict": "MALICIOUS"}, {"id": "alert-2", "status": "RESOLVED", "verdict": "MALICIOUS"}, ] } update_alert_command( mock_client, {"alert_ids": "alert-1,alert-2", "status": "RESOLVED", "verdict": "MALICIOUS"}, ) mock_client.update_alerts.assert_called_once_with( { "alertIds": ["alert-1", "alert-2"], "status": "RESOLVED", "verdict": "MALICIOUS", } ) def test_update_alert_command_accepts_alert_id_alias(mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch("Vega.load_current_incident", return_value={}) mock_client = mocker.Mock(spec=Client) mock_client.update_alerts.return_value = { "alerts": [ {"id": "alert-1", "status": "OPEN", "verdict": "NA"}, {"id": "alert-2", "status": "OPEN", "verdict": "NA"}, ] } update_alert_command(mock_client, {"alert_id": ["alert-1", "alert-2"], "status": "OPEN"}) mock_client.update_alerts.assert_called_once_with({"alertIds": ["alert-1", "alert-2"], "status": "OPEN"}) def test_update_incident_command_updates_multiple_incidents(mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch("Vega.load_current_incident", return_value={}) mock_client = mocker.Mock(spec=Client) mock_client.update_incidents.return_value = { "incidents": [ {"incidentId": "inc-1", "status": "RESOLVED", "verdict": "MALICIOUS"}, {"incidentId": "inc-2", "status": "RESOLVED", "verdict": "MALICIOUS"}, ] } result = update_incident_command( mock_client, { "incident_ids": ["inc-1", "inc-2"], "status": "RESOLVED", "verdict": "MALICIOUS", }, ) mock_client.update_incidents.assert_called_once_with( { "incidentIds": ["inc-1", "inc-2"], "status": "RESOLVED", "verdict": {"value": "MALICIOUS", "reasoning": ""}, } ) assert result.outputs[0]["id"] == "inc-1" assert result.outputs[1]["id"] == "inc-2" assert "Updated Vega Incidents" in result.readable_output def test_update_incident_command_accepts_incident_id_alias(mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch("Vega.load_current_incident", return_value={}) mock_client = mocker.Mock(spec=Client) mock_client.update_incidents.return_value = { "incidents": [ {"incidentId": "inc-1", "status": "INVESTIGATING", "verdict": "SUSPICIOUS"}, {"incidentId": "inc-2", "status": "INVESTIGATING", "verdict": "SUSPICIOUS"}, ] } update_incident_command( mock_client, { "incident_id": ["inc-1", "inc-2"], "status": "INVESTIGATING", "verdict": "SUSPICIOUS", }, ) mock_client.update_incidents.assert_called_once_with( { "incidentIds": ["inc-1", "inc-2"], "status": "INVESTIGATING", "verdict": {"value": "SUSPICIOUS", "reasoning": ""}, } ) def test_update_alert_command_requires_update_fields(mocker): mocker.patch("Vega.load_current_incident", return_value={}) mock_client = mocker.Mock(spec=Client) with pytest.raises( DemistoException, match="At least one of status, severity, verdict, verdict reasoning, comment, or assignees", ): update_alert_command(mock_client, {"alert_ids": "alert-1"}) def test_update_incident_command_updates_with_comment(mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch("Vega.load_current_incident", return_value={}) mock_client = mocker.Mock(spec=Client) mock_client.update_incidents.return_value = { "incidents": [{"incidentId": "inc-1", "status": "INVESTIGATING", "verdict": "SUSPICIOUS"}] } mock_client.get_incident_by_id.return_value = { "id": "inc-1", "status": "INVESTIGATING", "verdict": "SUSPICIOUS", } result = update_incident_command( mock_client, { "incident_ids": "inc-1", "status": "INVESTIGATING", "verdict": "SUSPICIOUS", "comment": "Reviewed in XSOAR", }, ) mock_client.update_incidents.assert_called_once_with( { "incidentIds": ["inc-1"], "status": "INVESTIGATING", "verdict": {"value": "SUSPICIOUS", "reasoning": ""}, "comment": "Reviewed in XSOAR", } ) assert result.outputs["id"] == "inc-1" assert "Updated Vega Incidents" in result.readable_output def test_update_incident_command_comment_only_returns_note(mocker): mocker.patch("Vega.load_current_incident", return_value={}) mock_client = mocker.Mock(spec=Client) mock_client.update_incidents.return_value = {"incidents": [{"incidentId": "inc-1"}]} result = update_incident_command(mock_client, {"incident_ids": "inc-1", "comment": "test comment 1"}) mock_client.update_incidents.assert_called_once_with({"incidentIds": ["inc-1"], "comment": "test comment 1"}) assert result.readable_output == "test comment 1" assert result.entry_type == EntryType.NOTE assert result.mark_as_note is True def test_build_comment_war_room_entry_uses_plain_text_note(): entry = _build_comment_war_room_entry("test comment 2", tags=["From Vega"]) assert entry["Type"] == EntryType.NOTE assert entry["Contents"] == "test comment 2" assert entry["ContentsFormat"] == EntryFormat.TEXT assert entry["Note"] is True assert entry["Tags"] == ["From Vega"] def test_resolve_incident_id_from_incident_uses_explicit_incident_id(): incident = { "type": "Vega Incident", "CustomFields": {"vegaincidentid": "inc-from-field"}, } assert resolve_incident_id_from_incident({"incident_ids": "inc-explicit"}, incident) == "inc-explicit" assert resolve_incident_id_from_incident({"incident_id": "inc-legacy"}, incident) == "inc-legacy" def test_resolve_incident_status_for_update_uses_incident_status_field(): incident = {"CustomFields": {VEGA_INCIDENT_STATUS_FIELD: "INVESTIGATING"}} assert _resolve_incident_status_for_update({}, incident) == "INVESTIGATING" def test_resolve_incident_status_for_update_falls_back_to_legacy_vegastatus(): incident = {"CustomFields": {VEGA_ALERT_STATUS_FIELD: "ON HOLD"}} assert _resolve_incident_status_for_update({}, incident) == "ON HOLD" def test_resolve_incident_status_for_update_prefers_vegaincidentstatus(): incident = { "CustomFields": { VEGA_INCIDENT_STATUS_FIELD: "UNDER REVIEW", VEGA_ALERT_STATUS_FIELD: "OPEN", } } assert _resolve_incident_status_for_update({}, incident) == "UNDER REVIEW" def test_format_raw_entity_for_xsoar_normalizes_severity(): incident = {"id": "inc-1", "severity": "high", "vegaEntityType": "Vega Incident"} _format_raw_entity_for_xsoar(incident) assert incident["severity"] == "HIGH" def test_normalize_vega_severity_for_display(): assert _normalize_vega_severity_for_display("medium") == "MEDIUM" assert _normalize_vega_severity_for_display(2) == "MEDIUM" assert _normalize_vega_severity_for_display("3") == "HIGH" def test_extract_vega_verdict_from_entity_prefers_user_verdict(): entity = { "verdict": "SUSPICIOUS", "userVerdict": {"value": "BENIGN", "reasoning": "Reviewed by analyst"}, } assert _extract_vega_verdict_from_entity(entity) == "BENIGN" def test_build_mirror_sync_object_includes_only_sync_fields(): incident = { "id": "inc-1", "status": "INVESTIGATING", "severity": 2, "verdict": "SUSPICIOUS", "userVerdict": {"value": "BENIGN"}, "verdictReasoning": "Confirmed benign", "incidentSummary": "Should not mirror", "assignee": {"displayName": "Analyst"}, "comments": [{"text": "note", "addedAt": "2026-06-16T12:00:00Z", "addedBy": "a"}], } sync_object = _build_mirror_sync_object(incident, MIRROR_ENTITY_SUFFIX_INCIDENT) assert sync_object["id"] == "inc-1" assert sync_object["mirror_id"] == "incident:inc-1" assert "type" not in sync_object assert sync_object["vegaEntityType"] == "Vega Incident" assert sync_object["severity"] == "MEDIUM" assert sync_object["verdict"] == "BENIGN" assert sync_object["verdictReasoning"] == "Confirmed benign" assert sync_object["status"] == "INVESTIGATING" assert sync_object["CustomFields"]["vegaincidentid"] == "inc-1" assert sync_object["CustomFields"]["vegaincidentstatus"] == "INVESTIGATING" assert sync_object["CustomFields"]["vegaseverity"] == "MEDIUM" assert sync_object["CustomFields"]["vegaverdict"] == "BENIGN" assert sync_object["CustomFields"]["vegaverdictreasoning"] == "Confirmed benign" assert "vegaComments" in sync_object assert "note" in sync_object["vegaComments"] assert sync_object["CustomFields"]["vegacomments"] == sync_object["vegaComments"] assert "incidentSummary" not in sync_object assert "assignee" not in sync_object def test_build_mirror_sync_object_reflects_removed_comments(): incident = { "id": "inc-1", "status": "INVESTIGATING", "severity": 2, "comments": [], } sync_object = _build_mirror_sync_object(incident, MIRROR_ENTITY_SUFFIX_INCIDENT) assert "vegaComments" in sync_object assert "No comments are available" in sync_object["vegaComments"] assert sync_object["CustomFields"]["vegacomments"] == sync_object["vegaComments"] assert "vegaComments" not in sync_object or "Removed comment" not in sync_object["vegaComments"] def test_resolve_mirror_updated_from_uses_mirror_cursor(): last_update = (datetime.now(UTC) - timedelta(minutes=10)).strftime("%Y-%m-%dT%H:%M:%SZ") updated_from = _resolve_mirror_updated_from(last_update) parsed = datetime.strptime(updated_from, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC) assert parsed <= datetime.now(UTC) - timedelta(minutes=11) def test_resolve_mirror_updated_to_uses_future_buffer(): updated_to = _resolve_mirror_updated_to() parsed = datetime.strptime(updated_to, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC) assert parsed >= datetime.now(UTC) def test_entity_updated_after_returns_false_without_timestamp(): entity = {"status": "OPEN"} assert ( _entity_updated_after( entity, MIRROR_ENTITY_SUFFIX_ALERT, datetime(2026, 6, 15, 11, 0, 0, tzinfo=UTC), ) is False ) def test_normalize_verdict_reasoning_from_user_verdict(): raw = {"userVerdict": {"value": "BENIGN", "reasoning": "Reviewed by analyst"}} assert _normalize_verdict_reasoning_for_display(raw) == "N/A" assert _extract_verdict_reasoning_from_entity(raw) is None def test_build_mirror_sync_object_refreshes_mirror_direction_each_cycle(mocker): mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mocker.patch.object(demisto, "integrationInstance", return_value="Vega_instance_1") sync_object = _build_mirror_sync_object( {"id": "alert-1", "status": "OPEN", "severity": "HIGH"}, MIRROR_ENTITY_SUFFIX_ALERT, ) assert sync_object["dbotMirrorDirection"] == "Both" assert sync_object["mirror_direction"] == "Both" assert sync_object["dbotMirrorInstance"] == "Vega_instance_1" assert sync_object["dbotMirrorId"] == "alert:alert-1" def test_build_mirror_sync_object_sets_in_direction_when_outgoing_mirror_disabled( mocker, ): mocker.patch.object(demisto, "params", return_value={"autoclosure": "false"}) mocker.patch.object(demisto, "integrationInstance", return_value="Vega_instance_1") sync_object = _build_mirror_sync_object( {"id": "inc-1", "status": "INVESTIGATING", "severity": "MEDIUM"}, MIRROR_ENTITY_SUFFIX_INCIDENT, ) assert sync_object["dbotMirrorDirection"] == "In" assert sync_object["mirror_direction"] == "In" def test_build_mirror_sync_object_includes_alert_severity(): alert = { "id": "alert-1", "status": "OPEN", "severity": "HIGH", "verdict": "SUSPICIOUS", "verdictReasoning": "Suspicious activity", } sync_object = _build_mirror_sync_object(alert, MIRROR_ENTITY_SUFFIX_ALERT) assert sync_object["id"] == "alert-1" assert sync_object["mirror_id"] == "alert:alert-1" assert "type" not in sync_object assert sync_object["vegaEntityType"] == "Vega Alert" assert sync_object["severity"] == "HIGH" assert sync_object["verdictReasoning"] == "Suspicious activity" assert sync_object["CustomFields"]["alertid"] == "alert-1" assert sync_object["CustomFields"]["vegaalertseverity"] == "HIGH" assert sync_object["CustomFields"]["vegastatus"] == "OPEN" def test_build_mirror_sync_object_strips_prefixed_remote_id(): alert = { "id": "019e1b27-5128-7633-9b70-77925a8971ca", "status": "OPEN", "severity": "HIGH", } sync_object = _build_mirror_sync_object( alert, MIRROR_ENTITY_SUFFIX_ALERT, remote_id="alert:019e1b27-5128-7633-9b70-77925a8971ca", ) assert sync_object["id"] == "019e1b27-5128-7633-9b70-77925a8971ca" assert sync_object["mirror_id"] == "alert:019e1b27-5128-7633-9b70-77925a8971ca" def test_build_mirror_sync_object_upgrades_legacy_bare_remote_id(): alert = { "id": "alert-1", "status": "OPEN", "severity": "HIGH", } sync_object = _build_mirror_sync_object( alert, MIRROR_ENTITY_SUFFIX_ALERT, remote_id="alert-1", ) assert sync_object["id"] == "alert-1" assert sync_object["mirror_id"] == "alert:alert-1" assert sync_object["CustomFields"]["alertid"] == "alert-1" def test_build_mirror_sync_object_includes_verdict_reasoning_for_incident(): incident = { "id": "inc-1", "status": "INVESTIGATING", "verdict": "SUSPICIOUS", "verdictReasoning": "Reviewed by analyst", "incidentSummary": "Should not mirror", } sync_object = _build_mirror_sync_object(incident, MIRROR_ENTITY_SUFFIX_INCIDENT) assert sync_object["verdict"] == "SUSPICIOUS" assert sync_object["verdictReasoning"] == "Reviewed by analyst" def test_get_remote_data_command_enriches_incident_details(mocker): mock_client = mocker.Mock(spec=Client) mock_client.get_incident_for_mirror.return_value = { "id": "inc-1", "status": "INVESTIGATING", } mock_client.get_incident_by_id.return_value = { "id": "inc-1", "status": "INVESTIGATING", "verdictReasoning": "Loaded from details", } result = get_remote_data_command( mock_client, { "id": "inc-1", "lastUpdate": "2026-06-15T11:00:00Z", "data": {"type": "Vega Incident"}, }, ) lookup_filters = _resolve_mirror_incident_lookup_filters("2026-06-15T11:00:00Z") mock_client.get_incident_by_id.assert_called_once_with("inc-1", **lookup_filters) assert result.mirrored_object["verdictReasoning"] == "Loaded from details" def test_get_remote_data_command_prefers_incident_detail_reasoning(mocker): mock_client = mocker.Mock(spec=Client) mock_client.get_incident_for_mirror.return_value = { "id": "inc-1", "status": "INVESTIGATING", "verdictReasoning": "Stale list value", } mock_client.get_incident_by_id.return_value = { "id": "inc-1", "status": "INVESTIGATING", "verdictReasoning": "Updated analyst note", "userVerdict": {"value": "BENIGN", "reasoning": "Should not be used"}, } result = get_remote_data_command( mock_client, { "id": "inc-1", "lastUpdate": "2026-06-15T11:00:00Z", "data": {"type": "Vega Incident"}, }, ) lookup_filters = _resolve_mirror_incident_lookup_filters("2026-06-15T11:00:00Z") mock_client.get_incident_by_id.assert_called_once_with("inc-1", **lookup_filters) assert result.mirrored_object["verdictReasoning"] == "Updated analyst note" def test_resolve_remote_entity_vega_alert_context_skips_incident_lookup(mocker): mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mock_client = mocker.Mock(spec=Client) mock_client.get_alert_for_mirror.return_value = { "id": "alert-1", "name": "Test Alert", "status": "OPEN", "detectionId": "det-1", } entity, entity_type_suffix = _resolve_remote_entity(mock_client, "alert-1", "Vega Alert") mock_client.get_alert_for_mirror.assert_called_once_with( "alert-1", **_resolve_mirror_entity_lookup_filters(), ) mock_client.get_alert_by_id.assert_not_called() mock_client.get_incident_for_mirror.assert_not_called() assert entity["id"] == "alert-1" assert entity_type_suffix == MIRROR_ENTITY_SUFFIX_ALERT def test_resolve_remote_entity_falls_back_to_full_get_alerts(mocker): mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mock_client = mocker.Mock(spec=Client) mock_client.get_alert_for_mirror.return_value = {} mock_client.get_alert_by_id.return_value = { "id": "019e1b27-511f-7580-a3a6-064c90c35689", "status": "OPEN", "severity": "HIGH", } entity, entity_type_suffix = _resolve_remote_entity( mock_client, "019e1b27-511f-7580-a3a6-064c90c35689", "Vega Alert", ) entity_lookup_filters = _resolve_mirror_entity_lookup_filters() mock_client.get_alert_for_mirror.assert_called_once_with( "019e1b27-511f-7580-a3a6-064c90c35689", **entity_lookup_filters, ) mock_client.get_alert_by_id.assert_called_once_with( "019e1b27-511f-7580-a3a6-064c90c35689", **entity_lookup_filters, ) assert entity["id"] == "019e1b27-511f-7580-a3a6-064c90c35689" assert entity_type_suffix == MIRROR_ENTITY_SUFFIX_ALERT def test_entity_matches_remote_id_accepts_vega_alert_id(mocker): mocker.patch.object(demisto, "debug") entity = {"id": "019e1b27-511f-7580-a3a6-064c90c35689", "vegaAlertId": "VEGA-3409"} assert _entity_matches_remote_id(entity, "019e1b27-511f-7580-a3a6-064c90c35689") assert _entity_matches_remote_id(entity, "VEGA-3409") assert not _entity_matches_remote_id(entity, "missing-id") def test_get_mirroring_fields_uses_calling_context_fallback(mocker): mocker.patch.object(demisto, "integrationInstance", return_value="") mocker.patch.object( demisto, "callingContext", {"context": {"IntegrationInstance": "Vega_prod"}}, ) mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) fields = _get_mirroring_fields() assert fields["mirror_instance"] == "Vega_prod" def test_build_effective_incident_update_args_field_change_updates_severity_only(): effective_args = _build_effective_incident_update_args( {"old": "LOW", "new": "HIGH"}, {"CustomFields": {"vegaincidentstatus": "INVESTIGATING"}}, ) assert effective_args["severity"] == "HIGH" assert "status" not in effective_args def test_build_effective_incident_update_args_field_change_updates_verdict_reasoning_only(): effective_args = _build_effective_incident_update_args( {"old": "Old reasoning", "new": "Confirmed malicious activity"}, { "CustomFields": { "vegaincidentstatus": "INVESTIGATING", "vegaverdict": "MALICIOUS", } }, ) assert effective_args["verdict_reasoning"] == "Confirmed malicious activity" assert effective_args["verdict"] == "MALICIOUS" assert "status" not in effective_args def test_build_direct_incident_update_payload_supports_reasoning_only(): payload = _build_direct_incident_update_payload({"verdict_reasoning": "Confirmed malicious activity"}) assert payload["verdict"]["value"] == "NA" assert payload["verdict"]["reasoning"] == "Confirmed malicious activity" def test_build_direct_alert_update_payload_supports_assignees(): payload = _build_direct_alert_update_payload({"assignees": ["user-1", "user-2"]}) assert payload == {"assignees": ["user-1", "user-2"]} def test_build_direct_incident_update_payload_supports_assignee_emails(): payload = _build_direct_incident_update_payload({"assignee_emails": ["analyst@example.com", "lead@example.com"]}) assert payload == {"assigneeEmails": ["analyst@example.com", "lead@example.com"]} def test_update_alert_command_supports_assignees(mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch("Vega.load_current_incident", return_value={}) mock_client = mocker.Mock(spec=Client) mock_client.update_alerts.return_value = { "alerts": [ { "id": "alert-1", "status": "OPEN", "verdict": "NA", "assignee": {"email": "analyst@example.com", "displayName": "Analyst"}, } ] } result = update_alert_command( mock_client, {"alert_ids": "alert-1", "assignees": ["user-1", "user-2"]}, ) mock_client.update_alerts.assert_called_once_with( { "alertIds": ["alert-1"], "assignees": ["user-1", "user-2"], } ) assert result.outputs["assignee"] == "analyst@example.com" def test_update_incident_command_supports_assignee_emails(mocker): mocker.patch.object(demisto, "getIntegrationContext", return_value={}) mocker.patch.object(demisto, "setIntegrationContext") mocker.patch("Vega.load_current_incident", return_value={}) mock_client = mocker.Mock(spec=Client) mock_client.update_incidents.return_value = { "incidents": [ { "incidentId": "inc-1", "status": "NEW", "verdict": "NA", "assignee": {"email": "lead@example.com"}, } ] } result = update_incident_command( mock_client, { "incident_ids": "inc-1", "assignee_emails": ["lead@example.com", "analyst@example.com"], }, ) mock_client.update_incidents.assert_called_once_with( { "incidentIds": ["inc-1"], "assigneeEmails": ["lead@example.com", "analyst@example.com"], } ) assert result.outputs["assignee"] == "lead@example.com" def test_get_mirroring_fields_autoclosure_enabled(mocker): mocker.patch.object( demisto, "params", return_value={"autoclosure": "true"}, ) mocker.patch.object(demisto, "integrationInstance", return_value="Vega_instance_1") fields = _get_mirroring_fields() assert fields["mirror_direction"] == "Both" assert fields["mirror_instance"] == "Vega_instance_1" def test_get_mirroring_fields_autoclosure_disabled(mocker): mocker.patch.object( demisto, "params", return_value={"autoclosure": "false"}, ) mocker.patch.object(demisto, "integrationInstance", return_value="Vega_instance_1") fields = _get_mirroring_fields() assert fields["mirror_direction"] == "In" assert fields["mirror_instance"] == "Vega_instance_1" def test_is_xsoar_to_vega_mirroring_disabled_when_autoclosure_false(): assert _is_xsoar_to_vega_mirroring_enabled({"autoclosure": "false"}) is False def test_is_xsoar_to_vega_mirroring_enabled_defaults_true(): assert _is_xsoar_to_vega_mirroring_enabled({}) is True def test_collect_outgoing_entry_comments_skips_mirror_tagged_notes(): entries = [ {"Type": EntryType.NOTE, "Contents": "Analyst note", "Tags": []}, { "Type": EntryType.NOTE, "Contents": "From Vega comment", "Tags": [VEGA_MIRROR_TAG_FROM_VEGA], }, { "Type": EntryType.NOTE, "Contents": "To Vega comment", "Tags": [VEGA_MIRROR_TAG_TO_VEGA], }, ] assert _collect_outgoing_entry_comments(entries) == ["Analyst note"] def test_resolve_remote_entity_prefers_alert_when_type_context_set(mocker): mock_client = mocker.Mock(spec=Client) shared_id = "shared-id" mock_client.get_alert_for_mirror.return_value = { "id": shared_id, "vegaAlertId": "VA-shared", "name": "Related Alert", "status": "OPEN", "detectionId": "det-1", } mock_client.get_incident_for_mirror.return_value = { "id": shared_id, "name": "Vega Incident", "status": "INVESTIGATING", "lastUpdated": "2026-06-16T12:00:00Z", "incidentSummary": "Summary", "alertsCount": 1, } entity, entity_type_suffix = _resolve_remote_entity(mock_client, shared_id, "Vega Alert") assert entity["detectionId"] == "det-1" assert entity_type_suffix == MIRROR_ENTITY_SUFFIX_ALERT def test_poll_entity_is_alert(): assert _poll_entity_is_alert({"id": "a-1", "vegaAlertId": "VA-1"}) is True assert _poll_entity_is_alert({"id": "i-1"}) is False assert _poll_entity_is_alert({"id": "a-1", "vegaAlertId": " "}) is False def test_mirror_entity_suffix_from_poll_entity(): assert _mirror_entity_suffix_from_poll_entity({"id": "a-1", "vegaAlertId": "VA-1"}) == MIRROR_ENTITY_SUFFIX_ALERT assert _mirror_entity_suffix_from_poll_entity({"id": "i-1"}) == MIRROR_ENTITY_SUFFIX_INCIDENT def test_get_modified_remote_data_command_skips_ambiguous_shared_bare_id(mocker): mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "executeCommand", return_value=[{"Contents": {"data": []}}]) shared_id = "019e1b27-511f-7580-a3a6-03f68cfea577" mock_client = mocker.Mock(spec=Client) mock_client.get_alerts.return_value = { "alerts": [ { "id": shared_id, "vegaAlertId": "VA-123", "updatedAt": "2026-06-15T12:00:00Z", } ], "total": 1, } mock_client.get_incidents.return_value = { "incidents": [{"id": shared_id, "lastUpdated": "2026-06-15T12:00:00Z"}], "total": 1, } result = get_modified_remote_data_command( mock_client, {"lastUpdate": "2026-06-01T00:00:00Z"}, ) assert set(result.modified_incident_ids) == { f"alert:{shared_id}", f"incident:{shared_id}", } def test_get_modified_remote_data_command(mocker): mocker.patch.object(demisto, "error") mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "executeCommand", return_value=[{"Contents": {"data": []}}]) mock_client = mocker.Mock(spec=Client) mock_client.get_alerts.return_value = { "alerts": [ { "id": "alert-1", "vegaAlertId": "VA-1", "updatedAt": "2026-06-15T12:00:00Z", }, { "id": "alert-2", "vegaAlertId": "VA-2", "updatedAt": "2026-06-15T12:00:00Z", }, ], "total": 2, } mock_client.get_incidents.side_effect = DemistoException("incidents unavailable") result = get_modified_remote_data_command( mock_client, {"lastUpdate": "2026-06-01T00:00:00Z"}, ) assert mock_client.get_alerts.call_args.kwargs["updated_from"] is not None assert "updated_to" not in mock_client.get_alerts.call_args.kwargs assert set(result.modified_incident_ids) == { "alert:alert-1", "alert:alert-2", "alert-1", "alert-2", } def test_get_modified_remote_data_command_respects_entity_filter(mocker): mocker.patch.object(demisto, "params", return_value={"vega_entities": ["Alerts"]}) mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "executeCommand", return_value=[{"Contents": {"data": []}}]) mock_client = mocker.Mock(spec=Client) mock_client.get_alerts.return_value = { "alerts": [ { "id": "alert-1", "vegaAlertId": "VA-1", "updatedAt": "2026-06-15T12:00:00Z", } ], "total": 1, } result = get_modified_remote_data_command( mock_client, {"lastUpdate": "2026-06-01T00:00:00Z"}, ) mock_client.get_alerts.assert_called_once() assert mock_client.get_alerts.call_args.kwargs["updated_from"] is not None assert "updated_to" not in mock_client.get_alerts.call_args.kwargs mock_client.get_incidents.assert_not_called() assert set(result.modified_incident_ids) == {"alert:alert-1", "alert-1"} def test_build_incoming_status_sync_entries_does_not_reopen_open_alert(): entity = {"status": "OPEN", "updatedAt": "2026-06-15T12:00:00Z"} entries = _build_incoming_status_sync_entries(entity, MIRROR_ENTITY_SUFFIX_ALERT, datetime(2026, 6, 15, 11, 0, 0, tzinfo=UTC)) assert entries == [] def test_build_incoming_status_sync_entries_closes_resolved_alert(): entity = {"status": "RESOLVED", "updatedAt": "2026-06-15T12:00:00Z"} entries = _build_incoming_status_sync_entries(entity, MIRROR_ENTITY_SUFFIX_ALERT, datetime(2026, 6, 15, 11, 0, 0, tzinfo=UTC)) assert len(entries) == 1 assert entries[0]["Contents"]["dbotIncidentClose"] is True def test_entity_updated_after_uses_incident_last_updated(): entity = {"status": "INVESTIGATING", "lastUpdated": "2026-06-15T12:00:00Z"} assert _entity_updated_after( entity, MIRROR_ENTITY_SUFFIX_INCIDENT, datetime(2026, 6, 15, 11, 0, 0, tzinfo=UTC), ) def test_get_modified_remote_data_command_both_entities(mocker): mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "executeCommand", return_value=[{"Contents": {"data": []}}]) mock_client = mocker.Mock(spec=Client) mock_client.get_alerts.return_value = { "alerts": [ { "id": "alert-1", "vegaAlertId": "VA-1", "updatedAt": "2026-06-15T12:00:00Z", }, { "id": "alert-2", "vegaAlertId": "VA-2", "updatedAt": "2026-06-15T12:00:00Z", }, ], "total": 2, } mock_client.get_incidents.return_value = { "incidents": [{"id": "inc-1", "lastUpdated": "2026-06-15T12:00:00Z"}], "total": 1, } result = get_modified_remote_data_command( mock_client, {"lastUpdate": "2026-06-01T00:00:00Z"}, ) assert mock_client.get_alerts.call_args.kwargs["updated_from"] is not None assert "updated_to" not in mock_client.get_alerts.call_args.kwargs assert mock_client.get_incidents.call_args.kwargs["updated_to"] is not None assert set(result.modified_incident_ids) == { "alert:alert-1", "alert:alert-2", "alert-1", "alert-2", "incident:inc-1", "inc-1", } def test_get_remote_data_command_alert_with_comment(mocker): mocker.patch("Vega.load_current_incident", return_value={"type": "Vega Alert"}) mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mocker.patch.object(demisto, "integrationInstance", return_value="Vega_instance_1") mocker.patch.object(demisto, "debug") mock_client = mocker.Mock(spec=Client) mock_client.get_alert_for_mirror.return_value = { "id": "alert-1", "name": "Test Alert", "severity": "HIGH", "status": "OPEN", "verdict": "NA", "comments": [ { "text": "Updated in Vega", "addedBy": "analyst@example.com", "addedAt": "2026-06-15T12:00:00Z", } ], } mock_client.get_incident_for_mirror.return_value = {} result = get_remote_data_command( mock_client, { "id": "alert-1", "lastUpdate": "2026-06-15T11:00:00Z", "data": {"type": "Vega Alert"}, }, integration_url="https://api.vega.io", ) assert result.mirrored_object["id"] == "alert-1" assert result.mirrored_object["mirror_id"] == "alert:alert-1" assert "type" not in result.mirrored_object assert result.mirrored_object["vegaEntityType"] == "Vega Alert" assert result.mirrored_object["CustomFields"]["alertid"] == "alert-1" assert "vegaComments" in result.mirrored_object assert "Updated in Vega" in result.mirrored_object["vegaComments"] assert "Updated in Vega" in result.mirrored_object["CustomFields"]["vegacomments"] assert len(result.entries) >= 1 assert result.entries[0]["Contents"].startswith("analyst@example.com") assert result.entries[0]["Tags"] == [VEGA_MIRROR_TAG_FROM_VEGA] def test_get_remote_data_command_vega_alert_context_skips_incident_lookup(mocker): mocker.patch.object(demisto, "integrationInstance", return_value="Vega_instance_1") mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mock_client = mocker.Mock(spec=Client) mock_client.get_alert_for_mirror.return_value = { "id": "alert-1", "name": "Test Alert", "severity": "HIGH", "status": "OPEN", "verdict": "BENIGN", "verdictReasoning": "Confirmed benign", "updatedAt": "2026-06-15T12:00:00Z", "comments": [], } result = get_remote_data_command( mock_client, { "id": "alert:alert-1", "lastUpdate": "2026-06-15T11:00:00Z", "data": {"type": "Vega Alert"}, }, ) mock_client.get_alert_for_mirror.assert_called_once_with( "alert-1", **_resolve_mirror_entity_lookup_filters(), ) mock_client.get_incident_for_mirror.assert_not_called() assert result.mirrored_object["id"] == "alert-1" assert result.mirrored_object["mirror_id"] == "alert:alert-1" assert "type" not in result.mirrored_object assert result.mirrored_object["vegaEntityType"] == "Vega Alert" assert result.mirrored_object["mirror_instance"] == "Vega_instance_1" assert result.mirrored_object["CustomFields"]["alertid"] == "alert-1" assert result.mirrored_object["severity"] == "HIGH" assert result.mirrored_object["verdictReasoning"] == "Confirmed benign" assert result.mirrored_object["CustomFields"]["vegaalertseverity"] == "HIGH" assert result.mirrored_object["CustomFields"]["vegastatus"] == "OPEN" def test_get_remote_data_command_uses_investigation_context_for_bare_alert_id(mocker): mocker.patch("Vega.load_current_incident", return_value={"type": "Vega Alert"}) mocker.patch.object(demisto, "integrationInstance", return_value="Vega_instance_1") mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mocker.patch.object(demisto, "debug") mock_client = mocker.Mock(spec=Client) mock_client.get_alert_for_mirror.return_value = { "id": "019e1b27-511f-7580-a3a6-063a06c73ecb", "name": "Test Alert", "severity": "HIGH", "status": "OPEN", "verdict": "BENIGN", "updatedAt": "2026-06-15T12:00:00Z", "comments": [], } result = get_remote_data_command( mock_client, { "id": "019e1b27-511f-7580-a3a6-063a06c73ecb", "lastUpdate": "2026-06-15T11:00:00Z", }, ) mock_client.get_alert_for_mirror.assert_called_once_with( "019e1b27-511f-7580-a3a6-063a06c73ecb", **_resolve_mirror_entity_lookup_filters(), ) mock_client.get_incident_for_mirror.assert_not_called() assert result.mirrored_object["vegaEntityType"] == "Vega Alert" def test_get_remote_data_command_enforces_incident_type_for_shared_id(mocker): """When alert and incident share a UUID, keep Vega Incident investigations on the incident path.""" mocker.patch.object(demisto, "debug") mocker.patch( "Vega.load_current_incident", return_value={ "type": "Vega Incident", "CustomFields": {"vegaincidentid": "019e1b27-6d49-7ea1-a9d2-f30bf8c69165"}, }, ) shared_id = "019e1b27-6d49-7ea1-a9d2-f30bf8c69165" mock_client = mocker.Mock(spec=Client) mock_client.get_alert_for_mirror.return_value = { "id": shared_id, "name": "Related Alert", "status": "OPEN", } mock_client.get_incident_for_mirror.return_value = { "id": shared_id, "name": "Vega Incident", "status": "INVESTIGATING", "lastUpdated": "2026-06-16T12:00:00Z", } mock_client.get_incident_by_id.return_value = { "id": shared_id, "status": "INVESTIGATING", "verdictReasoning": "Confirmed benign", } result = get_remote_data_command( mock_client, {"id": shared_id, "lastUpdate": "2026-06-15T11:00:00Z"}, ) mock_client.get_alert_for_mirror.assert_not_called() assert "type" not in result.mirrored_object assert result.mirrored_object["vegaEntityType"] == "Vega Incident" assert result.mirrored_object["CustomFields"]["vegaincidentid"] == shared_id assert "alertid" not in result.mirrored_object["CustomFields"] assert result.mirrored_object["CustomFields"]["vegaincidentstatus"] == "INVESTIGATING" def test_resolve_remote_entity_prefers_alert_when_both_match_without_context(mocker): mock_client = mocker.Mock(spec=Client) shared_id = "019e1b27-6d48-7f30-8932-f1d3596141ef" mock_client.get_alert_for_mirror.return_value = { "id": shared_id, "vegaAlertId": "VA-shared", "name": "Related Alert", "status": "OPEN", "detectionId": "det-1", } mock_client.get_incident_for_mirror.return_value = { "id": shared_id, "name": "Vega Incident", "status": "INVESTIGATING", "lastUpdated": "2026-06-16T12:00:00Z", "incidentSummary": "Summary", "alertsCount": 2, } entity, entity_type_suffix = _resolve_remote_entity(mock_client, shared_id) assert entity["name"] == "Related Alert" assert entity_type_suffix == MIRROR_ENTITY_SUFFIX_ALERT def test_resolve_remote_entity_prefers_incident_when_both_match(mocker): mock_client = mocker.Mock(spec=Client) shared_id = "019e1b27-6d48-7f30-8932-f1d3596141ef" mock_client.get_alert_for_mirror.return_value = { "id": shared_id, "vegaAlertId": "VA-shared", "name": "Related Alert", "status": "OPEN", "detectionId": "det-1", } mock_client.get_incident_for_mirror.return_value = { "id": shared_id, "name": "Vega Incident", "status": "INVESTIGATING", "lastUpdated": "2026-06-16T12:00:00Z", "incidentSummary": "Summary", "alertsCount": 2, } entity, entity_type_suffix = _resolve_remote_entity(mock_client, shared_id, "Vega Incident") assert entity["name"] == "Vega Incident" assert entity_type_suffix == MIRROR_ENTITY_SUFFIX_INCIDENT def test_resolve_remote_entity_ignores_mismatched_alert_payload(mocker): mock_client = mocker.Mock(spec=Client) mock_client.get_alert_for_mirror.return_value = { "id": "different-alert-id", "detectionId": "det-1", } mock_client.get_incident_for_mirror.return_value = { "id": "inc-1", "status": "INVESTIGATING", "lastUpdated": "2026-06-16T12:00:00Z", "incidentSummary": "Summary", } entity, entity_type_suffix = _resolve_remote_entity(mock_client, "inc-1") assert entity["id"] == "inc-1" assert entity_type_suffix == MIRROR_ENTITY_SUFFIX_INCIDENT def test_get_remote_data_command_preserves_incident_type_context(mocker): mock_client = mocker.Mock(spec=Client) shared_id = "019e1b27-6d48-7f30-8932-f1d3596141ef" mock_client.get_incident_for_mirror.return_value = { "id": shared_id, "name": "Vega Incident", "status": "INVESTIGATING", "lastUpdated": "2026-06-16T12:00:00Z", "incidentSummary": "Summary", "alertsCount": 1, "comments": [], "verdictReasoning": "Confirmed benign", } mock_client.get_incident_by_id.return_value = { "id": shared_id, "name": "Vega Incident", "status": "INVESTIGATING", "lastUpdated": "2026-06-16T12:00:00Z", "incidentSummary": "Summary", "alertsCount": 1, "comments": [], "verdictReasoning": "Confirmed benign", } result = get_remote_data_command( mock_client, { "id": f"incident:{shared_id}", "lastUpdate": "2026-06-15T11:00:00Z", "data": {"type": "Vega Incident"}, }, integration_url="https://api.vega.io", ) assert result.mirrored_object["id"] == shared_id assert "type" not in result.mirrored_object assert result.mirrored_object["vegaEntityType"] == "Vega Incident" assert result.mirrored_object["mirror_id"] == f"incident:{shared_id}" assert result.mirrored_object["CustomFields"]["vegaincidentid"] == shared_id assert result.mirrored_object["status"] == "INVESTIGATING" assert result.mirrored_object["CustomFields"]["vegaincidentstatus"] == "INVESTIGATING" assert result.mirrored_object["CustomFields"]["vegaverdictreasoning"] == "Confirmed benign" assert "detectionId" not in result.mirrored_object assert "incidentSummary" not in result.mirrored_object def test_get_remote_data_command_preserves_incident_type_with_bare_id(mocker): mock_client = mocker.Mock(spec=Client) shared_id = "019e1b27-6d48-7f30-8932-f1d3596141ef" mock_client.get_incident_for_mirror.return_value = { "id": shared_id, "name": "Vega Incident", "status": "INVESTIGATING", "lastUpdated": "2026-06-16T12:00:00Z", "incidentSummary": "Summary", "comments": [], } mock_client.get_incident_by_id.return_value = { "id": shared_id, "status": "INVESTIGATING", "verdictReasoning": "Confirmed benign", } result = get_remote_data_command( mock_client, { "id": shared_id, "lastUpdate": "2026-06-15T11:00:00Z", "data": { "type": "Vega Incident", "CustomFields": {"vegaincidentid": shared_id}, }, }, ) mock_client.get_alert_for_mirror.assert_not_called() assert "type" not in result.mirrored_object assert result.mirrored_object["vegaEntityType"] == "Vega Incident" assert result.mirrored_object["CustomFields"]["vegaincidentstatus"] == "INVESTIGATING" def test_resolve_remote_entity_uses_prefixed_incident_id(mocker): mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mock_client = mocker.Mock(spec=Client) mock_client.get_incident_for_mirror.return_value = { "id": "inc-1", "status": "INVESTIGATING", "lastUpdated": "2026-06-16T12:00:00Z", "incidentSummary": "Summary", } entity, entity_type_suffix = _resolve_remote_entity( mock_client, "incident:inc-1", mirror_last_update="2026-06-15T11:00:00Z", ) mock_client.get_alert_for_mirror.assert_not_called() mock_client.get_incident_for_mirror.assert_called_once() assert mock_client.get_incident_for_mirror.call_args.args[0] == "inc-1" lookup_filters = mock_client.get_incident_for_mirror.call_args.kwargs assert lookup_filters == _resolve_mirror_entity_lookup_filters() assert entity["id"] == "inc-1" assert entity_type_suffix == MIRROR_ENTITY_SUFFIX_INCIDENT def test_normalize_incident_api_entity_uses_incident_id(): normalized = _normalize_incident_api_entity( { "incidentId": "019e1b27-6d49-7ea1-a9d2-f30bf8c69165", "lastUpdate": "2026-06-16T12:00:00Z", "alertCount": 3, } ) assert normalized["id"] == "019e1b27-6d49-7ea1-a9d2-f30bf8c69165" assert normalized["lastUpdated"] == "2026-06-16T12:00:00Z" assert normalized["alertsCount"] == 3 def test_get_incident_by_id_returns_empty_when_not_found(mocker): client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) mocker.patch.object(client, "get_incidents", return_value={"incidents": [], "total": 0}) incident = client.get_incident_by_id("inc-1") assert incident == {} def test_get_alert_for_mirror_uses_lightweight_query(mocker): client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) mock_graphql = mocker.patch.object( client, "_graphql_request", return_value={ "data": { "getAlerts": { "alerts": [ { "id": "alert-1", "status": "OPEN", "updatedAt": "2026-06-15T12:00:00Z", } ], "total": 1, } } }, ) alert = client.get_alert_for_mirror("alert-1") assert alert["id"] == "alert-1" mock_graphql.assert_called_once() assert mock_graphql.call_args.args[0] == GET_ALERT_MIRROR_QUERY assert mock_graphql.call_args.args[1] == { "alertIds": ["alert-1"], "limit": 1, "offset": 0, } def test_get_alert_for_mirror_passes_from_time_filter(mocker): client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) mock_graphql = mocker.patch.object( client, "_graphql_request", return_value={ "data": { "getAlerts": { "alerts": [ { "id": "alert-1", "status": "OPEN", "updatedAt": "2026-06-15T12:00:00Z", } ], "total": 1, } } }, ) alert = client.get_alert_for_mirror("alert-1", from_time="2026-06-01T00:00:00Z") assert alert["id"] == "alert-1" assert mock_graphql.call_args.args[1] == { "alertIds": ["alert-1"], "from": "2026-06-01T00:00:00Z", "limit": 1, "offset": 0, } def test_resolve_remote_entity_alert_uses_lookup_filters(mocker): mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mock_client = mocker.Mock(spec=Client) mock_client.get_alert_for_mirror.return_value = { "id": "alert-1", "status": "OPEN", "updatedAt": "2026-06-15T12:00:00Z", } entity, entity_type_suffix = _resolve_remote_entity( mock_client, "alert:alert-1", mirror_last_update="2026-06-15T11:00:00Z", ) mock_client.get_incident_for_mirror.assert_not_called() assert mock_client.get_alert_for_mirror.call_args.args[0] == "alert-1" assert mock_client.get_alert_for_mirror.call_args.kwargs == _resolve_mirror_entity_lookup_filters() assert entity["id"] == "alert-1" assert entity_type_suffix == MIRROR_ENTITY_SUFFIX_ALERT def test_normalize_mirror_field_value_prefers_new_value(): assert _normalize_mirror_field_value({"old": "OPEN", "new": "RESOLVED"}) == "RESOLVED" def test_mirror_field_value_reads_old_new_delta_from_custom_fields(): value = _mirror_field_value( VEGA_ALERT_STATUS_FIELD, {"CustomFields": {VEGA_ALERT_STATUS_FIELD: {"old": "OPEN", "new": "RESOLVED"}}}, {}, ) assert value == "RESOLVED" def test_update_remote_system_command_updates_alert_from_old_new_delta(mocker): mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mocker.patch.object(demisto, "debug") mock_client = mocker.Mock(spec=Client) mock_client.get_alert_for_mirror.return_value = {"id": "alert-1", "status": "OPEN"} update_remote_system_command( mock_client, { "remoteId": "alert:alert-1", "incidentChanged": "true", "delta": {"CustomFields": {VEGA_ALERT_STATUS_FIELD: {"old": "OPEN", "new": "RESOLVED"}}}, "data": { "type": "Vega Alert", "CustomFields": {VEGA_ALERT_STATUS_FIELD: "OPEN"}, }, }, ) mock_client.update_alerts.assert_called_once_with( { "alertIds": ["alert-1"], "status": "RESOLVED", } ) def test_mirror_field_changed_in_delta_treats_equivalent_values_as_unchanged(): delta = { "CustomFields": { VEGA_ALERT_STATUS_FIELD: {"old": "Open", "new": "OPEN"}, VEGA_ALERT_SEVERITY_FIELD: {"old": "Critical", "new": "CRITICAL"}, VEGA_VERDICT_FIELD: {"old": "N/A", "new": "NA"}, } } assert _mirror_field_changed_in_delta(VEGA_ALERT_STATUS_FIELD, delta, MIRROR_ENTITY_SUFFIX_ALERT) is False assert _mirror_field_changed_in_delta(VEGA_ALERT_SEVERITY_FIELD, delta, MIRROR_ENTITY_SUFFIX_ALERT) is False assert _mirror_field_changed_in_delta(VEGA_VERDICT_FIELD, delta, MIRROR_ENTITY_SUFFIX_ALERT) is False def test_build_outgoing_alert_mirror_update_skips_unchanged_delta_fields(): update_input = _build_outgoing_alert_mirror_update( { "CustomFields": { VEGA_ALERT_STATUS_FIELD: {"old": "Open", "new": "Open"}, VEGA_ALERT_SEVERITY_FIELD: {"old": "Critical", "new": "Critical"}, VEGA_VERDICT_FIELD: {"old": "N/A", "new": "NA"}, } }, { "type": "Vega Alert", "CustomFields": { VEGA_ALERT_STATUS_FIELD: "Open", VEGA_ALERT_SEVERITY_FIELD: "Critical", VEGA_VERDICT_FIELD: "N/A", }, }, None, ) assert update_input == {} def test_update_remote_system_command_skips_incoming_mirror_echo_updates(mocker): mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mocker.patch("Vega.load_current_incident", return_value={"type": "Vega Alert"}) mocker.patch.object(demisto, "debug") mock_client = mocker.Mock(spec=Client) mock_client.get_alert_for_mirror.return_value = { "id": "alert-1", "status": "OPEN", "severity": "CRITICAL", "verdict": "NA", } update_remote_system_command( mock_client, { "remoteId": "alert:alert-1", "incidentChanged": "true", "delta": { "CustomFields": { VEGA_ALERT_STATUS_FIELD: {"old": "Open", "new": "Open"}, VEGA_ALERT_SEVERITY_FIELD: {"old": "Critical", "new": "Critical"}, VEGA_VERDICT_FIELD: {"old": "N/A", "new": "NA"}, } }, "data": { "type": "Vega Alert", "CustomFields": { VEGA_ALERT_STATUS_FIELD: "Open", VEGA_ALERT_SEVERITY_FIELD: "Critical", VEGA_VERDICT_FIELD: "N/A", }, }, }, ) mock_client.update_alerts.assert_not_called() def test_update_remote_system_command_mirrors_war_room_comment_without_field_echo( mocker, ): mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mocker.patch("Vega.load_current_incident", return_value={"type": "Vega Alert"}) mocker.patch.object(demisto, "debug") mock_client = mocker.Mock(spec=Client) mock_client.get_alert_for_mirror.return_value = { "id": "alert-1", "status": "OPEN", "severity": "CRITICAL", } update_remote_system_command( mock_client, { "remoteId": "alert:alert-1", "incidentChanged": "true", "delta": { "CustomFields": { VEGA_ALERT_STATUS_FIELD: {"old": "Open", "new": "Open"}, VEGA_ALERT_SEVERITY_FIELD: {"old": "Critical", "new": "Critical"}, } }, "data": { "type": "Vega Alert", "CustomFields": { VEGA_ALERT_STATUS_FIELD: "Open", VEGA_ALERT_SEVERITY_FIELD: "Critical", }, }, "entries": [ { "Type": EntryType.NOTE, "Contents": "test 1", "Tags": [], } ], }, ) mock_client.update_alerts.assert_called_once_with({"alertIds": ["alert-1"], "comment": "test 1"}) def test_get_incident_for_mirror_uses_lightweight_query(mocker): client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) mock_graphql = mocker.patch.object( client, "_graphql_request", return_value={ "data": { "getIncidents": { "incidents": [ { "id": "inc-1", "status": "INVESTIGATING", "lastUpdated": "2026-06-15T12:00:00Z", } ], "total": 1, } } }, ) incident = client.get_incident_for_mirror("inc-1") assert incident["id"] == "inc-1" mock_graphql.assert_called_once() assert mock_graphql.call_args.args[0] == GET_INCIDENT_MIRROR_QUERY assert mock_graphql.call_args.args[1] == { "incidentIds": ["inc-1"], "limit": 1, "offset": 0, } def test_get_incident_for_mirror_passes_lookup_time_filters(mocker): client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) mock_graphql = mocker.patch.object( client, "_graphql_request", return_value={ "data": { "getIncidents": { "incidents": [ { "id": "inc-1", "status": "INVESTIGATING", "lastUpdated": "2026-06-15T12:00:00Z", } ], "total": 1, } } }, ) incident = client.get_incident_for_mirror("inc-1", from_time="2026-06-15T10:00:00Z") assert incident["id"] == "inc-1" mock_graphql.assert_called_once() assert mock_graphql.call_args.args[0] == GET_INCIDENT_MIRROR_QUERY assert mock_graphql.call_args.args[1] == { "incidentIds": ["inc-1"], "limit": 1, "offset": 0, "from": "2026-06-15T10:00:00Z", } def test_get_remote_data_command_passes_last_update_to_incident_lookup(mocker): mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mock_client = mocker.Mock(spec=Client) mock_client.get_incident_for_mirror.return_value = { "id": "inc-1", "status": "INVESTIGATING", } mock_client.get_incident_by_id.return_value = { "id": "inc-1", "status": "INVESTIGATING", "verdictReasoning": "Loaded from details", } get_remote_data_command( mock_client, { "id": "incident:inc-1", "lastUpdate": "2026-06-15T11:00:00Z", "data": {"type": "Vega Incident"}, }, ) entity_lookup_filters = _resolve_mirror_entity_lookup_filters() detail_lookup_filters = _resolve_mirror_incident_lookup_filters("2026-06-15T11:00:00Z") mock_client.get_incident_for_mirror.assert_called_once_with("inc-1", **entity_lookup_filters) mock_client.get_incident_by_id.assert_called_once_with("inc-1", **detail_lookup_filters) def test_suppress_noisy_http_integration_logs_filters_header_lines(mocker): import http.client as http_client mocker.patch("Vega.is_debug_mode", return_value=True) captured: list[str] = [] integration_logger_write = LOG.write had_filter_flag = getattr(LOG, "_vega_http_log_filter_installed", False) def capture_write(msg): text = msg.decode(LOG.encoding) if isinstance(msg, bytes) else str(msg) captured.append(text) integration_logger_write(msg) LOG.write = capture_write LOG._vega_http_log_filter_installed = False try: _suppress_noisy_http_integration_logs() LOG.write("header: X-Amz-Cf-Pop: MRS52-P5\n") LOG.write("Vega mirror | stage=resolve-entity | lookup completed\n") assert captured == ["Vega mirror | stage=resolve-entity | lookup completed\n"] assert http_client.HTTPConnection.debuglevel == 0 finally: LOG.write = integration_logger_write LOG._vega_http_log_filter_installed = had_filter_flag def test_get_remote_data_command_not_found_preserves_incident_type(mocker): mocker.patch.object(demisto, "debug") mock_client = mocker.Mock(spec=Client) mock_client.get_incident_for_mirror.return_value = {} mock_client.get_incident_by_id.return_value = {} result = get_remote_data_command( mock_client, { "id": "incident:019e1b27-6d49-7ea1-a9d2-f30bf8c69165", "lastUpdate": "2026-06-15T11:00:00Z", }, ) assert result.mirrored_object["vegaEntityType"] == "Vega Incident" assert result.mirrored_object["id"] == "019e1b27-6d49-7ea1-a9d2-f30bf8c69165" assert result.mirrored_object["mirror_id"] == "incident:019e1b27-6d49-7ea1-a9d2-f30bf8c69165" def test_resolve_remote_entity_accepts_incident_id_field(mocker): mock_client = mocker.Mock(spec=Client) mock_client.get_incident_for_mirror.return_value = { "incidentId": "inc-1", "status": "INVESTIGATING", "lastUpdate": "2026-06-16T12:00:00Z", "incidentSummary": "Summary", } entity, entity_type_suffix = _resolve_remote_entity(mock_client, "incident:inc-1") mock_client.get_alert_for_mirror.assert_not_called() assert entity["id"] == "inc-1" assert entity_type_suffix == MIRROR_ENTITY_SUFFIX_INCIDENT def test_entity_type_from_mirror_payload_prefers_vegaincidentid_over_wrong_type(): payload = { "type": "Vega Alert", "CustomFields": {"vegaincidentid": "inc-1"}, } assert _entity_type_from_mirror_payload(payload) == "Vega Incident" def test_mirror_entity_type_from_args(): assert _mirror_entity_type_from_args({"data": {"type": "Vega Incident"}}, "inc-1") == "Vega Incident" def test_entity_type_from_field_keys_prefers_incident_when_both_present(): payload = { "CustomFields": { "vegaincidentid": "inc-1", "alertid": "alert-1", } } assert _entity_type_from_field_keys(payload) == "Vega Incident" assert _mirror_entity_type_from_args({"data": json.dumps({"Type": "Vega Alert"})}, "alert-1") == "Vega Alert" assert _mirror_entity_type_from_args({"id": "alert:alert-1"}, "alert:alert-1") == "Vega Alert" assert _mirror_entity_type_from_args({"id": "incident:inc-1"}, "incident:inc-1") == "Vega Incident" assert _mirror_entity_type_from_args({"delta": {"vegaincidentstatus": "INVESTIGATING"}}, "inc-1") == "Vega Incident" assert _mirror_entity_type_from_args({"delta": {"vegastatus": "OPEN"}}, "alert-1") == "Vega Alert" def test_mirror_entity_type_from_args_parses_raw_json_from_data(): raw = {"id": "alert-1", "vegaEntityType": "Vega Alert"} assert _mirror_entity_type_from_args({"data": {"rawJSON": json.dumps(raw)}}, "alert-1") == "Vega Alert" def test_mirror_entity_type_from_args_parses_custom_fields_from_data(): assert _mirror_entity_type_from_args({"data": {"CustomFields": {"vegaincidentid": "inc-1"}}}, "inc-1") == "Vega Incident" assert _mirror_entity_type_from_args({"data": {"CustomFields": {"alertid": "alert-1"}}}, "alert-1") == "Vega Alert" def test_mirror_entity_type_from_args_skips_investigation_context_when_disabled(mocker): load_current_incident = mocker.patch("Vega.load_current_incident") remote_id = "019e1b27-511f-7580-a3a6-063a06c73ecb" assert _mirror_entity_type_from_args({"id": remote_id}, remote_id, use_investigation_context=False) is None load_current_incident.assert_not_called() def test_mirror_entity_type_from_args_uses_investigation_context(mocker): mocker.patch("Vega.load_current_incident", return_value={"type": "Vega Alert"}) mocker.patch.object(demisto, "debug") assert ( _mirror_entity_type_from_args( {"id": "019e1b27-511f-7580-a3a6-063a06c73ecb"}, "019e1b27-511f-7580-a3a6-063a06c73ecb", ) == "Vega Alert" ) def test_update_remote_system_command_disabled(mocker): mocker.patch.object(demisto, "params", return_value={"autoclosure": "false"}) mock_client = mocker.Mock(spec=Client) remote_id = update_remote_system_command( mock_client, { "remoteId": "alert-1", "incidentChanged": "true", "delta": {"vegastatus": "RESOLVED"}, "data": {"vegastatus": "RESOLVED"}, }, ) assert remote_id == "alert-1" mock_client.update_alerts.assert_not_called() def test_update_remote_system_command_updates_alert_severity(mocker): mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mock_client = mocker.Mock(spec=Client) mock_client.get_alert_for_mirror.return_value = { "id": "alert-1", "status": "OPEN", "severity": "LOW", } mock_client.get_incident_for_mirror.return_value = {} remote_id = update_remote_system_command( mock_client, { "remoteId": "alert:alert-1", "incidentChanged": "true", "delta": {"vegaalertseverity": "CRITICAL"}, "data": {"vegaalertseverity": "CRITICAL"}, }, ) assert remote_id == "alert:alert-1" mock_client.update_alerts.assert_called_once_with( { "alertIds": ["alert-1"], "severity": "CRITICAL", } ) def test_update_remote_system_command_updates_alert(mocker): mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mocker.patch("Vega.load_current_incident", return_value={"type": "Vega Alert"}) mocker.patch.object(demisto, "debug") mock_client = mocker.Mock(spec=Client) mock_client.get_alert_for_mirror.return_value = {"id": "alert-1", "status": "OPEN"} mock_client.get_incident_for_mirror.return_value = {} remote_id = update_remote_system_command( mock_client, { "remoteId": "alert-1", "incidentChanged": "true", "delta": {"vegaverdict": "MALICIOUS", "vegaverdictreasoning": "Confirmed"}, "data": { "type": "Vega Alert", "vegaverdict": "MALICIOUS", "vegaverdictreasoning": "Confirmed", }, }, ) assert remote_id == "alert-1" mock_client.update_alerts.assert_called_once_with( { "alertIds": ["alert-1"], "verdict": "MALICIOUS", "verdictReasoning": "Confirmed", } ) def test_update_remote_system_command_pushes_new_comment(mocker): mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mocker.patch("Vega.load_current_incident", return_value={"type": "Vega Alert"}) mocker.patch.object(demisto, "debug") mock_client = mocker.Mock(spec=Client) mock_client.get_alert_for_mirror.return_value = {"id": "alert-1", "status": "OPEN"} mock_client.get_incident_for_mirror.return_value = {} update_remote_system_command( mock_client, { "remoteId": "alert-1", "incidentChanged": "true", "delta": {VEGA_NEW_COMMENT_FIELD: "Reviewed in XSOAR"}, "data": {"type": "Vega Alert", VEGA_NEW_COMMENT_FIELD: "Reviewed in XSOAR"}, }, ) mock_client.update_alerts.assert_called_once_with({"alertIds": ["alert-1"], "comment": "Reviewed in XSOAR"}) def test_update_remote_system_command_updates_incident_from_custom_fields_delta(mocker): mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mocker.patch.object(demisto, "debug") mock_client = mocker.Mock(spec=Client) mock_client.get_incident_for_mirror.return_value = { "id": "inc-1", "status": "INVESTIGATING", "severity": "HIGH", } update_remote_system_command( mock_client, { "remoteId": "incident:inc-1", "incidentChanged": "true", "delta": { "CustomFields": { "vegaincidentstatus": "UNDER REVIEW", "vegaverdict": "BENIGN", } }, "data": { "type": "Vega Incident", "CustomFields": { "vegaincidentstatus": "UNDER REVIEW", "vegaverdict": "BENIGN", }, }, }, ) mock_client.update_incidents.assert_called_once_with( { "incidentIds": ["inc-1"], "status": "UNDER_REVIEW", "verdict": {"value": "BENIGN", "reasoning": ""}, } ) def test_alert_to_incident_sets_mirror_metadata(mocker): mocker.patch.object(demisto, "integrationInstance", return_value="Vega_instance_1") mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) alert = { "id": "alert-1", "name": "Test Alert", "severity": "HIGH", "createdAt": TIMESTAMP_T1, } xsoar_incident = alert_to_incident(alert) assert xsoar_incident["dbotMirrorId"] == "alert:alert-1" assert xsoar_incident["dbotMirrorDirection"] == "Both" assert xsoar_incident["dbotMirrorInstance"] == "Vega_instance_1" def test_get_alert_by_id_handles_null_get_alerts_response(mocker): client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) mocker.patch.object(client, "get_alerts", return_value=None) assert client.get_alert_by_id("alert-1") == {} def test_update_alerts_handles_null_graphql_data(mocker): client = Client( base_url=BASE_URL, verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) mocker.patch.object( client, "_graphql_request", return_value={"data": None, "errors": [{"message": "Alert update failed"}]}, ) with pytest.raises(DemistoException, match="Alert update failed"): client.update_alerts({"alertIds": ["alert-1"], "status": "OPEN"}) def test_update_remote_system_command_surfaces_api_error_instead_of_none_type(mocker): mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mocker.patch("Vega.load_current_incident", return_value={"type": "Vega Alert"}) mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "error") mock_client = mocker.Mock(spec=Client) mock_client.get_alert_for_mirror.return_value = { "id": "019e1b27-511f-7580-a3a6-065e9e623a1a", "status": "OPEN", } mock_client.update_alerts.side_effect = DemistoException("Vega API error updating alerts: Alert update failed") remote_id = update_remote_system_command( mock_client, { "remoteId": "019e1b27-511f-7580-a3a6-065e9e623a1a", "incidentChanged": "true", "delta": {"vegastatus": "RESOLVED"}, "data": {"type": "Vega Alert", "vegastatus": "RESOLVED"}, }, ) assert remote_id == "019e1b27-511f-7580-a3a6-065e9e623a1a" mock_client.update_alerts.assert_called_once() def test_update_remote_system_command_updates_incident_from_delta_status_field(mocker): mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mocker.patch("Vega.load_current_incident", return_value={}) mocker.patch.object(demisto, "debug") mock_client = mocker.Mock(spec=Client) mock_client.get_incident_for_mirror.return_value = { "id": "inc-1", "status": "INVESTIGATING", "severity": "HIGH", } update_remote_system_command( mock_client, { "remoteId": "inc-1", "incidentChanged": "true", "delta": {"vegaincidentstatus": "UNDER REVIEW"}, "data": {"CustomFields": {"vegaincidentstatus": "INVESTIGATING"}}, }, ) mock_client.update_incidents.assert_called_once_with( { "incidentIds": ["inc-1"], "status": "UNDER_REVIEW", } ) mock_client.update_alerts.assert_not_called() def test_update_remote_system_command_uses_api_fallback_without_investigation_context( mocker, ): mocker.patch.object(demisto, "params", return_value={"autoclosure": "true"}) mocker.patch( "Vega.demisto.incident", side_effect=TypeError("'NoneType' object is not subscriptable"), ) load_current_incident = mocker.patch("Vega.load_current_incident") mocker.patch.object(demisto, "debug") mock_client = mocker.Mock(spec=Client) mock_client.get_alert_for_mirror.return_value = { "id": "019e1b27-5128-7633-9b70-782afb20f198", "status": "OPEN", } mock_client.get_incident_for_mirror.return_value = {} remote_id = update_remote_system_command( mock_client, { "remoteId": "019e1b27-5128-7633-9b70-782afb20f198", "incidentChanged": "true", "delta": {"vegastatus": "RESOLVED"}, "data": {"vegastatus": "RESOLVED"}, }, ) assert remote_id == "019e1b27-5128-7633-9b70-782afb20f198" load_current_incident.assert_not_called() mock_client.update_alerts.assert_called_once_with( { "alertIds": ["019e1b27-5128-7633-9b70-782afb20f198"], "status": "RESOLVED", } ) def test_get_mapping_fields_command(): response = get_mapping_fields_command() assert len(response.scheme_types_mappings) == 2 scheme_names = {scheme.type_name for scheme in response.scheme_types_mappings} assert scheme_names == {"Vega Alert", "Vega Incident"} def test_validate_lookback_minutes_accepts_valid_range(): from Vega import validate_lookback_minutes validate_lookback_minutes(1) validate_lookback_minutes(30) validate_lookback_minutes(60) validate_lookback_minutes("15") def test_validate_lookback_minutes_rejects_invalid_values(): from Vega import validate_lookback_minutes import pytest with pytest.raises(ValueError, match="Fetch Lookback"): validate_lookback_minutes(0) with pytest.raises(ValueError, match="Fetch Lookback"): validate_lookback_minutes(61) with pytest.raises(ValueError, match="Invalid number"): validate_lookback_minutes("not-a-number") with pytest.raises(ValueError, match="Fetch Lookback"): validate_lookback_minutes(None) def test_test_module_rejects_invalid_lookback_minutes(mocker): from Vega import Client, test_module as vega_test_module mocker.patch("Vega.demisto.getIntegrationContext", return_value={}) mocker.patch("Vega.demisto.setIntegrationContext") mocker.patch("Vega.demisto.info") client = Client( base_url="https://test.com", verify=False, proxy=False, access_key="test-key", access_key_id="test-key-id", ) assert ( vega_test_module(client, backfill_days=30, max_fetch=50, lookback_minutes=0) == "Fetch Lookback (minutes) must be an integer between 1 and 60." ) assert ( vega_test_module(client, backfill_days=30, max_fetch=50, lookback_minutes=61) == "Fetch Lookback (minutes) must be an integer between 1 and 60." ) assert ( vega_test_module(client, backfill_days=30, max_fetch=50, lookback_minutes="abc") == 'Invalid number: "lookback_minutes"="abc"' )