ThousandEyes
This Integration is used to to fetch-incidents via "Active alerts", get alert details via "Alert details", and get the "Agent list".
Vulnerability Management · ThousandEyes
Details
| ID | ThousandEyes |
|---|---|
| Provider | Cisco Systems |
| Category | Vulnerability Management |
| From Version | 6.2.0 |
| Docker Image | demisto/python3:3.12.8.3296088 |
| Supported Modules | Agentix XSIAM |
README
This Integration is used to fetch-incidents via “Active alerts”, get alert details via “Alert details”, and get the “Agent list”.
It was integrated and tested with API v6 of ThousandEyes.
Configure ThousandEyes in Cortex
| Parameter | Required |
|---|---|
| Base API URL | True |
| Password | True |
| Fetch incidents | False |
| Incidents Fetch Interval | False |
| Incident type | False |
| Trust any certificate (not secure) | False |
| Use system proxy settings | False |
| First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days, 3 months, 1 year) | False |
| Minimum Severity to filter out the fetched alerts (only applicable for incidents) | 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.
thousandeyes-get-alerts
Fetches all the alerts.
Base Command
thousandeyes-get-alerts
Input
| Argument Name | Description | Required |
|---|---|---|
| aid | AID to fetch Active Alerts from. | Optional |
| from_date | Explicit start date to fetch Alerts from. | Optional |
| to_date | Explicit end date to fetch Alerts to. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ThousandEyes.Alerts.AlertID | Integer | unique ID of the alert; each alert occurrence is assigned a new unique ID |
| ThousandEyes.Alerts.Active | Integer | 0 for inactive, 1 for active, 2 for disabled. Alert is disabled if either alert rule itself has been deleted or the test it is applied to has been disabled, deleted, disabled alerting, or disassociated the alert rule from the test |
| ThousandEyes.Alerts.Agents | Unknown | array of monitors where the alert has at some point been active since the point that the alert was triggered. Not shown on BGP alerts. |
| ThousandEyes.Alerts.AID | Integer | Unique identifier of the Group AID |
| ThousandEyes.Alerts.DateStart | Unknown | the date/time where an alert rule was triggered, expressed in UTC |
| ThousandEyes.Alerts.ApiLinks | Unknown | list of hyperlinks to other areas of the API |
| ThousandEyes.Alerts.PermaLink | String | hyperlink to alerts list, with row expanded |
| ThousandEyes.Alerts.RuleExpression | String | string expression of alert rule |
| ThousandEyes.Alerts.RuleID | Integer | unique ID of the alert rule |
| ThousandEyes.Alerts.RuleName | String | name of the alert rule |
| ThousandEyes.Alerts.TestID | Integer | unique ID of the test |
| ThousandEyes.Alerts.TestName | String | name of the test |
| ThousandEyes.Alerts.ViolationCount | Integer | number of sources currently meeting the alert criteria |
| ThousandEyes.Alerts.Type | Integer | type of alert being triggered |
| ThousandEyes.Alerts.Severity | Integer | field with one of the following values: INFO, MAJOR, MINOR, CRITICAL for all alert types |
thousandeyes-get-alert
Fetches a given alert.
Base Command
thousandeyes-get-alert
Input
| Argument Name | Description | Required |
|---|---|---|
| alert_id | Alert ID to fetch. | Required |
Context Output
There is no context output for this command.
thousandeyes-get-agents
Fetches all agents.
Base Command
thousandeyes-get-agents
Input
| Argument Name | Description | Required |
| — | — | — |
Context Output
| Path | Type | Description |
|---|---|---|
| ThousandEyes.Agents.AgentID | Integer | unique ID of agent |
| ThousandEyes.Agents.AgentName | String | display name of the agent |
| ThousandEyes.Agents.AgentType | String | Cloud, Enterprise or Enterprise Cluster, shows the type of agent |
| ThousandEyes.Agents.CountryID | String | ISO-3166-1 alpha-2 country code of the agent |
| ThousandEyes.Agents.Enabled | Boolean | 1 for enabled, 0 for disabled (Enterprise Agents only) |
| ThousandEyes.Agents.KeepBrowserCache | Boolean | 1 for enabled, 0 for disabled (Enterprise Agents and Enterprise Clusters only) |
| ThousandEyes.Agents.VerifySslCertificates | Boolean | 1 for enabled, 0 for disabled (Enterprise Agents and Enterprise Clusters only) |
| ThousandEyes.Agents.IpAdresses | Unknown | array of ipAddress entries |
| ThousandEyes.Agents.LastSeen | Unknown | yyyy-MM-dd hh:mm:ss, expressed in UTC (Enterprise Agents only) |
| ThousandEyes.Agents.Location | String | location of the agent |
| ThousandEyes.Agents.Network | String | name of the autonomous system in which the Agent is found (Enterprise Agents only) |
| ThousandEyes.Agents.Prefix | String | Network prefix, expressed in CIDR format (Enterprise Agents only) |
| ThousandEyes.Agents.PublicIpAddresses | Unknown | array of ipAddress entries |
| ThousandEyes.Agents.TargetForTests | String | target IP address or domain name representing test destination when agent is acting as a test target in an agent-to-agent test (Enterprise Agents only) |
| ThousandEyes.Agents.AgentState | String | Online, Offline or Disabled (standalone Enterprise Agents only) |
| ThousandEyes.Agents.Utilization | Integer | shows overall utilization percentage (online Enterprise Agents and Enterprise Clusters only) |
| ThousandEyes.Agents.IPv6Policy | String | IP version policy, can be FORCE_IPV4, PREFER_IPV6 or FORCE_IPV6 (Enterprise Agents and Enterprise Clusters only) |
| ThousandEyes.Agents.Hostname | String | fully qualified domain name of the agent (Enterprise Agents only) |
| ThousandEyes.Agents.CreatedDate | Unknown | yyyy-MM-dd hh:mm:ss, expressed in UTC. For Enterprise Clusters, this equals to the createdDate value of the initial cluster member before the conversion to cluster was performed (Enterprise Agents and Enterprise Clusters only) |
| ThousandEyes.Agents.ErrorDetails | Unknown | if an enterprise agent or a cluster member presents at least one error, the errors will be shown as an array of entries in the errorDetails field (Enterprise Agents and Enterprise Cluster members only) |
thousandeyes-get-agent
Fetches a given agent.
Base Command
thousandeyes-get-agent
Input
| Argument Name | Description | Required |
|---|---|---|
| agent_id | Agent ID to fetch. | Required |
Context Output
There is no context output for this command.
Configuration parameters
base_url— Base API URL (required)credentials— API Token (required)isFetch— Fetch incidentsincidentFetchInterval— Incidents Fetch IntervalincidentType— Incident typeinsecure— Trust any certificate (not secure)proxy— Use system proxy settingsfetch_time— First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days, 3 months, 1 year)severity— Minimum Severity to filter out the fetched alerts (only applicable for incidents)
Commands (4)
-
thousandeyes-get-agentFetches a given agent.
-
thousandeyes-get-agentsFetches all agents.
-
thousandeyes-get-alertFetches a given alert.
-
thousandeyes-get-alertsFetches all the alerts.
import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 import json import time import traceback from datetime import datetime import dateparser import urllib3 from requests import Response # Disable insecure warnings urllib3.disable_warnings() """ Global Variables """ INTEGRATION_NAME = "ThousandEyes" INTEGRATION_COMMAND_NAME = "thousandeyes" INTEGRATION_CONTEXT_NAME = "ThousandEyes" API_VERSION_ENDPOINT = "/v6" THOUSAND_EYES_DATE_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" MAX_INCIDENTS_TO_FETCH = 200 class Client(BaseClient): def handle_rate_lmiit_and_make_request( self, method: str, url_suffix: str, params: dict | None = None, data: dict | None = None, json_data: dict | None = None, timeout: float = 10, resp_type: str = "response", test_module: bool = False, ) -> Response | dict: """ Handles the reached rate limit, performs API request to the specified endpoint and reutrns the full Response object Args: method (str) required: The HTTP method, for example, GET, POST, and so on. url_suffix (str) required: The API endpoint. params (dict): URL parameters to specify the query. Default is None. data (dict): The data to send in a 'POST' request. Default is None. json_data (dict): The dictionary to send in a 'POST' request. Default is None. timeout: (float): Time (in seconds) for the client to wait to establish a connection before a timeout occurs. Default is 10. resp_type (str): Determines which data format to return from the HTTP request. Other options are 'text', 'content', 'xml' or 'response'. Use 'response' to return the full response object. Default is response. test_module (bool): Boolean flag to check if function is called from test-module or not Returns: Either a Response Object or Dictionary, depending on the resp_type value """ if test_module: response = self._http_request(method=method, url_suffix=url_suffix, resp_type=resp_type) return response try: while True: response = self._http_request( method=method, url_suffix=url_suffix, params=params, data=data, json_data=json_data, timeout=timeout, resp_type=resp_type, ) response_headers = response.headers org_rate_limit_remaining = response_headers.get("x-organization-rate-limit-remaining") org_rate_limit_reset = response_headers.get("x-organization-rate-limit-reset") if response.status_code == 200 and int(org_rate_limit_remaining) > 0: return response elif org_rate_limit_remaining == 0: current_timestamp = int(time.time()) time.sleep(org_rate_limit_reset - current_timestamp) continue else: status_code = response.status_code msg = response.text err_response = {"Status Code": status_code, "Err Message": msg} return err_response except Exception as e: raise Exception(e) def test_module_command(client: Client, *_) -> str: """ Checks to see if the connection is working as expected or not Args: client (Client Object): Client object with request. Returns: 'ok' if test successful. Raises: DemistoException: If test failed. """ endpoint: str = "/tests.json" method: str = "GET" response = client.handle_rate_lmiit_and_make_request(method=method, url_suffix=endpoint, test_module=True) if response.status_code == 200: # type: ignore return "ok" raise DemistoException(f"Test module failed, {response.text}") # type: ignore """ Helper Functions """ def fetch_all_aids(client: Client) -> list | dict: """ Fetches all AIDs configured in ThousandEyes Args: client (Client Object): Client object with request. Returns: List of Group AIDs """ endpoint = "/account-groups.json" method = "GET" # Hits the API in loop case the rate limit is reached. response = client.handle_rate_lmiit_and_make_request(method, endpoint) # If Response is an error, return the error if isinstance(response, dict): return response group_list = response.json().get("accountGroups") group_aid_list = [sub["aid"] for sub in group_list] return group_aid_list def parse_agent_response_helper(raw_response: dict) -> tuple[list, list]: """ Prepare the context and human readable data for Agents Args: raw_response (dict): Raw API response Returns: Tuple of Context Data and Human Readable Data. """ entry_context = [] human_readable = [] if raw_response: agents_list: list | None = raw_response.get("agents") if agents_list: for items in agents_list: item = assign_params( **{ "Agent ID": items.get("agentId"), "Agent Name": items.get("agentName"), "Agent Type": items.get("agentType"), "Country ID": items.get("countryId"), "Enabled": items.get("enabled"), "Keep Browser Cache": items.get("keepBrowserCache"), "Verify SSL Certificates": items.get("verifySslCertificates"), "Ip Adresses": items.get("ipAdresses"), "Last Seen": items.get("lastSeen"), "Location": items.get("location"), "Network": items.get("network"), "Prefix": items.get("prefix"), "Public IP Addresses": items.get("publicIpAddresses"), "Target For Tests": items.get("targetForTests"), "Agent State": items.get("agentState"), "Utilization": items.get("utilization"), "IPv6 Policy": items.get("ipv6Policy"), "Hostname": items.get("hostname"), "Created Date": items.get("createdDate"), "Error Details": items.get("errorDetails"), } ) human_readable.append(item) entry_context.append(item) return (entry_context, human_readable) def get_alerts_helper(alert_info: dict, aid: int | None, human_readable: bool) -> tuple[dict, dict]: """ Prepare the context and human readable data for Alerts Args: alert_info (Dict): Dict information of an Active Alert aid (int (Optional)): AID to prepare data for human_readable (boolean): Flag to prepare human readable data or not Returns: Tuple of Context Data and Human Readable Data. """ entry_context = {} human_readable_data = {} if alert_info: prepared_alert_info = { "Active": alert_info.get("active"), "Agents": alert_info.get("agents"), "AID": aid, "AlertID": alert_info.get("alertId"), "DateStart": alert_info.get("dateStart"), "ApiLinks": alert_info.get("apiLinks"), "PermaLink": alert_info.get("permalink"), "RuleExpression": alert_info.get("ruleExpression"), "RuleID": alert_info.get("ruleId"), "RuleName": alert_info.get("ruleName"), "TestID": alert_info.get("testId"), "TestName": alert_info.get("testName"), "ViolationCount": alert_info.get("violationCount"), "Type": alert_info.get("type"), "Severity": alert_info.get("severity"), } if human_readable: human_readable_data = assign_params(**prepared_alert_info) entry_context = assign_params(**prepared_alert_info) return (entry_context, human_readable_data) def parse_alerts_response( client: Client, method: str, response: Response | dict, aid: int | None, human_readable: bool = False ) -> tuple[bool, list, list, list]: """ Parses the fetched alerts and looks for information in additional pages (if any) Args: client (Client): Client object with the request. method (str): HTTP method to use response (Response, Dict): Response Object/Error Dict returned from first call for fetching alert of respective AID aid (int (Optional)): Group AID of which we are fetching the active alerts human_readable (bool): Flag to switch the parsing of human_readable data ON/OFF Returns: Tuple of (alerts_found (bool), raw_response (List[Dict]), entry_context (List[Dict]), human_readable_data (List[Dict])) """ raw_response: list[dict] = [] entry_context: list[dict] = [] human_readable_data: list[dict] = [] # Handle parsing results from multiple pages if any if isinstance(response, requests.models.Response): result = response.json() page_info = result.get("pages") next_url = page_info.get("next") alerts_list = result.get("alert") alerts_found = bool(alerts_list) while True: if alerts_found: for alert in alerts_list: raw_response.append(alert) entry_context_temp, human_readable_data_temp = get_alerts_helper(alert, aid, human_readable) entry_context.append(entry_context_temp) human_readable_data.append(human_readable_data_temp) if not next_url: break response = client.handle_rate_lmiit_and_make_request(method, next_url) # Raise exception if error is returned if isinstance(response, dict): raise Exception(response) # Untested parsing # Need to see actual API response # Made based on the assumption that response returned on next page is same as previous result = response.json() page_info = result.get("pages") next_url = page_info.get("next") alerts_list = result.get("alert") alerts_found = bool(alerts_list) alerts_found = bool(entry_context) return (alerts_found, raw_response, entry_context, human_readable_data) def parse_agent_response(response: Response): """ Set the fetched agent details to context and parse the results to warroom Args: response (Response): Response object from the API call Returns: CommandResults object containing required details to send to warroom """ if response: result = response.json() if result.get("agents"): title: str = f"{INTEGRATION_NAME} - Agents Output" entry_context, human_readable = parse_agent_response_helper(result) if entry_context: results = CommandResults( readable_output=tableToMarkdown(title, t=human_readable), outputs_prefix="ThousandEyes.Agents", outputs_key_field="AgentId", outputs=entry_context, raw_response=result, ) return results else: message = f"{INTEGRATION_NAME} - No Agent information found" results = CommandResults(readable_output=message) return results err_message = f"{INTEGRATION_NAME} - Could not find any results for the given query" results = CommandResults(readable_output=err_message) return results def filter_out_alerts_above_minimum_severity(raw_response_list: list, minimum_severity: str) -> list: """ Filters out alerts which are equal and below the provided minimum severity value Args: raw_response_list (List): List of Alert objects from the API response minimum_severity (str): Minimum severity value above which alerts need to be returned Returns: List of events/raw responses (meeting the filter criteria) to create """ severity_to_rank_mapping = {"INFO": 1, "MINOR": 2, "MAJOR": 3, "CRITICAL": 4} rank_to_severity_mapping = {1: "INFO", 2: "MINOR", 3: "MAJOR", 4: "CRITICAL"} minimum_severity_ranking: int | None = severity_to_rank_mapping.get(minimum_severity) # Can return None in case of Severity classes are modified by Thousand Eyes API if not minimum_severity_ranking: raise KeyError( "Severity classes are modified by ThousandEyes API, " "please modify the filter_out_alerts_above_minimum_severity() function accordingly" ) whitelisted_severity_ranks: list = [i for i in rank_to_severity_mapping if i >= minimum_severity_ranking] events_to_create = [] for raw_response in raw_response_list: severity_in_response: str = raw_response.get("severity") severity_ranking: int | None = severity_to_rank_mapping.get(severity_in_response) if severity_ranking in whitelisted_severity_ranks: events_to_create.append(raw_response) return events_to_create """ Command Functions """ def fetch_incidents_command( client: Client, max_results: int, last_run: dict, first_fetch_time: str, minimum_severity: str ) -> tuple[dict, list]: """ Fetches the events and returns the incidents list. Args: client (Client): Client object with the request. max_results (int): maximum number of events to fetch. last_run (Dict[str, str]): The latest incident creation time. first_fetch_time (Optional[str]): If last_run is None then fetch all incidents since first_fetch_time minimum_severity (str): Flag to filter out data based on severity of the alert Returns: Tuple of next run time dict and the list of incident objects """ # Handling First fetch time if isinstance(last_run, dict) and last_run.get("start_time"): # Should return datetime str representation in %Y-%m-%dT%H:%M:%S format last_fetch = last_run.get("start_time") else: last_fetch = dateparser.parse(first_fetch_time).strftime(THOUSAND_EYES_DATE_TIME_FORMAT) # type: ignore[union-attr] incidents_to_create: list = [] incident_created_time: str = "" events_to_create: list = get_alerts_command(client=client, from_date=last_fetch, fetch_for_incident=True) events_to_create: list = filter_out_alerts_above_minimum_severity(events_to_create, minimum_severity) for event in events_to_create: incident_created_time = datetime.utcnow().strftime(THOUSAND_EYES_DATE_TIME_FORMAT + "Z") incident = { "name": f"ThousandEyes Incident - {event.get('ruleName')} - {event.get('dateStart')}", "occured": incident_created_time, "rawJSON": json.dumps(event), } incidents_to_create.append(incident) # Update last run and remove the last 'Z' from datetime string last_fetch = incident_created_time[:-1] next_run = {"start_time": last_fetch} return next_run, incidents_to_create def get_alerts_command( client: Client, aid: int | None = None, from_date: str | None = None, to_date: str | None = None, fetch_for_incident: bool = False, ): """ Fetches live alerts Args: client (Client Object): Client object with request. aid (int) (Optional): Fetch Active Alerts for a specific AID. from_date (str) (Optional): Explicit start date to fetch Alerts from. to_date (str) (Optional): Explicit end date to fetch Alerts to. fetch_for_incident (Boolean) (Default - False): Boolean flag to specify if the alerts are fetched for incidents or not Returns: Results of CommandResults type or List of raw_response (if called from fetch_incidents_command() function) """ endpoint = "/alerts.json" method = "GET" raw_response_list: list[dict] = [] entry_context: list[dict] = [] human_readable_data: list[dict] = [] params: dict = {} if aid and not from_date and not to_date: params = {"aid": aid} elif aid and from_date and not to_date: params = {"aid": aid, "from": from_date} elif aid and from_date and to_date: params = {"aid": aid, "from": from_date, "to": to_date} if fetch_for_incident: params = {"from": from_date} group_aid_list = fetch_all_aids(client) if isinstance(group_aid_list, dict): err_msg = group_aid_list raise Exception(err_msg) for group_aid in group_aid_list: response = client.handle_rate_lmiit_and_make_request(method, endpoint, params=params) alerts_found, raw_response_temp_list, entry_context_temp, human_readable_data_temp = parse_alerts_response( client=client, method=method, response=response, aid=group_aid, human_readable=False ) for raw_response_temp in raw_response_temp_list: raw_response_list.append(raw_response_temp) return raw_response_list # Fetch results for non-incident calls made (From Warroom) response = client.handle_rate_lmiit_and_make_request(method, endpoint, params=params) alerts_found, raw_response_temp_list, entry_context_temp, human_readable_data_temp = parse_alerts_response( client=client, method=method, response=response, aid=aid, human_readable=True ) raw_response_list = raw_response_temp_list entry_context = entry_context_temp human_readable_data = human_readable_data_temp if alerts_found: if aid: title = f"{INTEGRATION_NAME} - Active Alerts found for AID: {aid}" else: title = f"{INTEGRATION_NAME} - Active Alerts found for AID: Default" else: if aid: title = f"{INTEGRATION_NAME} - No Active Alerts Found for AID: {aid}" else: title = f"{INTEGRATION_NAME} - No Active Alerts found for AID: Default" results = CommandResults( readable_output=tableToMarkdown(title, t=human_readable_data), outputs_prefix="ThousandEyes.Alerts", outputs_key_field="AlertId", outputs=entry_context, raw_response=raw_response_list, ) return results def get_alert_command(client: Client, alert_id: int): """ Fetch a given alert. Args: client (Client Object): Client object with request. alert_id (int): Alert ID to fetch. Returns: Tuple of Context Data and Human Readable Data. """ endpoint = f"/alerts/{alert_id}.json" method = "GET" response = client.handle_rate_lmiit_and_make_request(method, endpoint) # If Response is an error, return the error if isinstance(response, dict): return response return response.json() def get_agents_command(client: Client): """ Fetches all agents Args: client (Client Object): Client object with request. Returns: Tuple of Context Data and Human Readable Data. """ endpoint = "/agents.json" method = "GET" response = client.handle_rate_lmiit_and_make_request(method, endpoint) # If Response is an error, return the error if isinstance(response, dict): return response return parse_agent_response(response) def get_agent_command(client: Client, agent_id: int): """ Fetches a given agent. Args: client (Client Object): Client object with request. agent_id (int): Agent ID to fetch. Returns: Tuple of Context Data and Human Readable Data. """ endpoint = f"/agents/{agent_id}.json" method = "GET" response = client.handle_rate_lmiit_and_make_request(method, endpoint) # If Response is an error, return the error if isinstance(response, dict): return response return response.json() def main(): # Fetching required parameters params = demisto.params() base_url = params.get("base_url") + API_VERSION_ENDPOINT bearer_token = params.get("credentials").get("password") headers = {"Authorization": f"Bearer {bearer_token}", "Accept": "application/json"} verify_ssl = not params.get("insecure", False) proxy = params.get("proxy") # Initializing the Client Object with required configuration client = Client(base_url=base_url, verify=verify_ssl, proxy=proxy, headers=headers) command = demisto.command() demisto.debug(f"Command being called is {command}") commands = { "test-module": test_module_command, f"{INTEGRATION_COMMAND_NAME}-get-alerts": get_alerts_command, f"{INTEGRATION_COMMAND_NAME}-get-alert": get_alert_command, f"{INTEGRATION_COMMAND_NAME}-get-agents": get_agents_command, f"{INTEGRATION_COMMAND_NAME}-get-agent": get_agent_command, } try: if command in commands: results = commands[command](client=client, **demisto.args()) # type: ignore return_results(results) elif command == "fetch-incidents": max_results = arg_to_number(arg=demisto.params().get("max_fetch"), arg_name="max_fetch", required=False) first_fetch_timestamp = params.get("fetch_time", "3 days").strip() minimum_severity = params.get("severity") if not max_results or max_results > MAX_INCIDENTS_TO_FETCH: max_results = MAX_INCIDENTS_TO_FETCH next_run, incidents_to_create = fetch_incidents_command( client=client, max_results=max_results, last_run=demisto.getLastRun(), first_fetch_time=first_fetch_timestamp, minimum_severity=minimum_severity, ) demisto.setLastRun(next_run) demisto.incidents(incidents_to_create) else: raise NotImplementedError(f"{command} is not an existing ThousandEyes command") except Exception as e: demisto.error(traceback.format_exc()) # print the traceback err_msg = f"Error in {INTEGRATION_NAME} Integration [{e}]" return_error(err_msg, error=e) if __name__ in ("__main__", "__builtin__", "builtins"): main()