VeloCloudEventCollector

Collects events from the VeloCloud API.

Network Security · VeloCloud

Details

IDVeloCloudEventCollector
ProviderArista Networks
CategoryNetwork Security
From Version6.5.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesXSIAM

README

Collects events from the VeloCloud API.
This integration was integrated and tested with VeloCloud as of 2025-10-06.

Configure VeloCloud Event Collector in Cortex

Parameter Description Required
Server URL   True
Enterprise ID   True
API Key API key for authenticating with Arista VeloCloud False
First fetch timestamp   False
Limit of events per fetch   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.

velocloud-get-events


Get events fro`m Arista VeloCloud

Base Command

velocloud-get-events

Input

Argument Name Description Required
start_time Start time for event query in ISO format or human-readable format (e.g., ‘3 days ago’). Default is 1 day ago. Optional
end_time End time for event query in ISO format or human-readable format (e.g., ‘now’). Default is now. Optional
limit Maximum number of events to retrieve. Default is 100. Optional

Context Output

Path Type Description
VeloCloud.Event.logicalId String Event logical ID
VeloCloud.Event.category String Event category
VeloCloud.Event.event String Event type
VeloCloud.Event.severity String Event severity
VeloCloud.Event.message String Event message
VeloCloud.Event.detail String Event details, usually in JSON string format
VeloCloud.Event.eventTime Date Event timestamp
VeloCloud.Event.created Date Event creation time
VeloCloud.Event.edgeName String Name of the edge
VeloCloud.Event.enterpriseName String Name of the enterprise
VeloCloud.Event.enterpriseUsername String Username associated with the enterprise
VeloCloud.Event.segmentName String Name of the network segment

Command Example

!velocloud-get-events limit=1

{
    "VeloCloud": {
        "Event": {
            "category": "EDGE",
            "created": "2025-11-03T21:02:36.000Z",
            "detail": "{\"last_request_time\":0,\"client_mac\":\"03:11:22:33:44:55\",\"client_ipv4addr\":\"192.168.1.1\",\"hostname\":\"nexusquantum7\",\"os_type\":800,\"os_class\":1,\"os_class_name\":\"OTHER\",\"os_version\":\"\",\"device_type\":\"\",\"os_description\":\"Xerox Printer\",\"dhcp_param_list\":\"6,3,1,15,66,67,13,44,12\",\"segment_id\":0,\"edgeSerialNumber\":\"K7M9N2P5Q8R1\"}",
            "edgeName": "radiancecdnc43",
            "enterpriseName": "ACME Co.",
            "enterpriseUsername": "",
            "event": "EDGE_NEW_DEVICE",
            "eventTime": "2025-11-03T21:02:16.000Z",
            "logicalId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
            "message": "New or updated client device 03:11:22:33:44:55, ip 192.168.1.1, segId 0, hostname nexusquantum7, os Xerox Printer",
            "segmentName": "",
            "severity": "NOTICE"
        }
    }
}

Configuration parameters

  • url — Server URL (required)
  • enterprise_id — Enterprise ID (required)
  • api_key — (required)
  • first_fetch — First fetch timestamp
  • limit — Limit of events per fetch
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (1)

  • velocloud-get-events

    Get events fro`m Arista VeloCloud

"""Unit tests for VeloCloudEventCollector integration."""

import json
import os
from datetime import datetime, timedelta, UTC
from unittest.mock import patch

import pytest

from VeloCloudEventCollector import (
    Client,
    get_events_command,
    test_module as velocloud_test_module,
    fetch_events,
    fetch_events_command,
    velocloud_parse_date_range,
    format_events,
)


def load_test_data(test_data_filename):
    """Load test data from a JSON file.

    Args:
        test_data_filename: Name of the test data file

    Returns:
        Parsed JSON data from the file
    """
    with open(os.path.join("./test_data", test_data_filename)) as f:
        return json.load(f)


@pytest.fixture()
def client(requests_mock):
    """Create a VeloCloud client with mocked requests.

    Args:
        requests_mock: requests_mock fixture

    Returns:
        Configured VeloCloud Client instance
    """
    base_url = "https://tenant123.velocloud.net"
    enterprise_id = "12345678"
    return Client(
        base_url=base_url,
        enterprise_id=enterprise_id,
        verify=False,
        headers={},
        proxy=False,
    )


class TestGetEventsCommand:
    """Test get_events_command functionality."""

    def test_get_events_command_successful(self, requests_mock, client):
        """
        Given:
            - VeloCloud client is configured
        When:
            - Executing get_events_command
        Then:
            - Ensure command returns events with correct structure
        """
        # Load test data
        events_data = load_test_data("output-get-events.json")

        # Mock the API response
        requests_mock.post(
            "https://tenant123.velocloud.net/portal/rest/event/getEnterpriseEvents",
            json={"data": events_data, "metadata": {}},
        )

        # Prepare command arguments
        args = {
            "start_time": "2025-11-02T21:02:42.829772",
            "end_time": "2025-11-03T21:02:42.831369",
            "limit": "100",
        }

        # Execute command
        result = get_events_command(client, args)

        # Assertions
        assert result.outputs_prefix == "VeloCloud.Event"
        assert result.outputs_key_field == "logicalId"
        assert isinstance(result.outputs, list)
        assert len(result.outputs) == 3
        assert result.outputs[0]["event"] == "EDGE_NEW_DEVICE"
        assert result.outputs[0]["category"] == "EDGE"

    def test_get_events_command_with_default_values(self, requests_mock, client):
        """
        Given:
            - VeloCloud client is configured
        When:
            - Executing get_events_command without optional parameters
        Then:
            - Ensure default values are used
        """
        events_data = load_test_data("output-get-events.json")

        requests_mock.post(
            "https://tenant123.velocloud.net/portal/rest/event/getEnterpriseEvents",
            json={"data": events_data, "metadata": {}},
        )

        # Minimal args - let defaults apply
        args = {}

        result = get_events_command(client, args)

        assert result.outputs_prefix == "VeloCloud.Event"
        assert len(result.outputs) == len(events_data)

    def test_get_events_command_empty_response(self, requests_mock, client):
        """
        Given:
            - VeloCloud client is configured
        When:
            - API returns no events
        Then:
            - Ensure command handles empty response gracefully
        """
        requests_mock.post(
            "https://tenant123.velocloud.net/portal/rest/event/getEnterpriseEvents",
            json={"data": [], "metadata": {}},
        )

        args = {"limit": "10"}

        result = get_events_command(client, args)

        assert isinstance(result.outputs, list)
        assert len(result.outputs) == 0


class TestTestModule:
    """Test test_module functionality."""

    def test_test_module_success(self, requests_mock, client):
        """
        Given:
            - VeloCloud client is configured
            - API is accessible
        When:
            - Executing test_module
        Then:
            - Ensure 'ok' is returned
        """
        # Load real sample data and use first event
        sample_events = load_test_data("output-get-events.json")
        first_event = sample_events[0]

        requests_mock.post(
            "https://tenant123.velocloud.net/portal/rest/event/getEnterpriseEvents",
            json={"data": [first_event], "metadata": {}},
        )

        result = velocloud_test_module(client)

        assert result == "ok"

    def test_test_module_authentication_error(self, requests_mock, client):
        """
        Given:
            - VeloCloud client is configured
            - API authentication fails
        When:
            - Executing test_module
        Then:
            - Ensure authentication error message is returned
        """
        # Load real sample data for failed auth event
        failed_auth_event = load_test_data("failed-auth.json")

        requests_mock.post(
            "https://tenant123.velocloud.net/portal/rest/event/getEnterpriseEvents",
            status_code=200,
            json=failed_auth_event,
        )

        result = velocloud_test_module(client)

        assert "Invalid API Token" in result


class TestVelocloudParseDateRange:
    """Test velocloud_parse_date_range functionality."""

    def test_parse_date_range_with_last_run_time(self):
        """
        Given:
            - last_run_time is provided
        When:
            - Calling velocloud_parse_date_range
        Then:
            - Ensure start_time is incremented by 1 second
        """
        first_fetch = "3 days ago"
        last_run_time = "2025-11-02T10:00:00Z"

        start_time, end_time = velocloud_parse_date_range(first_fetch, last_run_time)

        parsed_start = datetime.fromisoformat(start_time)
        # Should be 1 second after last_run_time
        expected = datetime(2025, 11, 2, 10, 0, 1, tzinfo=UTC)
        assert parsed_start == expected

    def test_parse_date_range_without_last_run_time(self):
        """
        Given:
            - last_run_time is empty
        When:
            - Calling velocloud_parse_date_range
        Then:
            - Ensure start_time is based on first_fetch
        """
        first_fetch = "2 days ago"
        last_run_time = ""

        start_time, end_time = velocloud_parse_date_range(first_fetch, last_run_time)

        parsed_start = datetime.fromisoformat(start_time)

        # Start should be approximately 2 days ago
        # Using a larger tolerance since dateparser can have variations
        now = datetime.now(UTC)
        expected_time = now - timedelta(days=2)
        delta = abs((parsed_start - expected_time).total_seconds())
        assert delta < 86400  # Within 1 day of tolerance

    def test_parse_date_range_invalid_last_run_time_falls_back_to_first_fetch(self):
        """
        Given:
            - last_run_time is invalid
        When:
            - Calling velocloud_parse_date_range
        Then:
            - Ensure start_time falls back to first_fetch
        """
        first_fetch = "1 day ago"
        last_run_time = "invalid-date-string"

        start_time, end_time = velocloud_parse_date_range(first_fetch, last_run_time)

        parsed_start = datetime.fromisoformat(start_time)

        # Should fall back to first_fetch (1 day ago)
        now = datetime.now(UTC)
        expected_time = now - timedelta(days=1)
        delta = abs((parsed_start - expected_time).total_seconds())
        assert delta < 86400  # Within 1 day of tolerance

    def test_parse_date_range_invalid_both_uses_default(self):
        """
        Given:
            - Both last_run_time and first_fetch are invalid
        When:
            - Calling velocloud_parse_date_range
        Then:
            - Ensure start_time defaults to 1 day ago
        """
        first_fetch = "completely-invalid"
        last_run_time = ""

        start_time, end_time = velocloud_parse_date_range(first_fetch, last_run_time)

        parsed_start = datetime.fromisoformat(start_time)

        # Should default to 1 day ago
        now = datetime.now(UTC)
        expected_time = now - timedelta(days=1)
        delta = abs((parsed_start - expected_time).total_seconds())
        assert delta < 60

    def test_parse_date_range_end_time_is_now(self):
        """
        Given:
            - Any valid first_fetch
        When:
            - Calling velocloud_parse_date_range
        Then:
            - Ensure end_time is approximately now
        """
        first_fetch = "1 day ago"
        last_run_time = ""

        start_time, end_time = velocloud_parse_date_range(first_fetch, last_run_time)

        parsed_end = datetime.fromisoformat(end_time)
        now = datetime.now(UTC)

        # End time should be within 5 seconds of now
        delta = abs((parsed_end - now).total_seconds())
        assert delta < 5


class TestFormatEvents:
    """Test format_events functionality."""

    def test_format_events_with_event_time(self):
        """
        Given:
            - Events with eventTime field
        When:
            - Calling format_events
        Then:
            - Ensure _time is set to eventTime
        """
        # Load real test data and use first event (has eventTime)
        events_data = load_test_data("output-get-events.json")
        events = [events_data[0]]

        formatted = format_events(events)

        assert len(formatted) == 1
        assert formatted[0]["_time"] == events_data[0]["eventTime"]
        assert formatted[0]["logicalId"] == events_data[0]["logicalId"]
        assert formatted[0]["message"] == events_data[0]["message"]

    def test_format_events_with_created_fallback(self):
        """
        Given:
            - Events with created field instead of eventTime
        When:
            - Calling format_events
        Then:
            - Ensure _time is set to created as fallback
        """
        # Create event with only created field (remove eventTime)
        events_data = load_test_data("output-get-events.json")
        event = events_data[1].copy()
        event_created_time = event["created"]
        del event["eventTime"]
        events = [event]

        formatted = format_events(events)

        assert len(formatted) == 1
        assert formatted[0]["_time"] == event_created_time

    def test_format_events_prioritizes_event_time_over_created(self):
        """
        Given:
            - Events with both eventTime and created fields
        When:
            - Calling format_events
        Then:
            - Ensure eventTime is prioritized
        """
        # Load real test data (has both eventTime and created)
        events_data = load_test_data("output-get-events.json")
        events = [events_data[0]]

        formatted = format_events(events)

        # Should use eventTime, not created
        assert formatted[0]["_time"] == events_data[0]["eventTime"]
        assert formatted[0]["_time"] != events_data[0]["created"]

    def test_format_events_skips_events_without_timestamp(self):
        """
        Given:
            - Mix of events with and without timestamps
        When:
            - Calling format_events
        Then:
            - Ensure events without timestamps are skipped
        """
        # Load real test data and create mix with one event missing timestamps
        events_data = load_test_data("output-get-events.json")
        event_with_timestamp = events_data[0]
        event_without_timestamp = events_data[1].copy()
        del event_without_timestamp["eventTime"]
        del event_without_timestamp["created"]
        event_with_created = events_data[2]

        events = [event_with_timestamp, event_without_timestamp, event_with_created]

        formatted = format_events(events)

        assert len(formatted) == 2
        logical_ids = [e["logicalId"] for e in formatted]
        assert event_with_timestamp["logicalId"] in logical_ids
        assert event_without_timestamp["logicalId"] not in logical_ids
        assert event_with_created["logicalId"] in logical_ids

    def test_format_events_empty_list(self):
        """
        Given:
            - Empty events list
        When:
            - Calling format_events
        Then:
            - Ensure empty list is returned
        """
        events = []

        formatted = format_events(events)

        assert isinstance(formatted, list)
        assert len(formatted) == 0

    def test_format_events_preserves_all_fields(self):
        """
        Given:
            - Events with multiple fields
        When:
            - Calling format_events
        Then:
            - Ensure all original fields are preserved and _time is added
        """
        # Load real test data with all fields
        events_data = load_test_data("output-get-events.json")
        events = [events_data[0]]

        formatted = format_events(events)

        # Verify _time is added using eventTime
        assert formatted[0]["_time"] == events_data[0]["eventTime"]
        # Verify all original fields are preserved
        assert formatted[0]["logicalId"] == events_data[0]["logicalId"]
        assert formatted[0]["category"] == events_data[0]["category"]
        assert formatted[0]["severity"] == events_data[0]["severity"]
        assert formatted[0]["message"] == events_data[0]["message"]
        assert formatted[0]["edgeName"] == events_data[0]["edgeName"]
        assert formatted[0]["event"] == events_data[0]["event"]
        assert formatted[0]["created"] == events_data[0]["created"]


class TestFetchEvents:
    """Test fetch_events functionality."""

    def test_fetch_events_successful(self, requests_mock, client):
        """
        Given:
            - VeloCloud client is configured
            - API returns events
        When:
            - Calling fetch_events
        Then:
            - Ensure events and last_timestamp are returned
        """
        events_data = load_test_data("output-get-events.json")
        expected_new_timestamp = events_data[0].get("eventTime", "")
        requests_mock.post(
            "https://tenant123.velocloud.net/portal/rest/event/getEnterpriseEvents",
            json={"data": events_data, "metadata": {}},
        )

        last_timestamp = ""
        first_fetch = "1 day ago"

        fetched_events, new_timestamp = fetch_events(client, last_timestamp, first_fetch, limit=1000)

        assert isinstance(fetched_events, list)
        assert len(fetched_events) == len(events_data)
        assert new_timestamp == expected_new_timestamp

    def test_fetch_events_with_pagination(self, requests_mock, client):
        """
        Given:
            - VeloCloud client is configured
            - API returns events with pagination metadata
        When:
            - Calling fetch_events
        Then:
            - Ensure pagination is handled correctly
        """
        # Load real pagination test data
        pagination_data = load_test_data("output-fetch-events-pagination.json")
        page1 = pagination_data["page1"]
        page2 = pagination_data["page2"]

        # First request returns first page with nextPageLink
        requests_mock.post(
            "https://tenant123.velocloud.net/portal/rest/event/getEnterpriseEvents",
            [
                {"json": page1},
                {"json": page2},
            ],
        )

        last_timestamp = ""
        first_fetch = "1 day ago"

        fetched_events, new_timestamp = fetch_events(client, last_timestamp, first_fetch, limit=100)

        # Dynamically extract expected logicalIds from pagination data
        expected_events = page1["data"] + page2["data"]
        expected_logicalIds = [event["logicalId"] for event in expected_events]

        assert len(fetched_events) == len(expected_logicalIds)
        for i, event in enumerate(fetched_events):
            assert event["logicalId"] == expected_logicalIds[i]


class TestFetchEventsCommand:
    """Test fetch_events_command functionality."""

    @patch("VeloCloudEventCollector.send_events_to_xsiam")
    @patch("VeloCloudEventCollector.demisto")
    def test_fetch_events_command_successful(self, mock_demisto, mock_send_events, requests_mock, client):
        """
        Given:
            - VeloCloud client is configured
            - demisto functions are mocked
        When:
            - Executing fetch_events_command
        Then:
            - Ensure events are sent to XSIAM and last run is updated
        """
        events_data = load_test_data("output-get-events.json")

        requests_mock.post(
            "https://tenant123.velocloud.net/portal/rest/event/getEnterpriseEvents",
            json={"data": events_data, "metadata": {}},
        )

        # Setup demisto mocks
        mock_demisto.params.return_value = {
            "first_fetch": "1 day ago",
            "max_fetch": 1000,
        }
        mock_demisto.getLastRun.return_value = {}

        fetch_events_command(client)

        # Verify send_events_to_xsiam was called
        assert mock_send_events.called
        # Verify setLastRun was called
        assert mock_demisto.setLastRun.called

    @patch("VeloCloudEventCollector.send_events_to_xsiam")
    @patch("VeloCloudEventCollector.demisto")
    def test_fetch_events_command_with_last_run(self, mock_demisto, mock_send_events, requests_mock, client):
        """
        Given:
            - VeloCloud client is configured
            - Last run timestamp exists
        When:
            - Executing fetch_events_command
        Then:
            - Ensure only new events are fetched
        """
        events_data = load_test_data("output-get-events.json")

        requests_mock.post(
            "https://tenant123.velocloud.net/portal/rest/event/getEnterpriseEvents",
            json={"data": events_data, "metadata": {}},
        )

        last_run_timestamp = "2025-11-02T10:00:00Z"
        mock_demisto.params.return_value = {
            "first_fetch": "1 day ago",
            "max_fetch": 1000,
        }
        mock_demisto.getLastRun.return_value = {"last_event_time": last_run_timestamp}

        fetch_events_command(client)

        assert mock_send_events.called
        assert mock_demisto.setLastRun.called

    @patch("VeloCloudEventCollector.send_events_to_xsiam")
    @patch("VeloCloudEventCollector.demisto")
    def test_fetch_events_command_no_events(self, mock_demisto, mock_send_events, requests_mock, client):
        """
        Given:
            - VeloCloud client is configured
            - API returns no events
        When:
            - Executing fetch_events_command
        Then:
            - Ensure command handles empty response gracefully
        """
        requests_mock.post(
            "https://tenant123.velocloud.net/portal/rest/event/getEnterpriseEvents",
            json={"data": [], "metadata": {}},
        )

        mock_demisto.params.return_value = {
            "first_fetch": "1 day ago",
            "max_fetch": 1000,
        }
        mock_demisto.getLastRun.return_value = {}

        fetch_events_command(client)

        # setLastRun should still be called even with no events
        assert mock_demisto.setLastRun.called