UptycsEventCollector
Uptycs is a cloud-native security analytics platform that provides visibility, threat detection, and compliance across endpoints and cloud workloads.
Analytics & SIEM · Uptycs
Details
| ID | UptycsEventCollector |
|---|---|
| Provider | Uptycs |
| Category | Analytics & SIEM |
| From Version | 8.11.0 |
| Docker Image | demisto/fastapi:0.125.0.10158186 |
| Supported Modules | Agentix XSIAM |
README
This integration is currently in Beta, allowing you to test pre-release software. Note that it may contain bugs, and future updates could include changes that are not backward compatible. We welcome your feedback to help us identify issues and improve the integration.
Overview
Uptycs is a cloud-native security analytics platform that provides unified visibility across endpoints, cloud workloads, and containers. It leverages Osquery-based telemetry and threat intelligence to detect threats, monitor compliance, and investigate security incidents across hybrid environments.
This integration collects security alerts from the Uptycs platform and ingests them into Cortex XSIAM for centralized security monitoring and case (incident) response.
Authentication
This integration uses JWT-based authentication (HS256) to connect to the Uptycs alertsReporting API. The JWT token is generated using the API Key (as the issuer) and API Secret (as the signing key) from the Uptycs API Client Management page.
Before You Start
Before configuring the integration, you must obtain API credentials from your Uptycs console:
Step 1: Generate API Credentials in Uptycs
- Log in to your Uptycs console.
- Navigate to Configuration > Users > API Key.
- Click Create to generate a new API key.
- Download the JSON file containing the API credentials. The file includes:
- key — Your API Key
- secret — Your API Secret
- customerId — Your Customer ID
- domain — Your Uptycs domain (used to construct the Server URL)
Step 2: Construct the Server URL
The Server URL follows the format: https://<domain>.uptycs.io
For example, if your domain is mycompany, the Server URL would be https://mycompany.uptycs.io.
Step 3: (Optional) Obtain Role ID and Security Zone ID
If your Uptycs environment uses role-based access control or security zones:
- Navigate to Configuration > Roles to find the Role ID.
- Navigate to Configuration > Security Zones to find the Security Zone ID.
These are optional and only needed if your API access requires specific role or zone scoping.
Integration Parameters
| Parameter | Description | Required |
|---|---|---|
| Server URL | The Uptycs API base URL. Format: https://<domain>.uptycs.io |
True |
| API Key | The API Key from the Uptycs API Client Management page. Used for JWT authentication. | True |
| API Secret | The API Secret from the Uptycs API Client Management page. Used for JWT signing. | True |
| Customer ID | The Uptycs Customer ID (UUID). Found in the API key JSON file or the Uptycs console URL. | True |
| Role ID | Optional Role ID to include in the JWT token for role-based access control. | False |
| Security Zone ID | Optional Security Zone ID to include in the JWT token for zone-scoped access. | False |
| Trust any certificate (not secure) | When selected, the integration will not verify SSL certificates. | False |
| Use system proxy settings | When selected, the integration will use the system proxy settings. | False |
| Maximum number of alerts per fetch | Maximum number of alerts to fetch per collection cycle. Default: 10000 Note: The API returns a maximum of 1000 alerts per page. |
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.
uptycs-get-events
Gets alerts from Uptycs. This command is intended for developing and debugging and should be used with caution, as it can create duplicate events and exceed API rate limits.
Note: This command is not supported in XSOAR.
Base Command
uptycs-get-events
Input
| Argument Name | Description | Required |
|---|---|---|
| start_time | The start time to fetch alerts from. Supports relative time (e.g., “3 days ago”, “2 hours ago”, “now”) or absolute dates (ISO 8601 format, e.g., “2024-12-01” or “2024-12-01T10:00:00Z”). | Optional |
| end_time | The end time to fetch alerts until. Supports relative time (e.g., “1 hour ago”, “now”) or absolute dates (ISO 8601 format, e.g., “2024-12-01” or “2024-12-01T10:00:00Z”). If not specified, fetches until now. | Optional |
| limit | Maximum number of alerts to retrieve. Default is 10000. | Optional |
| should_push_events | Set to true to push events to XSIAM (use with caution to avoid duplicates), false to only return them in the War Room. Default is false. Possible values are: true, false. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Uptycs.Alert.id | String | Unique identifier of the alert. |
| Uptycs.Alert.agentType | String | Type of agent associated with the alert (e.g., asset). |
| Uptycs.Alert.alertId | String | Alert identifier. |
| Uptycs.Alert.alertRuleId | String | Identifier of the alert rule that triggered this alert. |
| Uptycs.Alert.alertRuleName | String | Name of the alert rule that triggered this alert. |
| Uptycs.Alert.alertStatusReasonId | String | Identifier for the alert status reason. |
| Uptycs.Alert.alertTags | Unknown | Tags associated with the alert (e.g., MITRE ATT&CK techniques). |
| Uptycs.Alert.alertTime | Date | Timestamp when the alert was triggered. |
| Uptycs.Alert.alertTimeSuppresionDuration | String | Duration of alert time suppression. |
| Uptycs.Alert.alertTimeSuppresionStart | String | Start time of alert time suppression. |
| Uptycs.Alert.assetCityId | String | City identifier of the asset. |
| Uptycs.Alert.assetCores | Number | Number of CPU cores on the asset. |
| Uptycs.Alert.assetCpuBrand | String | CPU brand of the asset. |
| Uptycs.Alert.assetDescription | String | Description of the asset. |
| Uptycs.Alert.assetDisabled | Boolean | Whether the asset is disabled. |
| Uptycs.Alert.assetGateway | String | Gateway IP address of the asset. |
| Uptycs.Alert.assetHardwareModel | String | Hardware model of the asset. |
| Uptycs.Alert.assetHardwareSerial | String | Hardware serial number of the asset. |
| Uptycs.Alert.assetHardwareVendor | String | Hardware vendor of the asset. |
| Uptycs.Alert.assetHostName | String | Hostname of the asset associated with the alert. |
| Uptycs.Alert.assetId | String | Unique identifier of the asset. |
| Uptycs.Alert.assetLastActivityAt | Date | Timestamp of the asset’s last activity. |
| Uptycs.Alert.assetLastEnrolledAt | Date | Timestamp when the asset was last enrolled. |
| Uptycs.Alert.assetLatitude | Number | Latitude of the asset’s location. |
| Uptycs.Alert.assetLive | Boolean | Whether the asset is currently live. |
| Uptycs.Alert.assetLocation | String | Geographic location of the asset. |
| Uptycs.Alert.assetLogicalCores | Number | Number of logical CPU cores on the asset. |
| Uptycs.Alert.assetLongitude | Number | Longitude of the asset’s location. |
| Uptycs.Alert.assetManualSlackAssignment | Boolean | Whether the asset has manual Slack assignment. |
| Uptycs.Alert.assetMemoryMb | Number | Memory in MB of the asset. |
| Uptycs.Alert.assetObjectGroupId | String | Object group identifier of the asset. |
| Uptycs.Alert.assetOs | String | Operating system of the asset. |
| Uptycs.Alert.assetOsFlavor | String | OS flavor of the asset (e.g., debian). |
| Uptycs.Alert.assetOsKey | String | OS key identifier of the asset. |
| Uptycs.Alert.assetOsVersion | String | OS version of the asset. |
| Uptycs.Alert.assetOsqueryVersion | String | Osquery version installed on the asset. |
| Uptycs.Alert.assetSlackUserId | String | Slack user ID associated with the asset. |
| Uptycs.Alert.assetStatus | String | Status of the asset (e.g., active). |
| Uptycs.Alert.assignedTo | String | User the alert is assigned to. |
| Uptycs.Alert.attackType | String | Type of attack associated with the alert. |
| Uptycs.Alert.cloudRegionCode | String | Cloud region code of the resource. |
| Uptycs.Alert.cloudResourceId | String | Cloud resource identifier. |
| Uptycs.Alert.cloudTenantId | String | Cloud tenant identifier. |
| Uptycs.Alert.cloudType | String | Cloud provider type. |
| Uptycs.Alert.code | String | Alert code identifier. |
| Uptycs.Alert.createdAt | Date | Timestamp when the alert was created. |
| Uptycs.Alert.custom | Boolean | Whether this is a custom alert rule. |
| Uptycs.Alert.customerId | String | Customer identifier. |
| Uptycs.Alert.description | String | Description of the alert. |
| Uptycs.Alert.displayName | String | Display name of the alert. |
| Uptycs.Alert.eventId | String | Event identifier associated with the alert. |
| Uptycs.Alert.exceptionMetadata | Unknown | Exception metadata containing detailed event information. |
| Uptycs.Alert.groupId | String | Group identifier. |
| Uptycs.Alert.groupName | String | Group name. |
| Uptycs.Alert.grouping | String | Alert grouping category (e.g., ATTACK). |
| Uptycs.Alert.groupingL2 | String | Second-level grouping (e.g., MITRE tactic). |
| Uptycs.Alert.groupingL3 | String | Third-level grouping (e.g., MITRE technique). |
| Uptycs.Alert.groupingL4 | String | Fourth-level grouping. |
| Uptycs.Alert.hashKey | String | Hash key of the alert. |
| Uptycs.Alert.isContainer | Boolean | Whether the alert is related to a container. |
| Uptycs.Alert.isTask | Boolean | Whether the alert is a task. |
| Uptycs.Alert.key | String | Key field of the alert. |
| Uptycs.Alert.lastActiveAt | Date | Timestamp when the alert was last active. |
| Uptycs.Alert.lastOccurredAt | Date | Timestamp when the alert last occurred. |
| Uptycs.Alert.metadata | Unknown | Alert metadata containing detailed event information. |
| Uptycs.Alert.noisy | Boolean | Whether the alert is marked as noisy. |
| Uptycs.Alert.note | String | Note attached to the alert. |
| Uptycs.Alert.noteCreatedAt | Date | Timestamp when the note was created. |
| Uptycs.Alert.noteCreatedBy | String | User who created the note. |
| Uptycs.Alert.noteId | String | Identifier of the note. |
| Uptycs.Alert.remediationActions | String | Remediation actions for the alert. |
| Uptycs.Alert.resolutionDays | Number | Number of days to resolve the alert. |
| Uptycs.Alert.resolvedAt | Date | Timestamp when the alert was resolved. |
| Uptycs.Alert.rowCount | Number | Number of rows associated with the alert. |
| Uptycs.Alert.ruleId | String | Rule identifier that triggered the alert. |
| Uptycs.Alert.severity | String | Severity level of the alert (e.g., low, medium, high, critical). |
| Uptycs.Alert.status | String | Current status of the alert (e.g., open, closed). |
| Uptycs.Alert.updatedAt | Date | Timestamp when the alert was last updated. |
| Uptycs.Alert.updatedBy | String | User who last updated the alert. |
| Uptycs.Alert.value | String | Value associated with the alert key. |
| Uptycs.Alert.source | String | Source of the alert (e.g., host). |
Configuration parameters
url— Server URL (required)api_key— API Key (required)credentials— API Secret (required)customer_id— Customer ID (required)role_id— Role IDsecurity_zone_id— Security Zone IDmax_fetch— Maximum number of alerts per fetch
Commands (1)
-
uptycs-get-eventsGets alerts from Uptycs. Use this command for development and debugging only, as it may produce duplicate events, exceed API rate limits, or disrupt the fetch mechanism.
# pylint: disable=E9010, E9011 """Uptycs Event Collector Integration - Unit Tests Pytest Unit Tests: all function names must start with "test_" More details: https://xsoar.pan.dev/docs/integrations/unit-testing """ import json import re from datetime import datetime, timezone # noqa: UP017 from typing import Any import jwt import pytest from CommonServerPython import * from ContentClientApiModule import ContentClientAuthenticationError import UptycsEventCollector # noqa: E402 from UptycsEventCollector import ( # noqa: E402 APIKeys, APIValues, Client, Config, deduplicate_events, determine_entry_status, enrich_events_for_xsiam, fetch_events_command, fetch_events_with_pagination, generate_jwt_token, get_events_command, get_formatted_utc_time, parse_date_or_use_current, parse_integration_params, test_module, ) # ======================================== # Constants # ======================================== SERVER_URL = "https://test.uptycs.io/" MOCK_API_KEY = "test-api-key-12345" MOCK_API_SECRET = "test-api-secret-67890" MOCK_CUSTOMER_ID = "test-customer-id-uuid" MOCK_ROLE_ID = "test-role-id" MOCK_SECURITY_ZONE_ID = "test-security-zone-id" MOCK_JWT_TOKEN = "mock.jwt.token" # ======================================== # Fixtures # ======================================== @pytest.fixture() def client(mocker): """Returns a mocked Client instance for testing. The client is mocked to prevent actual HTTP requests and JWT generation. """ mocker.patch.object(UptycsEventCollector, "generate_jwt_token", return_value=MOCK_JWT_TOKEN) client_instance = Client( base_url=SERVER_URL, api_key=MOCK_API_KEY, api_secret=MOCK_API_SECRET, customer_id=MOCK_CUSTOMER_ID, verify=True, proxy=False, ) mocker.patch.object(client_instance, "_http_request", return_value={"items": [], "totalCount": 0}) return client_instance # ======================================== # Tests: Helper Functions # ======================================== @pytest.mark.parametrize( "date_string,expected_type", [ ("2024-01-01T00:00:00Z", datetime), ("2025-09-15 17:10:00", datetime), ("3 days ago", datetime), ("1 week", datetime), (None, datetime), ("", datetime), ], ) def test_parse_date_or_use_current_success(date_string: str | None, expected_type: type): """Tests parse_date_or_use_current returns datetime for valid inputs.""" result = parse_date_or_use_current(date_string) assert isinstance(result, expected_type) assert result.tzinfo == timezone.utc # noqa: UP017 def test_parse_date_or_use_current_invalid_raises(): """Tests parse_date_or_use_current raises DemistoException for invalid date.""" with pytest.raises(DemistoException, match="Failed to parse date string"): parse_date_or_use_current("invalid_date_string_12345") @pytest.mark.parametrize( "date_input,expected_format_pattern", [ ("2024-01-01T00:00:00Z", r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}"), ("3 days ago", r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}"), (None, r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}"), ], ) def test_get_formatted_utc_time(date_input: str | None, expected_format_pattern: str): """Tests get_formatted_utc_time returns properly formatted string.""" result = get_formatted_utc_time(date_input) assert isinstance(result, str) assert re.match(expected_format_pattern, result) # ======================================== # Tests: JWT Token Generation # ======================================== def test_generate_jwt_token_structure(): """Tests generate_jwt_token returns a valid JWT with 3 parts.""" token = generate_jwt_token(MOCK_API_KEY, MOCK_API_SECRET) parts = token.split(".") assert len(parts) == 3, "JWT must have 3 parts: header.payload.signature" def test_generate_jwt_token_header(): """Tests generate_jwt_token includes correct header claims.""" token = generate_jwt_token(MOCK_API_KEY, MOCK_API_SECRET) header = jwt.get_unverified_header(token) assert header["alg"] == "HS256" assert header["typ"] == "JWT" @pytest.mark.parametrize( "role_id,security_zone_id,expected_claims,unexpected_claims", [ (None, None, {"iss": MOCK_API_KEY}, ["roleId", "securityZoneId"]), (MOCK_ROLE_ID, None, {"iss": MOCK_API_KEY, "roleId": MOCK_ROLE_ID}, ["securityZoneId"]), (None, MOCK_SECURITY_ZONE_ID, {"iss": MOCK_API_KEY, "securityZoneId": MOCK_SECURITY_ZONE_ID}, ["roleId"]), ( MOCK_ROLE_ID, MOCK_SECURITY_ZONE_ID, {"iss": MOCK_API_KEY, "roleId": MOCK_ROLE_ID, "securityZoneId": MOCK_SECURITY_ZONE_ID}, [], ), ], ) def test_generate_jwt_token_payload_claims( role_id: str | None, security_zone_id: str | None, expected_claims: dict[str, str], unexpected_claims: list[str], ): """Tests generate_jwt_token includes correct payload claims based on optional parameters.""" token = generate_jwt_token(MOCK_API_KEY, MOCK_API_SECRET, role_id=role_id, security_zone_id=security_zone_id) payload = jwt.decode(token, MOCK_API_SECRET, algorithms=["HS256"]) # Verify standard time claims assert "iat" in payload assert "exp" in payload assert payload["exp"] - payload["iat"] == Config.TOKEN_EXPIRY_SECONDS # Verify expected claims for claim, value in expected_claims.items(): assert payload[claim] == value # Verify unexpected claims are absent for claim in unexpected_claims: assert claim not in payload # ======================================== # Tests: parse_integration_params # ======================================== @pytest.mark.parametrize( "params,expected_error", [ ({}, r"(?i)server url is required"), ({"url": ""}, r"(?i)server url is required"), ({"url": SERVER_URL}, r"(?i)api key is required"), ({"url": SERVER_URL, "api_key": ""}, r"(?i)api key is required"), ({"url": SERVER_URL, "api_key": MOCK_API_KEY}, r"(?i)api secret is required"), ({"url": SERVER_URL, "api_key": MOCK_API_KEY, "credentials": {"password": ""}}, r"(?i)api secret is required"), ( {"url": SERVER_URL, "api_key": MOCK_API_KEY, "credentials": {"password": MOCK_API_SECRET}}, r"(?i)customer id is required", ), ( {"url": SERVER_URL, "api_key": MOCK_API_KEY, "credentials": {"password": MOCK_API_SECRET}, "customer_id": ""}, r"(?i)customer id is required", ), ], ) def test_parse_integration_params_missing_required_fail(params: dict[str, Any], expected_error: str): """Tests parse_integration_params fails if required fields are missing.""" with pytest.raises(DemistoException, match=expected_error): parse_integration_params(params) @pytest.mark.parametrize( "params,expected_verify,expected_proxy", [ ( { "url": SERVER_URL, "api_key": MOCK_API_KEY, "credentials": {"password": MOCK_API_SECRET}, "customer_id": MOCK_CUSTOMER_ID, "insecure": True, "proxy": True, }, False, True, ), ( { "url": SERVER_URL.rstrip("/"), "api_key": MOCK_API_KEY, "credentials": {"password": MOCK_API_SECRET}, "customer_id": MOCK_CUSTOMER_ID, "insecure": False, "proxy": False, }, True, False, ), ( { "url": f"{SERVER_URL}///", "api_key": MOCK_API_KEY, "credentials": {"password": MOCK_API_SECRET}, "customer_id": MOCK_CUSTOMER_ID, "insecure": False, "proxy": False, }, True, False, ), ], ) def test_parse_integration_params_success(params: dict[str, Any], expected_verify: bool, expected_proxy: bool): """Tests parse_integration_params handles valid configurations.""" result = parse_integration_params(params) assert result["base_url"] == SERVER_URL assert result["verify"] == expected_verify assert result["proxy"] == expected_proxy assert result["api_key"] == MOCK_API_KEY assert result["api_secret"] == MOCK_API_SECRET assert result["customer_id"] == MOCK_CUSTOMER_ID @pytest.mark.parametrize( "role_id_input,zone_id_input,expected_role_id,expected_zone_id", [ (MOCK_ROLE_ID, MOCK_SECURITY_ZONE_ID, MOCK_ROLE_ID, MOCK_SECURITY_ZONE_ID), ("", "", None, None), ], ) def test_parse_integration_params_optional_fields( role_id_input: str, zone_id_input: str, expected_role_id: str | None, expected_zone_id: str | None, ): """Tests parse_integration_params handles optional role_id and security_zone_id.""" params = { "url": SERVER_URL, "api_key": MOCK_API_KEY, "credentials": {"password": MOCK_API_SECRET}, "customer_id": MOCK_CUSTOMER_ID, "role_id": role_id_input, "security_zone_id": zone_id_input, } result = parse_integration_params(params) assert result["role_id"] == expected_role_id assert result["security_zone_id"] == expected_zone_id # ======================================== # Tests: determine_entry_status # ======================================== @pytest.mark.parametrize( "created_at,updated_at,expected_status", [ ("2024-01-01T00:00:00Z", "2024-01-01T00:00:00Z", "new"), ("2024-01-01T00:00:00Z", "2024-01-02T00:00:00Z", "updated"), ("2024-01-01T00:00:00Z", "2024-06-15T12:30:00Z", "updated"), ], ) def test_determine_entry_status(created_at: str, updated_at: str, expected_status: str): """Tests determine_entry_status returns correct status based on timestamps.""" result = determine_entry_status(created_at, updated_at) assert result == expected_status # ======================================== # Tests: enrich_events_for_xsiam # ======================================== @pytest.mark.parametrize( "event,expected_time,expected_status", [ ( {"id": "1", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z"}, "2024-01-01T00:00:00Z", "new", ), ( {"id": "1", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-02T00:00:00Z"}, "2024-01-01T00:00:00Z", "updated", ), ( {"id": "1", "createdAt": "2024-01-01T00:00:00Z"}, "2024-01-01T00:00:00Z", None, ), ( {"id": "1", "updatedAt": "2024-01-02T00:00:00Z"}, "2024-01-02T00:00:00Z", None, ), ( {"id": "1"}, None, None, ), ], ) def test_enrich_events_for_xsiam( event: dict[str, Any], expected_time: str | None, expected_status: str | None, ): """Tests enrich_events_for_xsiam sets _time and _entry_status correctly for various event shapes.""" events: list[dict[str, Any]] = [event] enrich_events_for_xsiam(events) if expected_time: assert events[0]["_time"] == expected_time else: assert "_time" not in events[0] if expected_status: assert events[0]["_entry_status"] == expected_status else: assert "_entry_status" not in events[0] def test_enrich_events_for_xsiam_multiple_events(): """Tests enrich_events_for_xsiam processes multiple events correctly.""" events: list[dict[str, Any]] = [ {"id": "1", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z"}, {"id": "2", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-02T00:00:00Z"}, {"id": "3", "updatedAt": "2024-01-03T00:00:00Z"}, ] enrich_events_for_xsiam(events) assert events[0]["_entry_status"] == "new" assert events[1]["_entry_status"] == "updated" assert "_entry_status" not in events[2] def test_enrich_events_for_xsiam_empty_list(): """Tests enrich_events_for_xsiam handles empty list.""" events: list[dict[str, Any]] = [] enrich_events_for_xsiam(events) assert events == [] # ======================================== # Tests: deduplicate_events # ======================================== @pytest.mark.parametrize( "events,last_fetched_ids,expected_count,expected_first_id,description", [ ( [{"id": "1", "createdAt": "2024-01-01"}, {"id": "2", "createdAt": "2024-01-02"}], [], 2, "1", "first_run_no_last_ids_empty", ), ([], ["last_id"], 0, None, "empty_events"), ( [ {"id": "1", "createdAt": "2024-01-01"}, {"id": "2", "createdAt": "2024-01-02"}, {"id": "3", "createdAt": "2024-01-03"}, ], ["1"], 2, "2", "single_id_filtered", ), ( [ {"id": "1", "createdAt": "2024-01-01"}, {"id": "2", "createdAt": "2024-01-02"}, {"id": "3", "createdAt": "2024-01-03"}, ], ["1", "2"], 1, "3", "multiple_ids_filtered", ), ( [ {"id": "1", "createdAt": "2024-01-01"}, {"id": "2", "createdAt": "2024-01-02"}, {"id": "3", "createdAt": "2024-01-03"}, ], ["1", "2", "3"], 0, None, "all_duplicates", ), ( [ {"id": "4", "createdAt": "2024-01-04"}, {"id": "5", "createdAt": "2024-01-05"}, ], ["1", "2", "3"], 2, "4", "no_matches_all_new", ), ( [ {"createdAt": "2024-01-01", "data": "event1"}, {"id": "2", "createdAt": "2024-01-02"}, ], ["1"], 2, None, "events_without_id", ), ], ) def test_deduplicate_events( events: list[dict[str, Any]], last_fetched_ids: list[str], expected_count: int, expected_first_id: str | None, description: str, ): """Tests deduplicate_events function with various scenarios.""" result = deduplicate_events(events, last_fetched_ids) assert len(result) == expected_count, f"Failed for {description}" if expected_first_id: assert result[0]["id"] == expected_first_id def test_deduplicate_events_preserves_order(): """Tests that deduplicate_events preserves event order.""" events = [ {"id": "1", "createdAt": "2024-01-01", "data": "first"}, {"id": "2", "createdAt": "2024-01-02", "data": "second"}, {"id": "3", "createdAt": "2024-01-03", "data": "third"}, {"id": "4", "createdAt": "2024-01-04", "data": "fourth"}, ] result = deduplicate_events(events, ["1", "2"]) assert len(result) == 2 assert result[0]["id"] == "3" assert result[0]["data"] == "third" assert result[1]["id"] == "4" assert result[1]["data"] == "fourth" # ======================================== # Tests: Client Initialization # ======================================== def test_client_initialization(client: Client): """Tests Client initialization.""" assert client.customer_id == MOCK_CUSTOMER_ID assert client._base_url == SERVER_URL def test_client_initialization_with_optional_params(mocker): """Tests Client initialization with role_id and security_zone_id.""" mocker.patch.object(UptycsEventCollector, "generate_jwt_token", return_value=MOCK_JWT_TOKEN) client_instance = Client( base_url=SERVER_URL, api_key=MOCK_API_KEY, api_secret=MOCK_API_SECRET, customer_id=MOCK_CUSTOMER_ID, verify=True, proxy=False, role_id=MOCK_ROLE_ID, security_zone_id=MOCK_SECURITY_ZONE_ID, ) assert client_instance.customer_id == MOCK_CUSTOMER_ID UptycsEventCollector.generate_jwt_token.assert_called_once_with( # type: ignore[attr-defined] api_key=MOCK_API_KEY, api_secret=MOCK_API_SECRET, role_id=MOCK_ROLE_ID, security_zone_id=MOCK_SECURITY_ZONE_ID, ) # ======================================== # Tests: Client.get_alerts # ======================================== def test_get_alerts_success(mocker, client: Client): """Tests get_alerts returns items and count from API response.""" mock_items = [ {"id": "alert1", "createdAt": "2024-01-01T00:00:00Z"}, {"id": "alert2", "createdAt": "2024-01-02T00:00:00Z"}, ] mocker.patch.object(client, "_http_request", return_value={"items": mock_items}) items = client.get_alerts(created_after="2024-01-01T00:00:00", created_before="2024-01-03T00:00:00") assert len(items) == 2 assert items[0]["id"] == "alert1" def test_get_alerts_empty_response(mocker, client: Client): """Tests get_alerts handles empty response.""" mocker.patch.object(client, "_http_request", return_value={"items": []}) items = client.get_alerts(created_after="2024-01-01T00:00:00", created_before="2024-01-03T00:00:00") assert len(items) == 0 def test_get_alerts_with_date_range(mocker, client: Client): """Tests get_alerts passes correct parameters for date range.""" mock_http = mocker.patch.object(client, "_http_request", return_value={"items": []}) client.get_alerts( created_after="2024-01-01T00:00:00", created_before="2024-01-02T00:00:00", offset=10, limit=50, ) call_kwargs = mock_http.call_args[1] assert call_kwargs["method"] == "GET" assert MOCK_CUSTOMER_ID in call_kwargs["url_suffix"] params = call_kwargs["params"] assert params[APIKeys.OFFSET] == 10 assert params[APIKeys.LIMIT] == 50 assert params[APIKeys.SORT] == APIValues.DEFAULT_SORT filters = json.loads(params[APIKeys.FILTERS]) assert "lastOccurredAt" in filters assert filters["lastOccurredAt"]["between"] == ["2024-01-01T00:00:00", "2024-01-02T00:00:00"] # ======================================== # Tests: fetch_events_with_pagination # ======================================== def test_fetch_events_with_pagination_single_page(mocker, client: Client): """Tests fetch_events_with_pagination with single page of results.""" mock_events = [{"id": f"event{i}", "createdAt": f"2024-01-0{i}T00:00:00Z"} for i in range(1, 4)] mocker.patch.object(client, "get_alerts", return_value=mock_events) events = fetch_events_with_pagination(client, "2024-01-01T00:00:00", None, 10) assert len(events) == 3 assert events[0]["id"] == "event1" def test_fetch_events_with_pagination_multiple_pages(mocker, client: Client): """Tests fetch_events_with_pagination handles multiple pages.""" mocker.patch.object(Config, "MAX_PAGE_SIZE", 3) page1 = [{"id": f"event{i}", "createdAt": f"2024-01-0{i}T00:00:00Z"} for i in range(1, 4)] page2 = [{"id": f"event{i}", "createdAt": f"2024-01-0{i}T00:00:00Z"} for i in range(4, 6)] mock_get_alerts = mocker.patch.object( client, "get_alerts", side_effect=[page1, page2], ) events = fetch_events_with_pagination(client, "2024-01-01T00:00:00", None, 10) assert len(events) == 5 assert mock_get_alerts.call_count == 2 def test_fetch_events_with_pagination_stops_at_max(mocker, client: Client): """Tests fetch_events_with_pagination stops at max_events.""" mocker.patch.object(Config, "MAX_PAGE_SIZE", 5) page1 = [{"id": f"event{i}", "createdAt": f"2024-01-{i:02d}T00:00:00Z"} for i in range(1, 6)] page2 = [{"id": f"event{i}", "createdAt": f"2024-01-{i:02d}T00:00:00Z"} for i in range(6, 9)] mocker.patch.object( client, "get_alerts", side_effect=[page1, page2], ) events = fetch_events_with_pagination(client, "2024-01-01T00:00:00", None, 7) assert len(events) == 7 def test_fetch_events_with_pagination_empty_page(mocker, client: Client): """Tests fetch_events_with_pagination handles empty page.""" mocker.patch.object(client, "get_alerts", return_value=[]) events = fetch_events_with_pagination(client, "2024-01-01T00:00:00", None, 10) assert len(events) == 0 def test_fetch_events_with_pagination_preserves_api_order(mocker, client: Client): """Tests fetch_events_with_pagination preserves the ascending order returned by the API.""" mock_events = [ {"id": "event1", "createdAt": "2024-01-01T00:00:00Z"}, {"id": "event2", "createdAt": "2024-01-02T00:00:00Z"}, {"id": "event3", "createdAt": "2024-01-03T00:00:00Z"}, ] mocker.patch.object(client, "get_alerts", return_value=mock_events) events = fetch_events_with_pagination(client, "2024-01-01T00:00:00", None, 10) assert events[0]["id"] == "event1" assert events[1]["id"] == "event2" assert events[2]["id"] == "event3" def test_fetch_events_with_pagination_slices_excess_events(mocker, client: Client): """Tests fetch_events_with_pagination slices excess events.""" mocker.patch.object(Config, "MAX_PAGE_SIZE", 10) page1 = [{"id": f"event{i}", "createdAt": f"2024-01-{i:02d}T00:00:00Z"} for i in range(1, 11)] page2 = [{"id": f"event{i}", "createdAt": f"2024-01-{i:02d}T00:00:00Z"} for i in range(11, 16)] mocker.patch.object( client, "get_alerts", side_effect=[page1, page2], ) events = fetch_events_with_pagination(client, "2024-01-01T00:00:00", None, 12) assert len(events) == 12 assert events[0]["id"] == "event1" assert events[-1]["id"] == "event12" @pytest.mark.parametrize( "created_after,created_before", [ ("2024-01-01T00:00:00", None), ("2024-01-01T00:00:00", "2024-01-02T00:00:00"), ], ) def test_fetch_events_with_pagination_date_parameters(mocker, client: Client, created_after: str, created_before: str | None): """Tests fetch_events_with_pagination passes date parameters correctly.""" mock_get_alerts = mocker.patch.object(client, "get_alerts", return_value=[]) fetch_events_with_pagination(client, created_after, created_before, 10) call_kwargs = mock_get_alerts.call_args[1] assert call_kwargs["created_after"] == created_after if created_before is not None: assert call_kwargs["created_before"] == created_before else: # When None is passed, fetch_events_with_pagination pins it to current UTC time assert call_kwargs["created_before"] is not None def test_fetch_events_with_pagination_pins_created_before(mocker, client: Client): """Tests fetch_events_with_pagination uses the same created_before across all pages when None is passed.""" mocker.patch.object(Config, "MAX_PAGE_SIZE", 2) page1 = [{"id": "event1"}, {"id": "event2"}] page2 = [{"id": "event3"}] mock_get_alerts = mocker.patch.object(client, "get_alerts", side_effect=[page1, page2]) fetch_events_with_pagination(client, "2024-01-01T00:00:00", None, 10) # Verify get_alerts was called twice (two pages) assert mock_get_alerts.call_count == 2 # Verify the same created_before was used for both calls first_call_created_before = mock_get_alerts.call_args_list[0][1]["created_before"] second_call_created_before = mock_get_alerts.call_args_list[1][1]["created_before"] assert first_call_created_before == second_call_created_before assert first_call_created_before is not None # ======================================== # Tests: test_module Command # ======================================== @pytest.mark.parametrize( "should_succeed,mock_return,mock_exception,expected_result", [ (True, [{"id": "test"}], None, "ok"), ( False, None, ContentClientAuthenticationError("Auth failed"), "Authorization Error: Verify API Key, API Secret, and Customer ID.", ), (False, None, DemistoException("Error [500] - Internal Server Error"), None), ], ) def test_test_module_command( mocker, client: Client, should_succeed: bool, mock_return: list[dict[str, Any]] | None, mock_exception: Exception | None, expected_result: str | None, ): """Tests test_module returns 'ok' on success, auth error message, or raises other errors.""" if should_succeed: mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", return_value=mock_return) result = test_module(client) assert result == expected_result elif expected_result: mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", side_effect=mock_exception) result = test_module(client) assert result == expected_result else: mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", side_effect=mock_exception) with pytest.raises(DemistoException, match=r"(?i)internal server error"): test_module(client) # ======================================== # Tests: get_events_command # ======================================== def test_get_events_command_success(mocker, client: Client): """Tests get_events_command returns correct CommandResults when should_push_events=False.""" mock_events = [{"id": "123", "alertRuleName": "test_rule", "createdAt": "2024-01-01T00:00:00Z"}] mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", return_value=mock_events) args = {"start_time": "3 days ago", "limit": "10", "should_push_events": "false"} result = get_events_command(client, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "Uptycs.Alert" assert result.outputs_key_field == "id" assert result.outputs == mock_events def test_get_events_command_with_push_events(mocker, client: Client): """Tests get_events_command pushes events to XSIAM when should_push_events=True.""" mock_events = [ {"id": "123", "alertRuleName": "test_rule", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z"} ] mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", return_value=mock_events) mocker.patch.object(UptycsEventCollector, "enrich_events_for_xsiam") mocker.patch.object(UptycsEventCollector, "send_events_to_xsiam") args = {"start_time": "3 days ago", "limit": "10", "should_push_events": "true"} result = get_events_command(client, args) assert isinstance(result, str) assert "1" in result UptycsEventCollector.enrich_events_for_xsiam.assert_called_once_with(mock_events) # type: ignore[attr-defined] UptycsEventCollector.send_events_to_xsiam.assert_called_once_with( # type: ignore[attr-defined] events=mock_events, vendor=Config.VENDOR, product=Config.PRODUCT ) def test_get_events_command_default_values(mocker, client: Client): """Tests get_events_command uses default values.""" mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", return_value=[]) result = get_events_command(client, {}) assert isinstance(result, CommandResults) assert result.outputs == [] def test_get_events_command_with_end_time(mocker, client: Client): """Tests get_events_command handles end_time parameter.""" mock_fetch = mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", return_value=[]) args = {"start_time": "1 hour ago", "end_time": "now", "should_push_events": "false"} result = get_events_command(client, args) assert isinstance(result, CommandResults) call_args = mock_fetch.call_args assert call_args[0][2] is not None # created_before def test_get_events_command_no_push_when_empty(mocker, client: Client): """Tests get_events_command does not push when no events and should_push_events=True.""" mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", return_value=[]) mock_send = mocker.patch.object(UptycsEventCollector, "send_events_to_xsiam") args = {"start_time": "1 hour ago", "should_push_events": "true"} result = get_events_command(client, args) assert isinstance(result, CommandResults) mock_send.assert_not_called() def test_get_events_command_invalid_start_time(client: Client): """Tests get_events_command raises DemistoException for an unparseable start_time.""" args = {"start_time": "not_a_valid_date_12345", "limit": "10"} with pytest.raises(DemistoException, match="Failed to parse date string"): get_events_command(client, args) # ======================================== # Tests: fetch_events_command # ======================================== @pytest.mark.parametrize( "test_case,last_run,params,mock_events,expected_last_run_time,expected_events_count", [ ( "first_run", {}, {"max_fetch": 100}, [ { "id": "1", "createdAt": "2024-01-01T00:00:00Z", "lastOccurredAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z", }, { "id": "2", "createdAt": "2024-01-02T00:00:00Z", "lastOccurredAt": "2024-01-02T00:00:00Z", "updatedAt": "2024-01-02T00:00:00Z", }, ], "2024-01-02T00:00:00Z", 2, ), ( "with_last_run", {"last_fetch": "2024-01-01T00:00:00", "last_fetched_ids": []}, {"max_fetch": 100}, [ { "id": "3", "createdAt": "2024-01-03T00:00:00Z", "lastOccurredAt": "2024-01-03T00:00:00Z", "updatedAt": "2024-01-03T00:00:00Z", }, ], "2024-01-03T00:00:00Z", 1, ), ], ) def test_fetch_events_command_scenarios( mocker, client: Client, test_case: str, last_run: dict[str, Any], params: dict[str, Any], mock_events: list[dict[str, Any]], expected_last_run_time: str, expected_events_count: int, ): """Tests fetch_events_command under various scenarios.""" mocker.patch.object(demisto, "getLastRun", return_value=last_run) mocker.patch.object(demisto, "setLastRun") mocker.patch.object(demisto, "params", return_value=params) mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", return_value=mock_events) mocker.patch.object(UptycsEventCollector, "enrich_events_for_xsiam") mocker.patch.object(UptycsEventCollector, "send_events_to_xsiam") fetch_events_command(client) demisto.setLastRun.assert_called_once() # type: ignore[attr-defined] call_args = demisto.setLastRun.call_args[0][0] # type: ignore[attr-defined] assert call_args["last_fetch"] == expected_last_run_time UptycsEventCollector.enrich_events_for_xsiam.assert_called_once() # type: ignore[attr-defined] UptycsEventCollector.send_events_to_xsiam.assert_called_once_with( # type: ignore[attr-defined] events=mock_events, vendor=Config.VENDOR, product=Config.PRODUCT ) def test_fetch_events_command_with_deduplication(mocker, client: Client): """Tests fetch_events_command deduplicates events based on last_fetched_ids.""" mock_events = [ { "id": "1", "createdAt": "2024-01-01T00:00:00Z", "lastOccurredAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z", }, { "id": "2", "createdAt": "2024-01-01T00:00:00Z", "lastOccurredAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z", }, { "id": "3", "createdAt": "2024-01-02T00:00:00Z", "lastOccurredAt": "2024-01-02T00:00:00Z", "updatedAt": "2024-01-02T00:00:00Z", }, ] mocker.patch.object(demisto, "getLastRun", return_value={"last_fetch": "2024-01-01T00:00:00", "last_fetched_ids": ["1", "2"]}) mocker.patch.object(demisto, "setLastRun") mocker.patch.object(demisto, "params", return_value={"max_fetch": 100}) mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", return_value=mock_events) mocker.patch.object(UptycsEventCollector, "enrich_events_for_xsiam") mocker.patch.object(UptycsEventCollector, "send_events_to_xsiam") fetch_events_command(client) # Only event 3 should be sent (events 1 and 2 are duplicates) expected_new_events = [mock_events[2]] UptycsEventCollector.enrich_events_for_xsiam.assert_called_once_with(expected_new_events) # type: ignore[attr-defined] def test_fetch_events_command_all_duplicates(mocker, client: Client): """Tests fetch_events_command when all fetched events are duplicates.""" mock_events = [ {"id": "1", "createdAt": "2024-01-01T00:00:00Z", "lastOccurredAt": "2024-01-01T00:00:00Z"}, {"id": "2", "createdAt": "2024-01-01T00:00:00Z", "lastOccurredAt": "2024-01-01T00:00:00Z"}, ] mocker.patch.object(demisto, "getLastRun", return_value={"last_fetch": "2024-01-01T00:00:00", "last_fetched_ids": ["1", "2"]}) mocker.patch.object(demisto, "setLastRun") mocker.patch.object(demisto, "params", return_value={"max_fetch": 100}) mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", return_value=mock_events) mocker.patch.object(UptycsEventCollector, "enrich_events_for_xsiam") mocker.patch.object(UptycsEventCollector, "send_events_to_xsiam") fetch_events_command(client) # No events sent to XSIAM (all duplicates) UptycsEventCollector.enrich_events_for_xsiam.assert_not_called() # type: ignore[attr-defined] UptycsEventCollector.send_events_to_xsiam.assert_not_called() # type: ignore[attr-defined] def test_fetch_events_command_no_events(mocker, client: Client): """Tests fetch_events_command when no events are fetched.""" mocker.patch.object(demisto, "getLastRun", return_value={}) mocker.patch.object(demisto, "setLastRun") mocker.patch.object(demisto, "params", return_value={}) mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", return_value=[]) mocker.patch.object(UptycsEventCollector, "send_events_to_xsiam") fetch_events_command(client) UptycsEventCollector.send_events_to_xsiam.assert_not_called() # type: ignore[attr-defined] def test_fetch_events_command_no_events_first_run_saves_state(mocker, client: Client): """Tests fetch_events_command saves state on first run even with no events.""" mocker.patch.object(demisto, "getLastRun", return_value={}) mocker.patch.object(demisto, "setLastRun") mocker.patch.object(demisto, "params", return_value={}) mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", return_value=[]) fetch_events_command(client) demisto.setLastRun.assert_called_once() # type: ignore[attr-defined] call_args = demisto.setLastRun.call_args[0][0] # type: ignore[attr-defined] assert "last_fetch" in call_args assert call_args["last_fetched_ids"] == [] def test_fetch_events_command_multiple_events_same_last_occurred_at(mocker, client: Client): """Tests fetch_events_command collects IDs at the last_fetch timestamp.""" mock_events = [ { "id": "1", "createdAt": "2024-01-01T00:00:00Z", "lastOccurredAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z", }, { "id": "2", "createdAt": "2024-01-01T00:00:00Z", "lastOccurredAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z", }, { "id": "3", "createdAt": "2024-01-01T00:00:00Z", "lastOccurredAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z", }, ] mocker.patch.object(demisto, "getLastRun", return_value={}) mocker.patch.object(demisto, "setLastRun") mocker.patch.object(demisto, "params", return_value={"max_fetch": 100}) mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", return_value=mock_events) mocker.patch.object(UptycsEventCollector, "enrich_events_for_xsiam") mocker.patch.object(UptycsEventCollector, "send_events_to_xsiam") fetch_events_command(client) call_args = demisto.setLastRun.call_args[0][0] # type: ignore[attr-defined] assert call_args["last_fetch"] == "2024-01-01T00:00:00Z" assert sorted(call_args["last_fetched_ids"]) == ["1", "2", "3"] def test_fetch_events_command_missing_last_occurred_at_no_state_update(mocker, client: Client): """Tests fetch_events_command does not update state when last event is missing lastOccurredAt.""" mock_events = [ {"id": "1", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z"}, ] mocker.patch.object(demisto, "getLastRun", return_value={}) mocker.patch.object(demisto, "setLastRun") mocker.patch.object(demisto, "params", return_value={"max_fetch": 100}) mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", return_value=mock_events) mocker.patch.object(UptycsEventCollector, "enrich_events_for_xsiam") mocker.patch.object(UptycsEventCollector, "send_events_to_xsiam") fetch_events_command(client) # Events should still be sent to XSIAM UptycsEventCollector.send_events_to_xsiam.assert_called_once() # type: ignore[attr-defined] # setLastRun should NOT be called since lastOccurredAt is missing demisto.setLastRun.assert_not_called() # type: ignore[attr-defined] def test_fetch_events_command_last_fetch_uses_last_occurred_at_not_created_at(mocker, client: Client): """Tests fetch_events_command sets last_fetch from lastOccurredAt, not createdAt, when they differ.""" mock_events = [ { "id": "1", "createdAt": "2024-01-01T00:00:00Z", "lastOccurredAt": "2024-01-10T00:00:00Z", "updatedAt": "2024-01-10T00:00:00Z", }, { "id": "2", "createdAt": "2024-01-05T00:00:00Z", "lastOccurredAt": "2024-01-15T00:00:00Z", "updatedAt": "2024-01-15T00:00:00Z", }, ] mocker.patch.object(demisto, "getLastRun", return_value={}) mocker.patch.object(demisto, "setLastRun") mocker.patch.object(demisto, "params", return_value={"max_fetch": 100}) mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", return_value=mock_events) mocker.patch.object(UptycsEventCollector, "enrich_events_for_xsiam") mocker.patch.object(UptycsEventCollector, "send_events_to_xsiam") fetch_events_command(client) call_args = demisto.setLastRun.call_args[0][0] # type: ignore[attr-defined] # last_fetch should be from lastOccurredAt of the last event, NOT createdAt assert call_args["last_fetch"] == "2024-01-15T00:00:00Z" assert call_args["last_fetch"] != "2024-01-05T00:00:00Z" # createdAt of last event assert call_args["last_fetched_ids"] == ["2"] # ======================================== # Tests: Main Function # ======================================== def test_main_invalid_command_fail(mocker, capfd): """Tests main() raises error for invalid command.""" with capfd.disabled(): mocker.patch.object(demisto, "command", return_value="invalid-command") mocker.patch.object( demisto, "params", return_value={ "url": SERVER_URL, "api_key": MOCK_API_KEY, "credentials": {"password": MOCK_API_SECRET}, "customer_id": MOCK_CUSTOMER_ID, }, ) mocker.patch.object(demisto, "args", return_value={}) mocker.patch.object(UptycsEventCollector, "generate_jwt_token", return_value=MOCK_JWT_TOKEN) mock_return_error = mocker.patch("UptycsEventCollector.return_error") UptycsEventCollector.main() mock_return_error.assert_called_once() error_call_args = mock_return_error.call_args[0][0] assert re.search(r"invalid-command", error_call_args, re.IGNORECASE) assert re.search(r"not implemented", error_call_args, re.IGNORECASE) @pytest.mark.parametrize( "command_name", [ "test-module", "uptycs-get-events", "fetch-events", ], ) def test_main_command_success(mocker, command_name: str): """Tests main() executes supported commands successfully.""" mocker.patch.object(demisto, "command", return_value=command_name) mocker.patch.object( demisto, "params", return_value={ "url": SERVER_URL, "api_key": MOCK_API_KEY, "credentials": {"password": MOCK_API_SECRET}, "customer_id": MOCK_CUSTOMER_ID, }, ) mocker.patch.object(demisto, "args", return_value={}) mocker.patch.object(demisto, "getLastRun", return_value={}) mocker.patch.object(UptycsEventCollector, "generate_jwt_token", return_value=MOCK_JWT_TOKEN) mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", return_value=[]) mock_return_results = mocker.patch("UptycsEventCollector.return_results") UptycsEventCollector.main() if command_name in ("test-module", "uptycs-get-events"): mock_return_results.assert_called_once() def test_main_command_execution_error(mocker, capfd): """Tests main() handles command execution errors gracefully.""" with capfd.disabled(): mocker.patch.object(demisto, "command", return_value="uptycs-get-events") mocker.patch.object( demisto, "params", return_value={ "url": SERVER_URL, "api_key": MOCK_API_KEY, "credentials": {"password": MOCK_API_SECRET}, "customer_id": MOCK_CUSTOMER_ID, }, ) mocker.patch.object(demisto, "args", return_value={}) mocker.patch.object(UptycsEventCollector, "generate_jwt_token", return_value=MOCK_JWT_TOKEN) mocker.patch.object(UptycsEventCollector, "fetch_events_with_pagination", side_effect=Exception("API Error")) mock_return_error = mocker.patch("UptycsEventCollector.return_error") UptycsEventCollector.main() mock_return_error.assert_called_once() error_message = mock_return_error.call_args[0][0] assert re.search(r"uptycs-get-events", error_message, re.IGNORECASE) def test_main_parse_params_error(mocker, capfd): """Tests main() handles parameter parsing errors.""" with capfd.disabled(): mocker.patch.object(demisto, "command", return_value="test-module") mocker.patch.object(demisto, "params", return_value={}) mocker.patch.object(demisto, "args", return_value={}) mock_return_error = mocker.patch("UptycsEventCollector.return_error") UptycsEventCollector.main() mock_return_error.assert_called_once() error_message = mock_return_error.call_args[0][0] assert re.search(r"(server url|api key|api secret|customer id) is required", error_message, re.IGNORECASE)