from CommonServerPython import * import json import urllib3 from typing import Any # Disable insecure warnings urllib3.disable_warnings() """ CONSTANTS """ BASE_URL = "https://api.recordedfuture.com/gw/xsiam" STATUS_TO_RETRY = [500, 501, 502, 503, 504] DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" VENDOR = "Recorded Future" PRODUCT = "Intelligence Cloud" """ CLIENT CLASS """ class Client(BaseClient): def _call(self, url_suffix, **kwargs): request_kwargs = { "method": "get", "url_suffix": url_suffix, "timeout": 90, "retries": 3, "status_list_to_retry": STATUS_TO_RETRY, } request_kwargs.update(kwargs) return self._http_request(**request_kwargs) def test_connection(self) -> dict[str, Any]: """Check connection.""" return self._call(url_suffix="/config/info") def get_alerts(self, params: dict = None) -> dict[str, Any]: """Get alerts.""" return self._call(url_suffix="/alert/search", params=params) """ COMMAND FUNCTIONS """ def test_module(client: Client): """ Tests API connectivity and authentication' When 'ok' is returned it indicates the integration works like it is supposed to and connection to the service is successful. Raises exceptions if something goes wrong. Args: client (Client): RecordedFuture client to use. Returns: str: 'ok' if test passed, anything else will raise an exception and will fail the test. """ try: client.test_connection() return_results("ok") except Exception as err: message = str(err) try: error = json.loads(str(err).split("\n")[1]) if "fail" in error.get("result", {}).get("status", ""): message = error.get("result", {})["message"] except Exception: message = f"Unknown error. Please verify that the API URL and Token are correctly configured. RAW Error: {err}" raise DemistoException(f"Failed due to - {message}") def get_events(client, params: dict) -> list: """ Retrieves events using the RecordedFuture API. Args: client (Client): RecordedFuture client to use. params (dict): The params to send to the API basically contains the limit for example {'limit': '1000'}. Returns: list: (list) of events that will be created in XSIAM. """ result = client.get_alerts(params) events = result.get("data", []) hr = tableToMarkdown(name="Test Event", t=events) return_results(CommandResults(readable_output=hr, raw_response=events)) return events def get_triggered(event: dict) -> str: """Get the 'triggered' value from an event without milliseconds since the API ignores them. Args: event (dict): The event from API. Returns: str: the "triggered" value. """ if event: return event.get("log", {}).get("triggered", "").split(".")[0] return "" def fetch_events(client: Client, **kwargs) -> tuple[list, dict]: """ Args: client (Client): RecordedFuture client to use. Returns: list: (list) of events that will be created in XSIAM. dict: The lastRun object to save for next run. """ params = { "triggered": f'[{kwargs.get("last_run")},]', "orderby": "triggered", "direction": "asc", "limit": kwargs.get("limit"), } response = client.get_alerts(params) next_run = {} if events := response.get("data", []): # Obtain the latest triggered time (for the next fetch round) next_run_time = get_triggered(events[0]) # We need the IDs of the events with the same trigger time as the latest, # So that we can remove them in the next fetch, Since we are fetching from (including) this time. next_run_ids = {event.get("id") for event in events if get_triggered(event) == next_run_time} # In case all events were triggered at the same time and the limit equals their amount, # We should increase the next run time, Otherwise the fetch will get stuck at this time forever. if len(next_run_ids) == int(kwargs.get("limit")): # type: ignore next_run_time = (datetime.strptime(next_run_time, DATE_FORMAT) + timedelta(seconds=1)).strftime(DATE_FORMAT) # Filter out events that have already been fetched. if last_run_event_ids := demisto.getLastRun().get("last_run_ids"): demisto.info(f"this is the last_run_event_ids {last_run_event_ids}") events = list(filter(lambda x: x.get("id") not in last_run_event_ids, events)) next_run = {"last_run_time": next_run_time, "last_run_ids": list(next_run_ids)} return events, next_run """ HELPER FUNCTIONS """ def add_time_key_to_events(events: list = None): """ Adds the _time key to the events. Args: events: list, the events to add the time key to. """ for event in events or []: event["_time"] = demisto.get(event, "log.triggered") """ MAIN FUNCTION """ def main() -> None: """ main function, parses params and runs command functions """ params = demisto.params() args = demisto.args() command = demisto.command() verify_certificate = not params.get("insecure", False) proxy = params.get("proxy", False) api_key = params.get("credentials", {}).get("password") headers = {"X-RFToken": api_key} demisto.info(f"Command being called is {command}") try: client = Client(base_url=BASE_URL, headers=headers, verify=verify_certificate, proxy=proxy) if command == "test-module": test_module(client) elif command == "recorded-future-get-events": events = get_events(client, params={"limit": args.get("limit", 10)}) if argToBoolean(args.get("should_push_events", False)): add_time_key_to_events(events) send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT) elif command == "fetch-events": if not (last_run := demisto.getLastRun().get("last_run_time")): last_run = arg_to_datetime(params.get("first_fetch", "3 days")).strftime(DATE_FORMAT) # type: ignore events, next_run = fetch_events( client=client, limit=args.get("limit") or params.get("max_fetch") or 1000, last_run=last_run ) add_time_key_to_events(events) send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT) if next_run: demisto.setLastRun(next_run) # Log exceptions and return errors 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()