Proofpoint Email Security Event Collector
Collects events for Proofpoint Email Security using the streaming API.
Analytics & SIEM · Proofpoint Email Security
Details
| ID | Proofpoint Email Security Event Collector |
|---|---|
| Provider | Thoma Bravo |
| Category | Analytics & SIEM |
| From Version | 6.9.0 |
| Docker Image | demisto/netutils:1.0.0.10187688 |
| Supported Modules | XSIAM |
README
Collects events for Proofpoint using the streaming API.
This integration was integrated and tested Proofpoint Email Security.
Configure Proofpoint Email Security Event Collector in Cortex
| Parameter | Description | Required |
|---|---|---|
| Server URL | True | |
| Cluster ID | The user group ID. | True |
| API key | True | |
| Use system proxy settings | False | |
| Fetch Events | False | |
| Event types to fetch | Denotes which event type to fetch, if not provided will fetch all kinds. | False |
| Fetch interval in seconds | True |
Commands
proofpoint-es-get-last-run-results
Retrieves the results of a connection attempt to Proofpoint, indicating whether it was successful or failed and why. If event fetching has been initiated, this command provides the results of the most recent fetch attempt.
proofpoint-es-get-events
Retrieves events from the Proofpoint Email Security PoD archive for a specified time range to backfill gaps in fetched events. This command is intended for development and debugging purposes and is to be used with caution after consulting with engineering, as it may create duplicate events, exceed API request rate limits, and disrupt the fetch events mechanism. Review the list of known limitations below for additional details.
Base Command
proofpoint-es-get-events
Input
| Argument | Description | Required |
|---|---|---|
| since_time | The start of the time range to fetch events from. E.g., ‘3 days ago’, ‘2025-01-01T10:00:00’. Rounds down to the nearest hour. | Required |
| to_time | The end of the time range to fetch events from. E.g., ‘2 days ago’, ‘2025-01-01T11:00:00’. Rounds up to the nearest hour. | Required |
| timezone_offset | The UTC timezone offset in hours to apply to the since_time and to_time arguments. E.g. -5 for UTC-5. | Optional |
| event_types | A comma-separated list of event types to fetch. If not provided, all types will be retrieved. | Optional |
| limit | The maximum number of events to fetch per specified event type. | Optional |
| should_push_events | If true, the command will push the events to the Cortex XSIAM dataset; otherwise, it will only display them. | Optional |
Context Output
There is no context output for this command.
Known Limitations
- The API does not allow use of the same API Key for more than one session at the same time. To open more multiple simultaneous websocket connections to receive the same event type, additional API Key(s) must be generated via the Proofpoint PoD dashboard.
- When running the
proofpoint-es-get-eventscommand, HTTP 409 (Conflict) errors may be raised if the integration instance has “Fetch Events” enabled. Ensure the “Fetch Events” checkbox is unchecked before triggering this command. - When running the
proofpoint-es-get-eventscommand, HTTP 400 (Bad Request) errors may be raised if the time range is older than 30 days. Ensure that both thesince_timeandto_timearguments are within the last 30 days.
Troubleshooting
In case of data ingestion delays or missing events, it is recommended to configure a separate integration instance per event type.
Configuration parameters
host— Server URL (required)cluster_id— Cluster ID (required)api_key— (required)fetch_interval— Fetch interval in seconds (required)longRunning— Long Running Instanceproxy— Use system proxy settingsevent_types— Event types to fetchisFetchEvents— Fetch events
Commands (2)
-
proofpoint-es-get-eventsRetrieves events from the Proofpoint Email Security PoD archive for a specified time range to backfill gaps in fetched events. This command is intended for development and debugging purposes and is to be used with caution after consulting with engineering, as it may create duplicate events, exceed API request rate limits, and disrupt the fetch events mechanism.
-
proofpoint-es-get-last-run-resultsRetrieves the results of a connection attempt to Proofpoint, indicating whether it was successful or failed and why. If event fetching has been initiated, this command provides the results of the most recent fetch attempt.
from contextlib import ExitStack, contextmanager from functools import partial from threading import Lock, Thread import demistomock as demisto from CommonServerPython import * from dateutil import tz from http import HTTPStatus from websockets import exceptions from websockets.sync.client import connect from websockets.sync.connection import Connection from datetime import datetime, timezone, timedelta VENDOR = "proofpoint" PRODUCT = "email_security" URL = "{host}/v1/stream?cid={cluster_id}&type={type}" FETCH_INTERVAL_IN_SECONDS = 60 FETCH_SLEEP = 5 SERVER_IDLE_TIMEOUT = 300 MAX_RECONNECT_ATTEMPTS = 5 MAX_BACKOFF_SECONDS = 60 BASE_BACKOFF_SECONDS = 2 EVENT_TYPES = ["message", "maillog", "audit"] DEFAULT_GET_EVENTS_LIMIT = 10 DATE_FILTER_FORMAT = "%Y-%m-%dT%H:%M:%S%z" PING_INTERVAL = 60 # Interval between keepalive pings in seconds PING_TIMEOUT = 120 # Timeout for waiting for a pong reply to a keepalive ping in seconds OPEN_TIMEOUT = 10 # Timeout for opening the WebSocket connection in seconds CLOSE_TIMEOUT = 60 # Timeout for closing the connection in seconds MAX_MESSAGE_SIZE: int | None = None # Disable the 1 MiB default size limit on incoming messages RECEIVE_TIMEOUT = 1 # Timeout for receiving events in seconds class EventConnection: def __init__( self, event_type: str, url: str, headers: dict, fetch_interval: int = FETCH_INTERVAL_IN_SECONDS, idle_timeout: int = SERVER_IDLE_TIMEOUT - 20, check_heartbeat: bool = True, ): demisto.info(f"Starting EventConnection of type {event_type}") self.event_type = event_type self.url = url self.headers = headers self.lock = Lock() self.idle_timeout = idle_timeout self.fetch_interval = fetch_interval self.connection = self.connect() if check_heartbeat: self.heartbeat_thread = Thread(target=self.heartbeat, daemon=True) self.heartbeat_thread.start() def connect(self) -> Connection: """ Establish a new WebSocket connection. Parameters are aligned with Proofpoint's official reference client (see ``logstream-client.ini`` ``[Websocket]`` section): * ``max_size=None`` disables the websockets library's default 1 MiB cap on incoming frames. Proofpoint may emit messages larger than 1MB; without this the server tears the connection down with code 1011 (internal error) and the client subsequently fails to re-establish the TLS session ("[Errno 104] Connection reset by peer"). * ``ping_timeout=None`` disables pong-reply enforcement, preventing the keep-alive watchdog from killing the socket while a large message is being received/decoded. * ``ping_interval``, ``open_timeout`` and ``close_timeout`` mirror the reference client defaults. """ return connect( self.url, additional_headers=self.headers, open_timeout=OPEN_TIMEOUT, close_timeout=CLOSE_TIMEOUT, ping_interval=PING_INTERVAL, ping_timeout=PING_TIMEOUT, max_size=MAX_MESSAGE_SIZE, ) @property def is_to_archive(self) -> bool: """ Check if the connection is to an archive. """ return bool("sinceTime" in self.url and "toTime" in self.url) def reconnect(self): """ Reconnect logic for the WebSocket connection using an exponential backoff strategy. This handles the HTTP 409 error by delaying the retry to allow the Proofpoint server to clear the session. """ attempt = 0 while attempt < MAX_RECONNECT_ATTEMPTS: attempt += 1 demisto.info(f"[{self.event_type}] Going to attempt reconnection #{attempt} / {MAX_RECONNECT_ATTEMPTS}.") try: self.connection = self.connect() demisto.info(f"[{self.event_type}] Successfully reconnected to WebSocket after {attempt} attempt(s).") return except exceptions.InvalidStatus as e: # Need to wait if HTTP 409 (Conflict) error was raised because the failed session # has not been fully terminated or released by the Proofpoint server if e.response.status_code == HTTPStatus.CONFLICT: delay = min(MAX_BACKOFF_SECONDS, BASE_BACKOFF_SECONDS * (2 ** (attempt - 1))) demisto.error( f"[{self.event_type}] Reconnection failed due to session conflict. " f"Waiting {delay} seconds before retrying (Attempt {attempt})." ) time.sleep(delay) else: demisto.error(f"[{self.event_type}] Connection status error: {e!s}.") raise except Exception as e: demisto.error(f"[{self.event_type}] General reconnection failure: {e!s}.") raise def heartbeat(self): """ Heartbeat thread function to periodically send keep-alives (pong) to the server. Keep-alives are sent regardless of the actual connection activity to ensure the connection remains open. """ while True: with self.lock: try: self.connection.pong() demisto.info(f"[{self.event_type}] Sent heartbeat pong") except exceptions.ConnectionClosedError as e: demisto.error(f"[{self.event_type}] Connection closed due to error in thread - {self.event_type}: {e!s}") self.reconnect() except exceptions.ConnectionClosedOK: demisto.info(f"[{self.event_type}] Connection closed OK in thread - {self.event_type}") self.reconnect() except Exception as e: demisto.error(f"[{self.event_type}] Unexpected error in heartbeat: {e!s} {traceback.format_exc()}") self.reconnect() time.sleep(self.idle_timeout) def receive(self, timeout: int = RECEIVE_TIMEOUT) -> dict[str, Any]: """Receive a new event from the websocket connection. Args: timeout (int): Maximum time to wait for a message to be received. Defaults to RECEIVE_TIMEOUT. Returns: dict[str, Any]: The raw event from the websocket. """ return json.loads(self.connection.recv(timeout=timeout)) def is_interval_passed(fetch_start_time: datetime, fetch_interval: int) -> bool: """This function checks if the given interval has passed since the given start time Args: fetch_start_time (datetime): The start time of the interval fetch_interval (int): The interval in seconds Returns: bool: True if the interval has passed, False otherwise """ return fetch_start_time + timedelta(seconds=fetch_interval) < datetime.utcnow() def set_the_integration_context(key: str, val: Any): """Adds a key-value pair to the integration context dictionary. If the key already exists in the integration context, the function will overwrite the existing value with the new one. """ cnx = demisto.getIntegrationContext() cnx[key] = val demisto.setIntegrationContext(cnx) @contextmanager def websocket_connections( host: str, cluster_id: str, api_key: str, since_time: str | None = None, to_time: str | None = None, fetch_interval: int = FETCH_INTERVAL_IN_SECONDS, event_types: list[str] = EVENT_TYPES, write_to_integration_context: bool = True, check_heartbeat: bool = True, ): """ Create a connection for every type of event. Args: host (str): host URL for the websocket connection. cluster_id (str): Proofpoint cluster ID to connect to. api_key (str): Proofpoint API key. since_time (str): Start time to fetch events from. to_time (str): End time for fetch, leave empty for real-time streaming. fetch_interval (int): Time between fetch iterations, used for estimating message receive times for idle heartbeat. event_types: (list[str]): The list of event types to collect. write_to_integration_context (bool): Whether to write events to the integration context. check_heartbeat (bool): Whether to check for heartbeat messages. Yields: list[EventConnection]: List containing an eventConnection for every event type """ demisto.info( f"Starting websocket connection to {host} with cluster id: {cluster_id}, sinceTime: {since_time}, toTime: {to_time}" ) url = URL if since_time: url += f"&sinceTime={since_time}" if to_time: url += f"&toTime={to_time}" url = partial(url.format, host=host, cluster_id=cluster_id) extra_headers = {"Authorization": f"Bearer {api_key}"} try: with ExitStack(): # Keep connection contexts for clean up connections = [ EventConnection( event_type=event_type, url=url(type=event_type), headers=extra_headers, fetch_interval=fetch_interval, check_heartbeat=check_heartbeat, ) for event_type in event_types ] if write_to_integration_context: set_the_integration_context( "last_run_results", f"Opened a connection successfully at {datetime.now().astimezone(tz.tzutc())}" ) yield connections except Exception as e: if write_to_integration_context: set_the_integration_context( "last_run_results", f"{e!s} \n This error happened at {datetime.now().astimezone(tz.tzutc())}" ) raise DemistoException(f"{e!s}\n") def fetch_events( connection: EventConnection, fetch_interval: int, integration_context: dict, should_skip_sleeping: List[bool], write_to_integration_context: bool = True, ) -> list[dict]: """ This function fetches events from the given connection, for the given fetch interval Args: connection (EventConnection): the connection to the event type fetch_interval (int): Total time to keep fetching before stopping integration_context (dict) The integration context dict. should_skip_sleeping (List[bool]): a list to update all execution results - False if timeout occurred, otherwise will append True. write_to_integration_context (bool): Whether to write to integration context. Returns: list[dict]: A list of events """ event_type = connection.event_type demisto.debug(f"[{event_type}] Starting to fetch events.") events: list[dict] = [] event_ids = set() # For deduplication within the *same* fetch cycle only! (Cannot deduplicate all events from the websocket) fetch_start_time = datetime.utcnow() demisto.info(f"[{event_type}] Preparing to acquire lock & recv.") with connection.lock: while not is_interval_passed(fetch_start_time, fetch_interval): try: event = receive_event(connection, timeout=RECEIVE_TIMEOUT) event_id = get_event_id(event) if event_id in event_ids: demisto.debug(f"[{event_type}] Already fetched {event_id=}. Skipping duplicate.") continue event_ids.add(event_id) events.append(event) except TimeoutError: # This exception typically indicates no new events from websocket stream demisto.debug(f"[{event_type}] Timeout while waiting for a new event in stream. Breaking.") break except exceptions.InvalidStatus as e: status_code = e.response.status_code demisto.error(f"[{event_type}] Invalid status: {status_code}. Starting recovery.") recover_after_disconnection(connection, events, event_ids, reconnect=True) continue except exceptions.ConnectionClosedError: demisto.error(f"[{event_type}] Connection closed with error. Starting recovery.") recover_after_disconnection(connection, events, event_ids, reconnect=True) continue except exceptions.ConnectionClosedOK: # This exception from archive websocket means event fetching completed successfully if connection.is_to_archive: demisto.info(f"[{event_type}] Connection closed with OK status. All archived events were fetched.") break demisto.error(f"[{event_type}] Connection closed with OK status. Starting recovery.") recover_after_disconnection(connection, events, event_ids, reconnect=True) continue except Exception as e: demisto.error(f"[{event_type}] Got general error in fetch_events. Starting recovery. Error: {e}") recover_after_disconnection(connection, events, event_ids, reconnect=False) events.extend(integration_context.get(event_type, [])) integration_context[event_type] = events # update events in context in case of fail integration_context["last_run_results"] = f"{e!s} \nThe error happened at {datetime.now().astimezone(tz.tzutc())}" if write_to_integration_context: set_integration_context(integration_context) raise DemistoException(str(e)) demisto.info(f"[{event_type}] Released the lock and finished recv.") num_events = len(events) last_event_time = events[-1].get("_time") if events else None demisto.debug(f"[{event_type}] Fetched {num_events} events with {last_event_time=}") demisto.debug(f"[{event_type}] Fetched events IDs: {', '.join([str(event_id) for event_id in event_ids])}.") if write_to_integration_context: fetch_end_time = datetime.utcnow() set_the_integration_context( "last_run_results", f"Got from connection {num_events} events starting at {fetch_start_time!s} until {fetch_end_time!s}", ) should_skip_sleeping.append(True) return events def get_event_id(event: dict): """ Extracts event ID using either the `id`, `guid`, or `filter.qid` field. Used for id-based deduplication and logging. Args: event (dict): The raw event. Returns: str: Event ID. """ return event.get("id") or event.get("guid") or event.get("filter", {}).get("qid") def receive_event(connection: EventConnection, timeout: int = RECEIVE_TIMEOUT) -> dict[str, Any]: """ Processes a single event by parsing its timestamp and adding metadata. Args: connection (EventConnection): The connection to the event type. timeout (int): Maximum time to wait for a message to be received. Defaults to RECEIVE_TIMEOUT. Returns: dict[str, Any]: The processed event with '_time' and 'event_type' fields. """ event = connection.receive(timeout=timeout) event_id = get_event_id(event) event_ts = event.get("ts") if not event_ts: # if timestamp is not in the response, use the current time demisto.debug(f"Event {event_id} does not have a timestamp, using current time.") event_ts = datetime.utcnow().isoformat() date = dateparser.parse(event_ts) if not date: demisto.debug(f"Event {event_id} has an invalid timestamp, using current time.") # if timestamp is not in correct format, use the current time date = datetime.utcnow() # the `ts` parameter is not always in UTC, so we need to convert it event["_time"] = date.astimezone(tz.tzutc()).isoformat() event["event_type"] = connection.event_type return event def receive_events_after_disconnection(connection: EventConnection) -> list[dict]: """Receive events after disconnection. Args: connection (EventConnection): The connection to the event type. Returns: list[dict]: A list of events received after disconnection. """ events = [] demisto.info(f"[{connection.event_type}] Trying to receive in-transit events following disconnection.") while True: try: event = receive_event(connection) events.append(event) except Exception as e: demisto.debug(f"[{connection.event_type}] Terminating post-disconnection receive loop after error: {e}") break demisto.info(f"[{connection.event_type}] Returning {len(events)} in-transit events following disconnection.") return events def recover_after_disconnection(connection: EventConnection, events: list[dict], event_ids: set[str], reconnect: bool = True): """Recover after disconnection by attempting to receive in-transit events and (optionally) reconnecting. Args: connection (EventConnection): The connection to the event type. events (list[dict]): The list of events. event_ids (set[str]): The set of event ids. reconnect (bool, optional): Whether to reconnect. Defaults to True. """ demisto.debug(f"[{connection.event_type}] Recovering after disconnection.") in_transit_events = receive_events_after_disconnection(connection) for event in in_transit_events: event_id = get_event_id(event) if event_id in event_ids: demisto.debug(f"[{connection.event_type}] Already fetched {event_id=}. Skipping duplicate.") continue event_ids.add(event_id) events.append(event) demisto.debug( f"[{connection.event_type}] Received {len(in_transit_events)} in-transit events and event ids after disconnection." ) if reconnect: demisto.info(f"[{connection.event_type}] Attempting to reconnect after disconnection.") try: connection.reconnect() except Exception as e: demisto.error( f"[{connection.event_type}] Failed to reconnect after disconnection. Error: {e}. {traceback.format_exc()}" ) raise def test_module(host: str, cluster_id: str, api_key: str): raise DemistoException( "No test option is available due to API limitations.\ To verify the configuration, run the proofpoint-es-get-last-run-results command." ) def get_last_run_results_command(): last_run_results = demisto.getIntegrationContext().get("last_run_results") if last_run_results: return CommandResults(readable_output=last_run_results) else: return CommandResults( readable_output="No results from the last run yet, \ please wait one minute and try running the command again." ) def perform_long_running_loop(connections: list[EventConnection], fetch_interval: int, should_skip_sleeping: List[bool]): """ Long running loop iteration function. Fetches events from each connection and sends them to XSIAM. Args: connections (list[EventConnection]): List of connection objects to fetch events from. fetch_interval (int): Fetch time per cycle allocated for each event type in seconds. should_skip_sleeping (List[bool]): a list to update all execution results - False if timeout occurred, otherwise will append True. """ integration_context = demisto.getIntegrationContext() events_to_send = [] for connection in connections: events = fetch_events(connection, fetch_interval, integration_context, should_skip_sleeping) events.extend(integration_context.get(connection.event_type, [])) integration_context[connection.event_type] = events # update events in context in case of fail demisto.debug(f"[{connection.event_type}] Adding {len(events)} events to XSIAM") events_to_send.extend(events) demisto.info(f"Going to send {len(events_to_send)} events to xsiam.") # Send the events to the XSIAM, with events from the context try: if events_to_send: send_events_to_xsiam(events_to_send, vendor=VENDOR, product=PRODUCT) else: demisto.info("No events to send to xsiam.") demisto.info(f"Finished sending {len(events_to_send)} events to xsiam, going to clear context.") # clear the context after sending the events for connection in connections: set_the_integration_context(connection.event_type, []) except Exception as e: demisto.error(f"Failed to send events to XSIAM. Error: {str(e)}, {traceback.format_exc()}") # save the events to the context so we can send them again in the next execution demisto.setIntegrationContext(integration_context) def long_running_execution_command(host: str, cluster_id: str, api_key: str, fetch_interval: int, event_types: List[str]): """ Performs the long running execution loop. Opens a connection to Proofpoints for every event type and fetches events in a loop. Heartbeat threads are opened for every connection to send keepalives if the connection is idle for too long. Args: host (str): host URL for the websocket connection. cluster_id (str): Proofpoint cluster ID to connect to. api_key (str): Proofpoint API key. fetch_interval (int): Total time allocated per fetch cycle. event_types (List[str]): The list of event types to collect. """ support_multithreading() demisto.info(f"Starting long running execution. Fetching {event_types=}.") while True: try: with websocket_connections( host, cluster_id, api_key, fetch_interval=fetch_interval, event_types=event_types ) as connections: demisto.info("Connected to websocket") fetch_interval = max(1, fetch_interval // len(event_types)) # Divide the fetch interval equally among event types while True: should_skip_sleeping: List[bool] = [] perform_long_running_loop(connections, fetch_interval, should_skip_sleeping) # sleep for a bit to not throttle the CPU if any(should_skip_sleeping): demisto.info("Finished perform_long_running_loop, should_skip_sleeping evaluated to True.") else: demisto.info( "Finished perform_long_running_loop, should_skip_sleeping evaluated to False, going to" f"sleep {FETCH_SLEEP} seconds." ) time.sleep(FETCH_SLEEP) demisto.info(f"Finished sleeping {FETCH_SLEEP} seconds.") except Exception as e: err = f"Got an error while running in long running {e}. {traceback.format_exc()}" demisto.updateModuleHealth(err, is_error=True) demisto.error(err) def get_events_command(host: str, cluster_id: str, api_key: str, args: dict[str, str]) -> tuple[list, CommandResults]: """Implements the `!proofpoint-est-get-events` command. Args: host (str): The host URL for the websocket connection. cluster_id (str): The Proofpoint cluster ID to connect to. api_key (str): The Proofpoint API key. args (dict[str, str]): The command arguments. Returns: tuple[list, CommandResults]: The list of events and the command results containing human-readable output. """ event_types = argToList(args.get("event_types")) or EVENT_TYPES limit = arg_to_number(args.get("limit")) or DEFAULT_GET_EVENTS_LIMIT tz_offset = arg_to_number(args.get("timezone_offset")) or 0 event_tz = timezone(timedelta(hours=tz_offset)) # `arg_to_datetime` does not return `None` since args are required. Added `type: ignore` to silence type checkers and linters since_time = arg_to_datetime(args.get("since_time"), required=True).replace(tzinfo=event_tz).strftime(DATE_FILTER_FORMAT) # type: ignore[union-attr] to_time = arg_to_datetime(args.get("to_time"), required=True).replace(tzinfo=event_tz).strftime(DATE_FILTER_FORMAT) # type: ignore[union-attr] all_events = [] time_interval = 1 * 60 demisto.debug(f"Starting to fetch {event_types=}, {since_time=}, {to_time=}") with websocket_connections( host, cluster_id, api_key, event_types=event_types, since_time=since_time, to_time=to_time, write_to_integration_context=False, check_heartbeat=False, ) as connections: for connection in connections: events = fetch_events( connection, time_interval, integration_context={}, should_skip_sleeping=[], write_to_integration_context=False, ) demisto.debug(f"[{connection.event_type}] Got {len(events)} events. Appending up to {limit} events.") all_events.extend(events[:limit]) return all_events, CommandResults(readable_output=tableToMarkdown(f"Events since {since_time} to {to_time}", all_events)) def main(): # pragma: no cover command = demisto.command() params = demisto.params() args = demisto.args() host = params.get("host", "") cluster_id = params.get("cluster_id", "") api_key = params.get("api_key", {}).get("password", "") fetch_interval = int(params.get("fetch_interval", FETCH_INTERVAL_IN_SECONDS)) event_types = argToList(params.get("event_types")) or EVENT_TYPES try: if command == "long-running-execution": if params.get("isFetchEvents", False): demisto.debug("Fetching events is enabled for this instance. Starting long running execution.") return_results(long_running_execution_command(host, cluster_id, api_key, fetch_interval, event_types)) else: demisto.debug(f"Fetching events is disabled for this instance. Sleeping for {fetch_interval} seconds.") time.sleep(fetch_interval) elif command == "test-module": return_results(test_module(host, cluster_id, api_key)) elif command == "proofpoint-es-get-last-run-results": return_results(get_last_run_results_command()) elif command == "proofpoint-es-get-events": should_push_events = argToBoolean(args.get("should_push_events", False)) events, command_results = get_events_command(host, cluster_id, api_key, args) if should_push_events: send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT) return_results(command_results) elif command == "fetch-events": # Return informative error message to prevent errors from appearing in tenant UI§ demisto.info("Fetching events is done via long running execution.") else: raise NotImplementedError(f"Command {command} is not implemented.") except Exception: return_error(f"Failed to execute {command} command.\nError:\n{traceback.format_exc()}") if __name__ in ("__main__", "__builtin__", "builtins"): main()