CiscoThousandEyes
This is the Cisco ThousandEyes event collector integration for Cortex XSIAM.
Data Enrichment & Threat Intelligence · Cisco ThousandEyes
Details
| ID | CiscoThousandEyes |
|---|---|
| Provider | Cisco Systems |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 8.4.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | XSIAM |
README
Cisco ThousandEyes Integration for Cortex XSIAM
This is the Cisco ThousandEyes event collector integration for Cortex XSIAM. This integration enables you to fetch events such as alerts and audit logs from Cisco ThousandEyes and process them within Cortex XSIAM.
This integration was developed and tested with version 1.0.0 of Cisco ThousandEyes.
Configure CiscoThousandEyes in Cortex XSIAM
- Navigate to Settings > Configurations > Automation & Feed Integrations.
- Search for CiscoThousandEyes.
- Click Add instance to create and configure a new integration instance.
| Parameter | Description | Required |
|---|---|---|
| Server URL | The URL of the Cisco ThousandEyes server (e.g., https://api.thousandeyes.com). |
True |
| User API Token | The API token for authenticating with Cisco ThousandEyes. | True |
| The maximum number of audit events per fetch | Maximum number of audit events to retrieve per fetch cycle. Default is 50. | False |
| The maximum number of alerts per fetch | Maximum number of alert events to retrieve per fetch cycle. Default is 50. | False |
| Trust any certificate (not secure) | If enabled, the integration will trust self-signed certificates. | False |
| Use system proxy settings | If enabled, the integration will use the system proxy settings. | False |
Note
This API returns a list of activity log events in the current account group.
If the user has View activity log permission for all users in the account group, the logs returned include events across all the account groups they belong to.
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.
cisco-thousandeyes-get-events
Gets events from Cisco ThousandEyes.
Base Command
!cisco-thousandeyes-get-events
Input
| Argument Name | Description | Required |
|---|---|---|
| limit | The number of events to return. Default is 10. | Optional |
| start_date | The start date from which to filter events. | Optional |
| end_date | The end date to which to filter events. | Optional |
| should_push_events | Set this argument to True in order to create events, otherwise the command will only display them. Possible values are: true, false. Default is false. | Optional |
Example Usage
!cisco-thousandeyes-get-events limit="100" start_date="2024-10-10T00:00:00Z" should_push_events=true
Context Output
There is no context output for this command.
Configuration parameters
url— Server URL (required)api_token— (required)max_events_per_fetch— The maximum number of audit events per fetch.max_alerts_per_fetch— The maximum number of alerts per fetch.insecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (1)
-
cisco-thousandeyes-get-eventsGets events from Cisco ThousandEyes.
from urllib.parse import unquote, urlencode import demistomock as demisto import urllib3 from CommonServerPython import * # Disable insecure warnings urllib3.disable_warnings() """ CONSTANTS """ DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" VENDOR = "Cisco" PRODUCT = "ThousandEyes" DEFAULT_MAX_FETCH_ALERT = 2500 DEFAULT_MAX_FETCH_AUDIT_EVENTS = 5000 PAGE_SIZE = 500 DEFAULT_LIMIT = 10 AUDIT = "audit" ALERTS = "alerts" ENDPOINTS = {ALERTS: "/v7/alerts", AUDIT: "/v7/audit-user-events"} DATE_KEYS = {ALERTS: "startDate", AUDIT: "date"} RESPONSE_MAPPING_KEY = {ALERTS: "alerts", AUDIT: "auditEvents"} """ CLIENT CLASS """ class Client(BaseClient): def __init__(self, base_url, headers, verify, proxy): super().__init__(base_url=base_url, headers=headers, verify=verify, proxy=proxy) def get_events( client: Client, fetch_type: str, fetch_limit: int, last_run: dict = {}, start_date: str = "", end_date: str = "" ) -> tuple: """ Fetches events of the specified type (ALERTS or AUDIT) with support for pagination and deduplication. Args: client (Client): API client for making HTTP requests. fetch_type (str): Type of events to fetch. fetch_limit (int): Maximum number of events to fetch. last_run (dict, optional): Metadata from the last fetch, including offset, next page, and last fetch date. start_date (str, optional): Start date for fetching events. end_date (str, optional): End date for fetching events. Returns: tuple: - fetched_events (list[dict]): List of fetched events. - next_run (dict): Metadata for the next fetch, including "last_fetch", "next_page", and "offset". Explanation: - Calculates fetch dates dynamically if not provided. - Handles pagination, deduplication, and enforces fetch limits. - Builds the next run metadata to resume fetching seamlessly. """ start_date, end_date = calculate_fetch_dates(start_date, last_run.get(fetch_type, {}), end_date) last_run = last_run.get(fetch_type, {}) demisto.debug(f"start fetching {fetch_type} type. with last_run: {last_run}") next_page_url = last_run.get("next_page", "") pagination_offset = last_run.get("offset", 0) params = {} if next_page_url else {"startDate": start_date, "endDate": end_date, "max": PAGE_SIZE} fetched_events: list[dict] = [] has_next = True while has_next: has_next = False request_url = ( unquote(next_page_url) if next_page_url else f"{client._base_url}{ENDPOINTS.get(fetch_type, '')}?{unquote(urlencode(params))}" ) response = client._http_request("GET", full_url=request_url) if next_page_url := response.get("_links", {}).get("next", {}).get("href"): has_next = True current_batch_events = response.get(RESPONSE_MAPPING_KEY.get(fetch_type), []) deduplicate_events(current_batch_events, params.get("startDate"), DATE_KEYS.get(fetch_type, "")) fetched_events.extend(current_batch_events[pagination_offset:]) if len(fetched_events) >= fetch_limit: demisto.debug(f"We reached the fetch limit . limit is: {fetch_limit}. received: {len(fetched_events)} events.") fetched_events = fetched_events[:fetch_limit] return fetched_events, prepare_next_run( fetch_type=fetch_type, fetch_limit=fetch_limit, last_batch_events=current_batch_events, fetched_events=fetched_events, request_url=request_url, previous_offset=last_run.get("offset", 0), last_run=last_run, ) pagination_offset = 0 # Events are fetched in descending order by date. # For new fetches (not paginated), use the latest event's date as the "last_fetch". # For paginated fetches, retain the "last_fetch" from the previous batch. last_fetch = ( last_run.get("last_fetch") if last_run.get("next_page") else (fetched_events[0].get(DATE_KEYS.get(fetch_type, "")) if fetched_events else params.get("last_fetch")) ) return fetched_events, {"last_fetch": last_fetch} def prepare_next_run( fetch_type: str, fetch_limit: int, last_batch_events: list[dict], fetched_events: list[dict], request_url: str, previous_offset: int, last_run: dict, ) -> dict: """ Calculates metadata for the next fetch, including the last fetch timestamp, next page URL, and pagination offset. Args: fetch_type (str): Type of events (ALERTS or AUDIT). fetch_limit (int): Maximum number of events to fetch. last_batch_events (list[dict]): Events from the last batch. fetched_events (list[dict]): All fetched events so far. request_url (str): URL of the current request. last_run (dict): Metadata from the previous fetch. Returns: dict: Contains "last_fetch" (str), "next_page" (str), and "offset" (int). Notes: Pagination logic varies by fetch type (ALERTS vs AUDIT). """ previous_page_url = last_run.get("next_page", "") previous_last_date = last_run.get("last_fetch", "") pagination_offset = fetch_limit % len(last_batch_events) next_page_url = request_url if fetch_type == AUDIT or pagination_offset else previous_page_url is_paginated_fetch = is_fetch_paginated(fetch_type, next_page_url, request_url, previous_page_url) return { "last_fetch": (previous_last_date if previous_page_url else fetched_events[0].get(DATE_KEYS.get(fetch_type, ""))), "next_page": next_page_url, "offset": (pagination_offset if is_paginated_fetch else pagination_offset + previous_offset), } def is_fetch_paginated(fetch_type: str, next_page_url: str, request_url: str, previous_page_url: str) -> bool: """ Determines whether the fetch process is paginated between fetches. For 'alerts', checks if the next page URL is different from the current request URL. For other types (e.g., 'events'), checks if the current request URL differs from the previous page URL. Args: fetch_type (str): The type of fetch ('alerts' or 'events'). next_page_url (str): The URL of the next page, if available. request_url (str): The URL of the current request. previous_page_url (str): The URL of the previous request. Returns: bool: True if the fetch is paginated, False otherwise. """ if fetch_type == ALERTS: return next_page_url != request_url return request_url != previous_page_url def deduplicate_events(events: list, start_date: Any, date_key: str) -> None: """ Filters and modifies the given list of events to only include events that occurred after the specified start date. Args: events: A list of event dictionaries. Each dictionary is expected to have the `date_key` containing the event date as a string. start_date: The start date as a string in ISO 8601 format. Events with dates earlier than this will be excluded. If None, the function does nothing. date_key (str): The key in each event dictionary that contains the event's date. Returns: None: The function modifies the `events` list in place. """ if not start_date: return demisto.debug(f"got {len(events)} before deduplication") start_date = arg_to_datetime(start_date) events[:] = [ event for event in events if (event_date := arg_to_datetime(event.get(date_key))) and start_date and event_date > start_date ] demisto.debug(f"got {len(events)} after deduplication") def calculate_fetch_dates(start_date: str, last_run: dict, end_date: str = "") -> tuple[str, str]: """ Calculates the start and end dates for fetching events. This function takes the start date and end date provided as arguments. If these are not provided, it uses the last run information to calculate the start and end dates. If the last run information is also not available, it uses the current time as the end date and the time one minute before the current time as the start date. Args: start_date (str): The start date for fetching events in '%Y-%m-%dT%H:%M:%SZ' format. last_run_key (str): The key to retrieve the last fetch date from the last run dictionary. last_run (dict): A dictionary containing information about the last run. end_date (str, optional): The end date for fetching events in '%Y-%m-%dT%H:%M:%SZ' format. Defaults to "". Returns: tuple: A tuple containing two elements: - The start date as a string in the format '%Y-%m-%dT%H:%M:%SZ'. - The end date as a string in the format '%Y-%m-%dT%H:%M:%SZ'. """ now_utc_time = get_current_time() # argument > last run > current time start_date = start_date or last_run.get("last_fetch") or ((now_utc_time - timedelta(minutes=1)).strftime(DATE_FORMAT)) # argument > current time end_date = end_date or now_utc_time.strftime(DATE_FORMAT) return start_date, end_date def test_module(client: Client) -> str: """ This method is used to test the connectivity and functionality of the client. Args: client (Client): The client object with methods for interacting with the API. Returns: str: Returns "ok" if the client is able to interact with the API successfully, raises an exception otherwise. """ try: fetch_events(client, max_fetch_audits=1, max_fetch_alerts=1) except Exception as e: if "Unauthorized" in str(e): return "Authorization Error: make sure API Key is correctly set" raise e return "ok" def validate_start_and_end_dates(args): """ Validates the start and end dates provided in the arguments. This function checks if the start date is missing or if it is greater than the end date. If either of these conditions is true, it raises a ValueError. Otherwise, it returns the start and end dates. Args: args (dict): A dictionary containing the arguments for the command. It should contain keys 'start_date' and 'end_date' with values representing the date range. Returns: tuple: A tuple containing two elements: - The start date as a string in the format '%Y-%m-%dT%H:%M:%SZ'. - The end date as a string in the format '%Y-%m-%dT%H:%M:%SZ'. Raises: ValueError: If the start date is missing or if it is greater than the end date. """ start_date_str = "" end_date_str = "" if start_date := arg_to_datetime(args.get("start_date")): start_date_str = start_date.strftime(DATE_FORMAT) if end_date := arg_to_datetime(args.get("end_date")): end_date_str = end_date.strftime(DATE_FORMAT) if (end_date and not start_date) or (start_date and end_date and start_date >= end_date): raise ValueError("Either the start date is missing or it is greater than the end date. Please provide valid dates.") return start_date_str, end_date_str def get_events_command(client: Client, args: dict) -> tuple[List[Dict], CommandResults]: start_date, end_date = validate_start_and_end_dates(args) limit = arg_to_number(args.get("limit")) or DEFAULT_LIMIT _, all_events = fetch_events( client=client, max_fetch_alerts=limit, max_fetch_audits=limit, start_date=start_date, end_date=end_date, ) alerts = [item for item in all_events if "id" in item] events = [item for item in all_events if "id" not in item] alerts_table = tableToMarkdown( name="Test Alerts", t=alerts, headers=["SOURCE_LOG_TYPE", "alertType", "startDate", "id", "duration", "suppressed", "meta", "violationCount"], ) event_table = tableToMarkdown( "Test Events", events, ["SOURCE_LOG_TYPE", "aid", "date", "event", "ipAddress", "uid", "user", "accountGroupName"] ) return all_events, CommandResults(readable_output=f"{alerts_table}\n{event_table}", raw_response=all_events) def fetch_events( client: Client, max_fetch_alerts: int, max_fetch_audits: int, start_date: str = "", end_date: str = "", ) -> tuple[Dict, List[Dict]]: """ Fetches alert and audit events from the specified client within the provided date range. Args: client (Client): The client instance to interact with the data source. max_fetch_alerts (int): Maximum number of alerts to fetch per request. max_fetch_audits (int): Maximum number of audit events to fetch per request. start_date (str, optional): The start date for fetching events in ISO 8601 format. Defaults to an empty string. end_date (str, optional): The end date for fetching events in ISO 8601 format. Defaults to an empty string. Returns: tuple[Dict, List[Dict]]: - A dictionary containing the next run information, including timestamps and pagination data. - A list of events (alerts and audits) to be ingested into XSIAM. """ alert_events, audit_events = [], [] alert_next_run, audit_next_run = {}, {} last_run = demisto.getLastRun() is_new_fetch = "nextTrigger" not in last_run if is_new_fetch or last_run.get(ALERTS, {}).get("next_page"): alert_events, alert_next_run = get_events( client, ALERTS, start_date=start_date, end_date=end_date, fetch_limit=max_fetch_alerts, last_run=last_run ) if is_new_fetch or last_run.get(AUDIT, {}).get("next_page"): audit_events, audit_next_run = get_events( client, AUDIT, start_date=start_date, end_date=end_date, fetch_limit=max_fetch_audits, last_run=last_run ) events = alert_events + audit_events add_type_to_events(events) next_run: Dict[str, Any] = {ALERTS: alert_next_run, AUDIT: audit_next_run} if any(d.get("next_page") for d in (alert_next_run, audit_next_run)): next_run["nextTrigger"] = "0" demisto.debug(f"Setting next run {next_run}.") return next_run, events """ MAIN FUNCTION """ def add_time_to_events(events: List[Dict] | None): """ Adds the _time key to the events. Args: events: List[Dict] - list of events to add the _time key to. Returns: list: The events with the _time key. """ if events: for event in events: create_time = arg_to_datetime(arg=event.get("startDate") or event.get("date")) event["_time"] = create_time.strftime(DATE_FORMAT) if create_time else None def add_type_to_events(events: List[Dict]) -> None: """ Adds a source log type to each event in the list based on the provided fetch type. Args: events (List[Dict]): A list of events to be updated. Each event is represented as a dictionary. fetch_type (str): The type of fetch operation, used to determine the corresponding source log type. Returns: None: The function modifies the input list of events in place. Notes: - The `SOURCE_LOG_TYPE` dictionary is expected to map fetch types to their respective source log type strings. - If the `events` list is empty, the function does nothing. """ if events: for event in events: event["SOURCE_LOG_TYPE"] = "Alerts" if "id" in event else "AuditEvents" def main() -> None: # pragma: no cover """ main function, parses params and runs command functions """ params = demisto.params() args = demisto.args() command = demisto.command() api_key = params.get("api_token", {}).get("password") base_url = params.get("url") verify_certificate = not params.get("insecure", True) proxy = params.get("proxy", False) max_alerts_per_fetch = arg_to_number(params.get("max_alerts_per_fetch")) or DEFAULT_MAX_FETCH_ALERT max_events_per_fetch = arg_to_number(params.get("max_events_per_fetch")) or DEFAULT_MAX_FETCH_AUDIT_EVENTS demisto.debug(f"Command being called is {command}") try: headers = {"Authorization": f"Bearer {api_key}"} client = Client(base_url=base_url, verify=verify_certificate, headers=headers, proxy=proxy) if command == "test-module": return_results(test_module(client)) elif command == "cisco-thousandeyes-get-events": should_push_events = argToBoolean(args.get("should_push_events", "false")) events, results = get_events_command(client, demisto.args()) return_results(results) if should_push_events: add_time_to_events(events) send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT) elif command == "fetch-events": next_run, events = fetch_events( client=client, max_fetch_alerts=max_alerts_per_fetch, max_fetch_audits=max_events_per_fetch, ) add_time_to_events(events) send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT) demisto.setLastRun(next_run) # Log exceptions and return errors except Exception as e: return_error(f"Failed to execute {command} command.\nError:\n{e!s}") """ ENTRY POINT """ if __name__ in ("__main__", "__builtin__", "builtins"): main()