def _init_function(self, opts): """ setup information on the log directory to setup :param opts: :return: """ res_opts = opts.get("resilient", {}) log_dir = res_opts.get("logdir") log_file = res_opts.get("logfile", LOG_FILE) if not log_dir: raise IntegrationError("Log directory not found") self.log_file = os.path.join(log_dir, log_file) if not os.path.isfile(self.log_file): raise IntegrationError("Log file incorrect: {}".format(self.log_file))
def check_ms_graph_response_code(status_code): """ Check that the request status code: raise an integration error if great than 300 and code is "not found" :param status_code: request statues code :return: """ if status_code >= 300 and status_code != 404: raise IntegrationError("Invalid response from Microsoft Graph API call.")
def report_callback(response): """ callback to review status code, 200 - report ready, 404 - report not ready :param response: :return: response """ if response.status_code == 200 or response.status_code == 404: return response else: raise IntegrationError(response.content)
def polling_thread(self): """contents of polling thread, alternately check for new data and wait""" pptr = PPTRClient(self.opts, self.options) while not self.stop_thread: incident_list = pptr.get_incidents(self.lastupdate, self.state) if 'error' in incident_list: LOG.warning(incident_list.get('error')) raise IntegrationError(incident_list.get('error')) try: ### BEGIN Processing incidents for incident in incident_list: LOG.info("Proofpoint TRAP Incident ID %d discovered: %s", incident['id'], incident.get('summary', 'No Summary Provided')) if len(self._find_resilient_incident_for_req(incident['id'], CUSTOM_FIELDS[0])) == 0: # Assemble Data table for incident i_table = self.make_data_table(incident['events']) # Get Extra Incident Fields i_fields = self.make_incident_fields(incident) # Build out artifacts for incident i_artifacts = self.make_incident_artifacts(incident) # Create incident and return response i_response = self.create_incident(i_fields, i_table) # Add Artifacts self.create_incident_artifact(i_response['id'], i_artifacts) # Add raw event payload as note i_comment = self.create_incident_comment(i_response['id'], incident) else: LOG.info("Incident already exists for TRAP Incident %d", incident['id']) # TODO: Add checks for Artifacts and Data Table rows # TODO: Check update_at against datetime.now and if the delta is greater than polling interval, # ensure that table data is up to date except TypeError as ex: LOG.error(ex) # Break out of loop before sleep if restart initiated. if self.stop_thread: break # Amount of time (seconds) to wait to check cases again, defaults to 10 mins if not set time.sleep(int(self.options.get("polling_interval", 10)) * 60)
def build_MS_graph_query_url(self, email_address, mail_folder, sender, start_date, end_date, has_attachments, message_subject, message_body): """ build_MS_graph_query_url returns the MS Graph URL to query messages with this specified parameters. :param email_address: a single email address to be queried. :param mail_folder: mailFolder id of the folder to search :param sender: email address of sender to search for :param start_date: date/time string of email received dated to start search :param end_date: date/time string of email received dated to end search :param has_attachments: boolean flag indicating to search for emails with or without attachments :param message_subject: search for emails containing this string in the "subject" of email :param message_body: search for emails containing this string in the "body" of email :return: list of emails in all user email account that match the search criteria. """ # Compute the mail folder if it is specified. folder_string = self.build_folder_string(mail_folder) # Create $search string query for search on message body string. search_query = self.build_search_query(message_body) # Create $filter query string for query on messages. filter_query = self.build_filter_query(start_date, end_date, sender, message_subject, has_attachments) # Assemble the MS Graph API query string. if search_query: if filter_query: ms_graph_query_url = u'{0}/users/{1}{2}/messages{3}&{4}'.format( self.ms_graph_url, email_address, folder_string, search_query, filter_query) else: ms_graph_query_url = u'{0}/users/{1}{2}/messages{3}'.format( self.ms_graph_url, email_address, folder_string, search_query) elif filter_query: ms_graph_query_url = u'{0}/users/{1}{2}/messages{3}'.format( self.ms_graph_url, email_address, folder_string, filter_query) else: raise IntegrationError( "Exchange Online: Query Messages: no query parameters specified." ) return ms_graph_query_url
def custom_response_err_msg(response): """ Custom handler for response handling. :param response: :return: response """ try: # Raise error is bad status code is returned response.raise_for_status() # Return requests.Response object return response except Exception as err: msg = str(err) if isinstance(err, HTTPError) and response.status_code == 404: msg = "{} - {}".format(PROOFPOINT_TAP_404_ERROR, response.text) log and log.error(msg) raise IntegrationError(msg)
def delete_messages_from_query_results(self, query_results): """ :param query_results: query result list returned from Query Message function JSON object as a string. :return: list of messages for each email address search: list of deleted messages from the query results ; and list of messages not deleted from query result """ # Convert string to JSON. try: query_results_json = json.loads(query_results) except ValueError as err: raise IntegrationError( "Invalid JSON string in Delete Message from Query Results.") delete_results = [] for user in query_results_json: email_address = user["email_address"] deleted_list = [] not_deleted_list = [] for message in user["email_list"]: # Call MS Graph API to delete the message response = self.delete_message(email_address, None, message["id"]) # If message was deleted a 204 code is returned. if response.status_code == 204: deleted_list.append(message) else: not_deleted_list.append(message) user_delete_results = { 'email_address': email_address, 'deleted_list': deleted_list, 'not_deleted_list': not_deleted_list } delete_results.append(user_delete_results) return delete_results
def _exchange_online_create_meeting_function(self, event, *args, **kwargs): """Function: This function will create a meeting event and sent a mail message to the meeting participants.""" try: # Initialize the results payload rp = ResultPayload(CONFIG_DATA_SECTION, **kwargs) # Validate fields validate_fields([ 'exo_meeting_email_address', 'exo_meeting_start_time', 'exo_meeting_end_time', 'exo_meeting_subject', 'exo_meeting_body' ], kwargs) # Get the function parameters: email_address = kwargs.get("exo_meeting_email_address") # text start_time = kwargs.get("exo_meeting_start_time") # datetimepicker end_time = kwargs.get("exo_meeting_end_time") # datetimepicker subject = kwargs.get("exo_meeting_subject") # text body = kwargs.get("exo_meeting_body") # text required_attendees = kwargs.get( "exo_meeting_required_attendees") # text optional_attendees = kwargs.get( "exo_meeting_optional_attendees") # text location = kwargs.get("exo_meeting_location") # text LOG.info(u"exo_meeting_email_address: %s", email_address) LOG.info(u"exo_meeting_start_time: %s", start_time) LOG.info(u"exo_meeting_end_time: %s", end_time) LOG.info(u"exo_meeting_subject: %s", subject) LOG.info(u"exo_meeting_body: %s", body) LOG.info(u"exo_meeting_required_attendees: %s", required_attendees) LOG.info(u"exo_meeting_optional_attendees: %s", optional_attendees) LOG.info(u"exo_meeting_location: %s", location) # Validate the meeting start/end time if start_time >= end_time: raise IntegrationError( "Exchange Online meeting start time is behind end time.") # Check meeting time is not in the past. now_utc = datetime.datetime.utcnow() meeting_time_utc = datetime.datetime.utcfromtimestamp(start_time / 1000) if now_utc > meeting_time_utc: raise IntegrationError( "Exchange Online meeting start date/time is in the past.") yield StatusMessage( u"Starting create meeting for email address: {}".format( email_address)) # Get the MS Graph helper class MS_graph_helper = MSGraphHelper( self.options.get("microsoft_graph_token_url"), self.options.get("microsoft_graph_url"), self.options.get("tenant_id"), self.options.get("client_id"), self.options.get("client_secret"), self.options.get("max_messages"), self.options.get("max_users"), self.options.get("max_retries_total", MAX_RETRIES_TOTAL), self.options.get("max_retries_backoff_factor", MAX_RETRIES_BACKOFF_FACTOR), self.options.get("max_batched_requests", MAX_BATCHED_REQUESTS), RequestsCommon(self.opts, self.options).get_proxies()) # Call MS Graph API to get the user profile response = MS_graph_helper.create_meeting( email_address, start_time, end_time, subject, body, required_attendees, optional_attendees, location) if response.status_code == 201: success = True else: success = False response_json = response.json() results = rp.done(success, response_json) # Add pretty printed string for easier to read output text in note. pretty_string = json.dumps(response_json, ensure_ascii=False, indent=4, separators=(',', ': ')) results['pretty_string'] = pretty_string yield StatusMessage( u"Returning create meeting results for email address: {}". format(email_address)) # Produce a FunctionResult with the results yield FunctionResult(results) except Exception as err: LOG.error(err) yield FunctionError(err)
def execute_call_v2(self, method, url, timeout=30, proxies=None, callback=None, **kwargs): """Constructs and sends a request. Returns :class:`Response` object. From the requests.requests() function, inputs are mapped to this function :param method: GET, HEAD, PATCH, POST, PUT, DELETE, OPTIONS :param url: URL for the request. :param params: (optional) Dictionary, list of tuples or bytes to send in the body of the :class:`Request`. :param data: (optional) Dictionary, list of tuples, bytes, or file-like object to send in the body of the :class:`Request`. :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`. :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`. :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`. :param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': file-tuple}``) for multipart encoding upload. ``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')`` or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content-type'`` is a string defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers to add for the file. :param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth. :param timeout: (optional) How many seconds to wait for the server to send data before giving up, as a float, or a :ref:`(connect timeout, read timeout) <timeouts>` tuple. :type timeout: float or tuple :param allow_redirects: (optional) Boolean. Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. Defaults to ``True``. :type allow_redirects: bool :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. :param verify: (optional) Either a boolean, in which case it controls whether we verify the server's TLS certificate, or a string, in which case it must be a path to a CA bundle to use. Defaults to ``True``. :param stream: (optional) if ``False``, the response content will be immediately downloaded. :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair. :param callback: callback routine used to handle errors :return: :class:`Response <Response>` object :rtype: requests.Response """ try: if method.lower() not in ('get', 'post', 'put', 'patch', 'delete', 'head', 'options'): raise IntegrationError("unknown method {}".format(method)) # If proxies was not set check if they are set in the config if proxies is None: proxies = self.get_proxies() # Log the parameter inputs that are not None args_dict = locals() args = args_dict.keys() for k in args: if k != "self" and k != "kwargs" and args_dict[k] is not None: log.debug(" {}: {}".format(k, args_dict[k])) # Pass request to requests.request() function response = requests.request(method, url, timeout=timeout, proxies=proxies, **kwargs) # Debug logging log.debug(response.status_code) log.debug(response.content) # custom handler for response handling # set callback to be the name of the method you would like to call # to do your custom error handling and return the response if callback: return callback(response) # Raise error is bad status code is returned response.raise_for_status() # Return requests.Response object return response except Exception as err: msg = str(err) log and log.error(msg) raise IntegrationError(msg)
def execute_call(self, verb, url, payload={}, log=None, basicauth=None, verify_flag=True, headers=None, proxies=None, timeout=None, resp_type='json', callback=None): """ Function: perform the http API call. Different types of http operations are supported: GET, HEAD, PATCH, POST, PUT, DELETE Errors raise IntegrationError If a callback method is provided, then it's called to handle the error When using argument 'json' Content-Type will automatically be set to "application/json" by requests lib. :param verb: GET, HEAD, PATCH, POST, PUT, DELETE :param url: :param basicauth: used for basic authentication - (user, password) :param payload: :param log: optional log statement :param verify_flag: True/False - False used for debugging generally :param headers: dictionary of http headers :param proxies: http and https proxies for call :param timeout: timeout before call should abort :param resp_type: type of output to return: json, text, bytes :param callback: callback routine used to handle errors :return: json of returned data """ try: (payload and log) and log.debug(payload) if verb.lower() not in ('get', 'post', 'put', 'patch', 'delete', 'head'): raise IntegrationError("unknown verb {}".format(verb)) if proxies is None: proxies = self.get_proxies() if verb.lower() == 'post': content_type = get_case_insensitive_key_value( headers, "Content-Type") if is_payload_in_json(content_type): resp = requests.request(verb.upper(), url, verify=verify_flag, headers=headers, json=payload, auth=basicauth, timeout=timeout, proxies=proxies) else: resp = requests.request(verb.upper(), url, verify=verify_flag, headers=headers, data=payload, auth=basicauth, timeout=timeout, proxies=proxies) else: resp = requests.request(verb.upper(), url, verify=verify_flag, headers=headers, params=payload, auth=basicauth, timeout=timeout, proxies=proxies) if resp is None: raise IntegrationError('no response returned') # custom handler for response handling? if callback: return callback(resp) # standard error handling if resp.status_code >= 300: # get the result # log resp.status_code in case resp.text isn't available raise IntegrationError("status_code: {}, msg: {}".format( resp.status_code, resp.text if resp.text else "N/A")) # check if anything returned log and log.debug(resp.text) # get the result if resp_type == 'json': r = resp.json() elif resp_type == 'text': r = resp.text elif resp_type == 'bytes': r = resp.content else: raise IntegrationError( "incorrect response type: {}".format(resp_type)) # Produce a IntegrationError with the return value return r # json object needed, not a string representation except Exception as err: msg = str(err) log and log.error(msg) raise IntegrationError(msg)
def _fn_cs_falcon_search_function(self, event, *args, **kwargs): """Function that queries your CrowdStrike Falcon Hosts for a list of Devices using a Filter and/or Query. If Devices are found they are returned as a Python List""" err_msg = None log = logging.getLogger(__name__) try: # Instansiate helper (which gets appconfigs) cs_helper = CrowdStrikeHelper(self.function_opts) # Get the function inputs: fn_inputs = { "cs_filter_string": cs_helper.get_function_input(kwargs, "cs_filter_string", True), # text (optional) "cs_query": cs_helper.get_function_input(kwargs, "cs_query", True) # text (optional) } # Create new Function ResultPayload with appconfigs and function inputs payload = ResultPayload(CrowdStrikeHelper.app_config_section, **fn_inputs) # Get crowdstrike filter and query cs_filter_string = fn_inputs.get("cs_filter_string") cs_query = fn_inputs.get("cs_query") # At least on of them has to be defined if cs_filter_string is None and cs_query is None: raise IntegrationError( "Function Input cs_filter_string or cs_query must be defined" ) yield StatusMessage("> Function Inputs OK") # Instansiate new RequestCommon object to facilitate CrowdStrike REST API calls rqc = RequestsCommon(self.opts, self.function_opts) # Fist we look ip device ids # Set the request URL and payload get_device_ids_url = "{0}{1}".format( cs_helper.bauth_base_url, "/devices/queries/devices/v1") get_device_ids_payload = {} if cs_filter_string is not None: get_device_ids_payload["filter"] = cs_filter_string if cs_query is not None: get_device_ids_payload["q"] = cs_query yield StatusMessage( u'> Searching CrowdStrike for devices. Filter: "{0}" Query: {1}' .format(cs_helper.str_to_unicode(cs_filter_string), cs_query)) # Make GET request for device_ids get_device_ids_response = rqc.execute_call( verb="GET", url=get_device_ids_url, payload=get_device_ids_payload, basicauth=(cs_helper.bauth_api_uuid, cs_helper.bauth_api_key), headers=cs_helper.json_header) device_ids = get_device_ids_response.get("resources", []) if len(device_ids) > 0: # Then we get device_details for each device_id yield StatusMessage("> Devices found. Getting device details") get_device_details_url = "{0}{1}".format( cs_helper.bauth_base_url, "/devices/entities/devices/v1") get_device_details_payload = {"ids": device_ids} get_device_details_response = rqc.execute_call( verb="GET", url=get_device_details_url, payload=get_device_details_payload, basicauth=(cs_helper.bauth_api_uuid, cs_helper.bauth_api_key), headers=cs_helper.json_header) device_details = get_device_details_response.get( "resources", []) if len(device_details) > 0: yield StatusMessage( "> Device details received. Finishing...") # For each device, convert their string timestamps to utc_time in ms for device in device_details: device[ "agent_local_time"] = cs_helper.timestamp_to_ms_epoch( device.get("agent_local_time"), timestamp_format="%Y-%m-%dT%H:%M:%S.%fZ") device["first_seen"] = cs_helper.timestamp_to_ms_epoch( device.get("first_seen")) device[ "modified_timestamp"] = cs_helper.timestamp_to_ms_epoch( device.get("modified_timestamp")) device["last_seen"] = cs_helper.timestamp_to_ms_epoch( device.get("last_seen")) payload = payload.done(True, device_details) else: err_msg = u'> Could not get device details from CrowdStrike. Filter: "{0}" Query: {1}'.format( cs_helper.str_to_unicode(cs_filter_string), cs_query) yield StatusMessage(err_msg) payload = payload.done(False, None, reason=err_msg) else: err_msg = u'> No devices found in CrowdStrike. Filter: "{0}" Query: {1}'.format( cs_helper.str_to_unicode(cs_filter_string), cs_query) yield StatusMessage(err_msg) payload = payload.done(False, None, reason=err_msg) results = payload log.debug("RESULTS: %s", results) log.info("Complete") # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: yield FunctionError()
def execute(self, method, url, timeout=None, proxies=None, callback=None, **kwargs): """ Constructs and sends a request. Returns a `requests.Response <https://docs.python-requests.org/en/latest/api/#requests.Response>`_ object. This uses the ``requests.request()`` function to make a call. The inputs are mapped to this function. See `requests.request() <https://docs.python-requests.org/en/latest/api/#requests.request>`_ for information on any parameters available, but not documented here. :param timeout: Number of seconds to wait for the server to send data before sending a float or a timeout tuple (connect timeout, read timeout). *See requests docs for more*. If ``None`` it looks in the ``[integrations]`` section of your app.config for the ``timeout`` setting. :type timeout: float or tuple :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. The mapping protocol must be in the format: .. code-block:: { "https_proxy": "https://localhost:8080, "http_proxy": "http://localhost:8080 } :type proxies: dict :param callback: (Optional) Once a response is received from the endpoint, return this callback function passing in the ``response`` as its only parameter. Can be used to specifically handle errors. :type callback: function :return: the ``response`` from the endpoint or return ``callback`` if defined. :rtype: `requests.Response <https://docs.python-requests.org/en/latest/api/#requests.Response>`_ object or ``callback`` function. """ try: if method.lower() not in ('get', 'post', 'put', 'patch', 'delete', 'head', 'options'): raise IntegrationError("unknown method {}".format(method)) # If proxies was not set check if they are set in the config if proxies is None: proxies = self.get_proxies() if timeout is None: timeout = self.get_timeout() # Log the parameter inputs that are not None args_dict = locals() # When debugging execute_call_v2 in PyCharm you may get an exception when executing the for-loop: # Dictionary changed size during iteration. # To work around this while debugging you can change the following line to: # args = list(args_dict.keys()) args = args_dict.keys() for k in args: if k != "self" and k != "kwargs" and args_dict[k] is not None: log.debug(" {}: {}".format(k, args_dict[k])) # Pass request to requests.request() function response = requests.request(method, url, timeout=timeout, proxies=proxies, **kwargs) # Debug logging log.debug(response.status_code) log.debug(response.content) # custom handler for response handling # set callback to be the name of the method you would like to call # to do your custom error handling and return the response if callback: return callback(response) # Raise error is bad status code is returned response.raise_for_status() # Return requests.Response object return response except Exception as err: msg = str(err) log.error(msg) raise IntegrationError(msg)