from time import sleep from datetime import datetime, timedelta from CommonServerPython import * # noqa # pylint: disable=unused-wildcard-import from CommonServerUserPython import * # noqa from copy import deepcopy import requests import urllib3 # Disable insecure warnings urllib3.disable_warnings() # pylint: disable=no-member """ CONSTANTS """ DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" # ISO8601 format with UTC, default in XSOAR IMPORTANCE_DICTIONARY = {"Low": "0", "Medium": "1", "High": "2"} ONGOING_DICTIONARY = { "Ongoing": "true", "Not Ongoing": "false", } IP_DICTIONARY = {"IPv4": 4, "IPv6": 6} ROUTERS_HR_HEADERS = [ "id", "name", "description", "is_proxy", "license_type", "snmp_authprotocol", "snmp_priv_protocol", "snmp_security_level", "snmp_version", ] MANAGED_OBJECTS_HR_HEADERS = ["id", "name", "tags", "match_type", "match_enabled", "match", "family", "autodetected"] """ CLIENT CLASS """ class NetscoutClient(BaseClient): """Client class to interact with the service API This Client implements API calls, and does not contain any XSOAR logic. Should only do requests and return data. It inherits from BaseClient defined in CommonServer Python. Most calls use _http_request() that handles proxy, SSL verification, etc. """ OPERATOR_NAME_DICTIONARY = { # : "importance": "importance_operator", "start_time": "start_time_operator", "stop_time": "stop_time_operator", } RELATIONSHIP_TO_TYPE = {"routers": "router"} MAX_ALERTS_FOR_FIRST_FETCH = 10000 def __init__( self, base_url, verify, proxy, first_fetch, headers=None, max_fetch=None, look_back=None, alert_class=None, alert_type=None, classification=None, importance=None, ongoing=None, ): self.first_fetch = first_fetch self.max_fetch = max_fetch self.look_back = look_back self.alert_class = alert_class self.alert_type = alert_type self.classification = classification self.importance = importance self.ongoing = ongoing self.importance_operator = ">" super().__init__(base_url=base_url, verify=verify, headers=headers, proxy=proxy) def http_request( self, method: str, url_suffix: Optional[str] = None, params: Optional[dict] = None, json_data: Optional[dict] = None, return_empty_response: Optional[bool] = None, status_list_to_retry: list = None, ): return super()._http_request( method=method, url_suffix=url_suffix, params=params, json_data=json_data, error_handler=self.error_handler, return_empty_response=return_empty_response, status_list_to_retry=status_list_to_retry, ) @staticmethod def error_handler(res: requests.Response): """ Error handler for API calls Args: res (requests.Response): Response to handle error for """ try: # Try to parse json error response error_entry = res.json() error: str = f"Error in API call [{res.status_code}] - {res.reason}" if res.status_code in (400, 422, 404): error_list: list = [] for err in error_entry.get("errors"): # Building the list of errors new_error_source = err.get("source", {}).get("pointer", "").split("/")[-1] new_error_details = err.get("detail") new_error = f"{new_error_source}: {new_error_details}" if new_error_source else new_error_details error_list.append(new_error) # If we manged to build a list of errors use it otherwise use basic information if error_list: error = f"{error}: \n" + "\n".join(error_list) elif res.status_code in (500, 401): message = error_entry.get("errors", [])[0].get("message") if message: error = f"{error}\n{message}" demisto.error(res.text) raise DemistoException(error) except ValueError: raise DemistoException(f"Could not parse error returned from Netscout Arbor Sightline server:\n{str(res.content)}") def build_relationships(self, **kwargs) -> dict: """ Builds the relationships object for creating a mitigation. An example of relationships object is: { "mitigation_template": { "data": { "id": "4", "type": "mitigation_template" } }, "alert": { "data": { "id": "101", "type": "alert" "id": "101", "type": "alert" } } } Args: kwargs (dict): Dict containing key values parameters to be used for relationships. for example: {'ip_version': 4} Returns: (dict): Netscout relationships object """ relationships: dict[str, Any] = {} for key, val in kwargs.items(): if val: # In some cases the name of the relationships is not the same as the type (most cases it is) _type = self.RELATIONSHIP_TO_TYPE.get(key, key) if key == "routers": relationships[key] = {"data": [{"type": _type, "id": val[0]}]} else: relationships[key] = {"data": {"type": _type, "id": val}} return relationships def build_data_attribute_filter(self, attributes_dict: dict) -> str: """ Builds data attribute filter in the NetscoutArbor form. For example: '/data/attributes/importance>1' where key=importance operator='>' and value=1. The function iterates over all arguments (besides operators listed in the OPERATOR_NAME_DICTIONARY) and chain together the 'key operator val' such that the argument name is 'key', its value is 'val' and operator is '=' if no relevant operator is present. In case of multiple parameters the attributes are separated with 'AND'. Args: attributes_dict (dict): Dict containing key values filter parameters. for example: {'importance': 1} Returns: (str): Netscout data attribute filter string. For example: /data/attributes/importance>1 AND /data/attributes/ongoing=true """ param_list = [] operator_names = self.OPERATOR_NAME_DICTIONARY.values() for key, val in attributes_dict.items(): # We don't create a filter for operator names if key not in operator_names and val: operator = "=" # type: str # If the current parameter supports a special operator (it appears in the OPERATOR_NAME_DICTIONARY), # we take the operator value using the operator name (that appears in the OPERATOR_NAME_DICTIONARY) if operator_name := self.OPERATOR_NAME_DICTIONARY.get(key): operator = attributes_dict.get(operator_name, "") if attributes_dict.get(operator_name) else "=" param_list.append(f"/data/attributes/{key + operator + val}") return " AND ".join(param_list) def fetch_incidents(self, params_dict: dict) -> tuple[list, dict]: """Perform fetch incidents process. Args: params_dict (dict): The params configured by the user to perform the fetch with. Returns: (list, dict): List of incidents to save and last run state. """ last_run = demisto.getLastRun() fetch_limit = last_run.get("limit") or self.max_fetch # end_time is now (and not extremely useful), but update_last_run_object wants it. start_time, end_time = get_fetch_run_time_range( last_run=last_run, first_fetch=self.first_fetch, look_back=self.look_back, date_format=DATE_FORMAT, ) # We can only filter using '> start_time' not '>= start_time'. Adjust by one second instead. tmp_start_time = datetime.strptime(start_time, DATE_FORMAT) tmp_start_time - timedelta(seconds=1) start_time = tmp_start_time.strftime(DATE_FORMAT) demisto.debug(f"Fetching incidents between {start_time} and {end_time}") incidents: list = [] time_attributes_dict = assign_params(start_time=start_time, start_time_operator=">") params_dict.update(time_attributes_dict) data_attribute_filter = self.build_data_attribute_filter(params_dict) demisto.debug(f"NetscoutArborSightline fetch params are: search_filter={data_attribute_filter}") # We use the status_list_to_retry since in some rare cases the API returns 500 error on consecutive API # calls. all_alerts = self.fetch_paged_alerts( fetch_limit=fetch_limit, search_filter=data_attribute_filter, status_list_to_retry=[500] ) demisto.debug(f"Got {len(all_alerts)} incidents from the API, before filtering") filtered_alerts = filter_incidents_by_duplicates_and_limit( incidents_res=all_alerts, last_run=last_run, fetch_limit=fetch_limit, id_field="id" ) demisto.debug(f"After filtering, there are {len(filtered_alerts)} incidents") for alert in filtered_alerts: id_ = alert.get("id") attributes = alert.get("attributes", {}) alert_type = attributes.get("alert_type") start_time = attributes.get("start_time") if start_time.endswith("+00:00"): start_time = start_time.removesuffix("+00:00") + "Z" incidents.append( { "name": f"{alert_type}: {id_}", "occurred": start_time, "dbotMirrorId": id_, "rawJSON": json.dumps(alert), } ) last_run = update_last_run_object( last_run=last_run, incidents=incidents, fetch_limit=self.max_fetch, start_fetch_time=start_time, end_fetch_time=end_time, look_back=self.look_back, created_time_field="occurred", id_field="dbotMirrorId", date_format=DATE_FORMAT, increase_last_run_time=False, ) demisto.debug(f"Last run after the fetch run: {last_run}") return incidents, last_run def fetch_incidents_loop(self) -> tuple[list, dict]: """ Calls the fetch incidents function to pull incidents with for each alert_type/alert_class separately. Returns: (list, str): List of incidents to save and string representing the creation time of the latest incident to be saved. """ incidents = [] params_dict = assign_params( alert_class=self.alert_class, alert_type=self.alert_type, importance=self.importance, classification=self.classification, importance_operator=self.importance_operator, ongoing=self.ongoing, ) if self.alert_type: key = "alert_type" class_type_list = self.alert_type elif self.alert_class: key = "alert_class" class_type_list = self.alert_class else: key = "" class_type_list = [] demisto.debug(f"No condition was matched {key=} {class_type_list=}") if self.alert_class or self.alert_type: for item in class_type_list: params_dict[key] = item last_incidents, last_run = self.fetch_incidents(params_dict) incidents += last_incidents sleep(5) else: incidents, last_run = self.fetch_incidents(params_dict) return incidents, last_run def fetch_paged_alerts( self, fetch_limit: int, search_filter: Optional[str] = None, status_list_to_retry: list = None, ) -> list[dict]: """Fetch alerts using API paging and combine the results into a single list.""" # Fetch an initial page to get the metadata about total matching alerts. first_page = self.list_alerts(search_filter=search_filter, status_list_to_retry=status_list_to_retry) meta = first_page.get("meta", {}) pagination = meta.get("pagination", {}) # Pagniation object returns null when no results are returned. total_count = pagination.get("totalCount") or 0 per_page = pagination.get("perPage") or 1 # Jump ahead if we don't need all the pages. first_index = total_count - fetch_limit + 1 first_page_num = first_index // per_page if first_page_num > 1: first_page = self.list_alerts( page=first_page_num, page_size=per_page, search_filter=search_filter, status_list_to_retry=status_list_to_retry ) pages = [first_page] # Loop over all the pages. while True: prev_links = pages[-1].get("links", {}) next_link = prev_links.get("next") if next_link is None: break # Fetch the next page of results. page = self._http_request( method="GET", full_url=next_link, status_list_to_retry=status_list_to_retry, ) pages.append(page) # Reverse the results so the oldest is first. alerts: list[dict] = [] for page in reversed(pages): alerts.extend(reversed(page.get("data", []))) return alerts def list_alerts( self, page: Optional[int] = None, page_size: Optional[int] = None, search_filter: Optional[str] = None, status_list_to_retry: list = None, ) -> dict: return self.http_request( method="GET", url_suffix="alerts/", status_list_to_retry=status_list_to_retry, params=assign_params(page=page, perPage=page_size, filter=search_filter), ) def get_alert(self, alert_id: str) -> dict: return self.http_request(method="GET", url_suffix=f"alerts/{alert_id}") def get_annotations(self, alert_id: str) -> dict: return self.http_request(method="GET", url_suffix=f"alerts/{alert_id}/annotations/") def list_mitigations(self, mitigation_id: str, page: Optional[int] = None, page_size: Optional[int] = None) -> dict: return self.http_request( method="GET", url_suffix=f"mitigations/{mitigation_id}" if mitigation_id else "mitigations", params=assign_params(page=page, perPage=page_size), ) def create_mitigation(self, data: dict) -> dict: return self.http_request(method="POST", url_suffix="mitigations/", json_data=data) def delete_mitigation(self, mitigation_id: str): self.http_request(method="DELETE", url_suffix=f"mitigations/{mitigation_id}", return_empty_response=True) def mitigation_template_list(self) -> dict: return self.http_request(method="GET", url_suffix="mitigation_templates/") def router_list(self) -> dict: return self.http_request(method="GET", url_suffix="routers/") def managed_object_list(self, page: Optional[int] = None, page_size: Optional[int] = None) -> dict: return self.http_request(method="GET", url_suffix="managed_objects/", params=assign_params(page=page, perPage=page_size)) def tms_group_list(self) -> dict: return self.http_request(method="GET", url_suffix="tms_groups/") """ HELPER FUNCTIONS """ def clean_links(target_obj: Union[dict, list]): """ Recursively look for a all keys named 'links' and remove them from the object. Args: target_obj (dict/list): An object to remove the links key from. """ if isinstance(target_obj, dict): remove_keys(target_obj, ["links"]) for val in target_obj.values(): clean_links(val) if isinstance(target_obj, list): for i in target_obj: clean_links(i) def validate_json_arg(json_str: str, arg_name: str) -> dict: """ Parse the json data. If the format is invalid an appropriate exception will be raised Args: json_str (str): The data to parse arg_name (str): The argument name where the data eas given (for exception purposes) Return: (dict): dict representing the given json """ try: sub_object = json.loads(json_str) return sub_object except Exception as err: raise DemistoException( f"The value given in the {arg_name} argument is not a valid JSON format:\n{json_str}\nERROR:\n{err}" ) def remove_keys(obj: dict, keys_to_remove: list): """ Removes the the given keys from a given dict. Args: obj (dict): The object to remove the key from. keys_to_remove (lst): List of keys to remove. """ for key in keys_to_remove: if obj.get(key): del obj[key] def flatten_key(obj: dict, key_to_flatten: str): """ Extract the data inside a given key to the root level of the object. Args: obj (dict): The object to extract the data from. key_to_flatten (str): The key name to extract. """ if sub_dictionary := obj.get(key_to_flatten): for sub_key, sub_val in sub_dictionary.items(): obj[sub_key] = sub_val del obj[key_to_flatten] def build_human_readable(data: dict) -> dict: """ Removes the relationships and subobject data from the object and extracts the data inside attributes to the root level of the object to be displayed nicely in human readable. Args: data (dict): The data to create human readable from. Return: (dict): The same object without the relationships data and with the attributes extracted to the root level. """ hr = deepcopy(data) flatten_key(hr, "attributes") remove_keys(hr, ["relationships", "subobject"]) return hr def build_output(data: dict, extend_data: bool = False, key_to_flat: str = "attributes", keys_to_remove: list = None) -> dict: keys_to_remove = keys_to_remove if keys_to_remove else ["relationships"] data_copy = deepcopy(data) clean_links(data_copy) if key_to_flat: flatten_key(data_copy, key_to_flat) if not extend_data: remove_keys(data_copy, keys_to_remove) return data_copy def cast_importance_to_minimal(importance: str) -> Optional[str]: """ If a minimal importance param was given, cast it to the corresponding minimal value to be used with the '>' operator. That is: High -> '2' -> '1' Medium -> '1' -> '0' Low -> '0' -> None (so it will be ignored and will not be used as an importance param) Args: importance (str): The importance to cast. Returns: (str): The value to be used withh the '>` operator. """ str_importance = IMPORTANCE_DICTIONARY.get(importance) if str_importance and str_importance != "0": return str(int(str_importance) - 1) else: return None """ COMMAND FUNCTIONS """ def test_module(client: NetscoutClient) -> str: client.fetch_incidents_loop() return "ok" def fetch_incidents_command(client: NetscoutClient): incidents, last_run = client.fetch_incidents_loop() demisto.incidents(incidents) demisto.setLastRun(last_run) def list_alerts_command(client: NetscoutClient, args: dict): limit = arg_to_number(args.get("limit")) page = arg_to_number(args.get("page")) alert_id = args.get("alert_id") alert_class = args.get("alert_class") alert_type = args.get("alert_type") classification = args.get("classification") importance = IMPORTANCE_DICTIONARY.get(args.get("importance", "")) importance_operator = args.get("importance_operator") ongoing = args.get("ongoing") if args.get("ongoing") else None start_time = args.get("start_time") start_time_operator = args.get("start_time_operator") stop_time = args.get("stop_time") stop_time_operator = args.get("stop_time_operator") managed_object_id = args.get("managed_object_id") extend_data = argToBoolean(args.get("extend_data", False)) if alert_id: raw_result = client.get_alert(alert_id) else: attributes_dict = assign_params( alert_id=alert_id, alert_class=alert_class, alert_type=alert_type, classification=classification, importance=importance, importance_operator=importance_operator, ongoing=ongoing, start_time=start_time, start_time_operator=start_time_operator, stop_time=stop_time, stop_time_operator=stop_time_operator, ) data_attribute_filter = client.build_data_attribute_filter(attributes_dict) data_relationships_filter = ( f"AND /data/relationships/managed_object/data/id={managed_object_id}" if managed_object_id else "" ) search_filter = data_attribute_filter + data_relationships_filter raw_result = client.list_alerts(page=page, page_size=limit, search_filter=search_filter) data = raw_result.get("data") data = data if isinstance(data, list) else [data] hr = [build_human_readable(data=alert) for alert in data] outputs = [build_output(data=alert, extend_data=extend_data) for alert in data] return CommandResults( outputs_prefix="NASightline.Alert", outputs_key_field="id", outputs=outputs, readable_output=tableToMarkdown("Alerts", hr), raw_response=raw_result, ) def alert_annotation_list_command(client: NetscoutClient, args: dict): alert_id = args.get("alert_id", "") extend_data = argToBoolean(args.get("extend_data", False)) raw_result = client.get_annotations(alert_id) data = raw_result.get("data", []) hr = [build_human_readable(data=annotation) for annotation in data] annotations = [build_output(data=annotation, extend_data=extend_data) for annotation in data] context = {"AlertID": alert_id, "Annotations": annotations} return CommandResults( outputs_prefix="NASightline.AlertAnnotation", outputs_key_field="AlertID", outputs=context, readable_output=tableToMarkdown(f"Alert {alert_id} annotations", hr), raw_response=raw_result, ) def mitigation_list_command(client: NetscoutClient, args: dict): page = arg_to_number(args.get("page")) limit = arg_to_number(args.get("limit")) mitigation_id = args.get("mitigation_id", "") extend_data = argToBoolean(args.get("extend_data", False)) raw_result = client.list_mitigations(mitigation_id, page=page, page_size=limit) data = raw_result.get("data") data = data if isinstance(data, list) else [data] hr = [build_human_readable(data=mitigation) for mitigation in data] mitigations = [ build_output(data=mitigation, keys_to_remove=["relationships", "subobject"], extend_data=extend_data) for mitigation in data ] return CommandResults( outputs_prefix="NASightline.Mitigation", outputs_key_field="id", outputs=mitigations, readable_output=tableToMarkdown("Mitigation list", hr), raw_response=raw_result, ) def mitigation_create_command(client: NetscoutClient, args: dict): ip_version = IP_DICTIONARY.get(args["ip_version"]) if not ip_version: raise DemistoException( 'ip_version value can be one of the following: ' f'{",".join(list(IP_DICTIONARY.keys()))}. {args.get("ip_version")} was given.' ) description = args.get("description") name = args.get("name") ongoing = args.get("ongoing", "false") sub_type = args.get("sub_type") sub_object = validate_json_arg(args["sub_object"], "sub_object") alert_id = args.get("alert_id") managed_object_id = args.get("managed_object_id") mitigation_template_id = args.get("mitigation_template_id") router_ids = argToList(args.get("router_ids")) tms_group_id = args.get("tms_group_id") extend_data = argToBoolean(args.get("extend_data", False)) relationships = client.build_relationships( alert=alert_id, managed_object=managed_object_id, mitigation_template=mitigation_template_id, routers=router_ids, tms_group=tms_group_id, ) attributes = assign_params( description=description, ip_version=ip_version, name=name, ongoing=ongoing, subtype=sub_type, subobject=sub_object ) object_data = {"relationships": relationships, "attributes": attributes} raw_result = client.create_mitigation(data={"data": object_data}) data = raw_result.get("data", {}) hr = build_human_readable(data=data) mitigation = build_output(data=data, extend_data=extend_data) return CommandResults( outputs_prefix="NASightline.Mitigation", outputs_key_field="id", outputs=mitigation, readable_output=tableToMarkdown("Mitigation was created", hr), raw_response=raw_result, ) def mitigation_delete_command(client: NetscoutClient, args: dict[str, str]): mitigation_id = args.get("mitigation_id", "") client.delete_mitigation(mitigation_id) hr = f"### Mitigation {mitigation_id} was deleted" return CommandResults(readable_output=hr) def mitigation_template_list_command(client: NetscoutClient, args: dict): extend_data = argToBoolean(args.get("extend_data", False)) raw_result = client.mitigation_template_list() data = raw_result.get("data") data = data if isinstance(data, list) else [data] hr = [build_human_readable(data=mitigation_template) for mitigation_template in data] mitigation_templates = [ build_output(data=mitigation_template, extend_data=extend_data, keys_to_remove=["relationships", "subobject"]) for mitigation_template in data ] return CommandResults( outputs_prefix="NASightline.MitigationTemplate", outputs_key_field="id", outputs=mitigation_templates, readable_output=tableToMarkdown("Mitigation template list", hr, removeNull=True), raw_response=raw_result, ) def router_list_command(client: NetscoutClient, args: dict): extend_data = argToBoolean(args.get("extend_data", False)) raw_result = client.router_list() data = raw_result.get("data") data = data if isinstance(data, list) else [data] hr = [build_human_readable(router) for router in data] routers = [build_output(data=router, extend_data=extend_data) for router in data] return CommandResults( outputs_prefix="NASightline.Router", outputs_key_field="id", outputs=routers, readable_output=tableToMarkdown("Router list", hr, headers=ROUTERS_HR_HEADERS, removeNull=True), raw_response=raw_result, ) def managed_object_list_command(client: NetscoutClient, args: dict): page = arg_to_number(args.get("page")) limit = arg_to_number(args.get("limit")) extend_data = argToBoolean(args.get("extend_data", False)) raw_result = client.managed_object_list(page=page, page_size=limit) data = raw_result.get("data") data = data if isinstance(data, list) else [data] objects = [build_output(data=managed_object, extend_data=extend_data) for managed_object in data] hr = [build_human_readable(data=managed_object) for managed_object in data] return CommandResults( outputs_prefix="NASightline.ManagedObject", outputs_key_field="id", outputs=objects, readable_output=tableToMarkdown("Managed object list", hr, headers=MANAGED_OBJECTS_HR_HEADERS, removeNull=True), raw_response=raw_result, ) def tms_group_list_command(client: NetscoutClient, args: dict): extend_data = argToBoolean(args.get("extend_data", False)) raw_result = client.tms_group_list() data = raw_result.get("data") data = data if isinstance(data, list) else [data] hr = [build_human_readable(data=tms_group) for tms_group in data] groups = [build_output(data=group, extend_data=extend_data) for group in data] return CommandResults( outputs_prefix="NASightline.TMSGroup", outputs_key_field="id", outputs=groups, readable_output=tableToMarkdown("TMS group list", hr, removeNull=True), raw_response=raw_result, ) """ MAIN FUNCTION """ def main() -> None: try: command = demisto.command() params = demisto.params() if not params.get("User") or not (api_token := params.get("User", {}).get("password")): raise DemistoException("Missing API Key. Fill in a valid key in the integration configuration.") base_url = urljoin(params["url"], "api/sp") verify_certificate = not params.get("insecure", False) proxy = params.get("proxy", False) first_fetch = None if first_fetch_dt := arg_to_datetime(params.get("first_fetch", "3 days")): first_fetch = first_fetch_dt.isoformat() max_fetch = min(arg_to_number(params.get("max_fetch")) or 50, 100) look_back = arg_to_number(params.get("look_back")) or 1 alert_class = argToList(params.get("alert_class")) alert_type = argToList(params.get("alert_type")) if alert_class and alert_type: raise DemistoException( "Cannot filter alerts with both 'Alert Class' and 'Alert Type' configured. Either choose " "the entire class you want to fetch or the specific types from within that class." ) classification = params.get("classification") importance = cast_importance_to_minimal(params.get("importance")) ongoing = ONGOING_DICTIONARY.get(params.get("ongoing")) demisto.debug(f"Command being called is {demisto.command()}") headers: dict = {"X-Arbux-APIToken": api_token} client = NetscoutClient( base_url=base_url, verify=verify_certificate, headers=headers, proxy=proxy, first_fetch=first_fetch, max_fetch=max_fetch, look_back=look_back, alert_class=alert_class, alert_type=alert_type, classification=classification, importance=importance, ongoing=ongoing, ) args: dict = demisto.args() result = "" if command == "test-module": result = test_module(client) elif command == "fetch-incidents": fetch_incidents_command(client) elif command == "na-sightline-alert-list": result = list_alerts_command(client, args) elif command == "na-sightline-alert-annotation-list": result = alert_annotation_list_command(client, args) elif command == "na-sightline-mitigation-list": result = mitigation_list_command(client, args) elif command == "na-sightline-mitigation-create": result = mitigation_create_command(client, args) elif command == "na-sightline-mitigation-delete": result = mitigation_delete_command(client, args) elif command == "na-sightline-mitigation-template-list": result = mitigation_template_list_command(client, args) elif command == "na-sightline-router-list": result = router_list_command(client, args) elif command == "na-sightline-managed-object-list": result = managed_object_list_command(client, args) elif command == "na-sightline-tms-group-list": result = tms_group_list_command(client, args) else: raise NotImplementedError(f"Command: {command} is not implemented") if result: return_results(result) except Exception as e: return_error(f"Failed to execute {command} command.\nError:\n{str(e)}") """ ENTRY POINT """ if __name__ in ("__main__", "__builtin__", "builtins"): main()