Example #1
0
    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))
Example #2
0
 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.")
Example #3
0
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)
Example #7
0
    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
Example #8
0
    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)
Example #11
0
    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()
Example #12
0
    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)