Esempio n. 1
0
    def _bit9_approval_request_query_function(self, event, *args, **kwargs):
        """Function: Return approval requests that match the given criteria."""
        try:
            validate_fields(["bit9_query"], kwargs)
            # Get the function parameters:
            bit9_query = kwargs.get("bit9_query")  # text

            log.info(u"bit9_query: %s", bit9_query)

            # Query example: 'id:6' (see https://<server>/api/bit9platform/v1 for details)
            bit9_client = CbProtectClient(self.options)
            results = bit9_client.query_approval_request(bit9_query)

            pretty_string = json.dumps(results,
                                       sort_keys=True,
                                       indent=4,
                                       separators=(',', ':'))

            # Query results should be a list
            if isinstance(results, list):
                log.info("%d results", len(results))
                results = {
                    "count": len(results),
                    "items": results,
                    "pretty_results": pretty_string
                }
                log.debug(results)
            else:
                log.warn(u"Expected a list but received:")
                log.warn(results)

            # Produce a FunctionResult with the results
            yield FunctionResult(results)
        except Exception as err:
            log.error(err)
            yield FunctionError(err)
Esempio n. 2
0
class Bit9PollComponent(ResilientComponent):
    """
    Event-driven polling for CarbonBlack Protection approval requests
    """

    # This doesn't listen to Action Module, only its internal channel for timer events
    # But we still inherit from ResilientComponent so we get a REST client etc
    channel = BIT9_POLL_CHANNEL

    def __init__(self, opts):
        """constructor provides access to the configuration options"""
        super(Bit9PollComponent, self).__init__(opts)
        self.log = logging.getLogger(__name__)
        self._load_options(opts)

        # Add the timestamp-parse function to the global JINJA environment
        env = environment()
        env.globals.update({"timestamp_to_millis": timestamp_to_millis})
        env.filters.update({"timestamp_to_millis": timestamp_to_millis})

        # Set up a one-off timer for polling the first time
        if self.escalation_interval:
            self.log.info(
                u"CbProtect escalation initialized, polling interval %s seconds",
                self.escalation_interval)
            Timer(min((self.escalation_interval, 5)), Poll(),
                  persist=False).register(self)

    @handler("reload")
    def _reload(self, event, opts):
        """Configuration options have changed, save new values"""
        self._load_options(opts)

    @handler("Poll")
    def _poll(self, event):
        """Handle the timer"""
        self.log.debug("CbProtect poll timer")
        self._escalate()

    @handler("PollCompleted")
    def _poll_completed(self, event):
        """Set up the next timer"""
        self.log.debug("CbProtect poll completed")
        Timer(self.escalation_interval, Poll(), persist=False).register(self)

    def _load_options(self, opts):
        """Read options from config"""
        self.options = opts["fn_cb_protection"]

        self.bit9_client = CbProtectClient(self.options)

        # Timer interval (seconds).  Default 10 minutes.
        self.escalation_interval = int(
            self.options.get("escalation_interval", 600))
        if self.escalation_interval == 0:
            self.log.warn(
                u"CbProtect escalation interval is not configured.  Automated escalation is disabled."
            )
            return

        # Conditions for which incidents are escalated
        # By default this is "all unresolved approval requests"
        self.escalation_query = self.options.get("escalation_query",
                                                 "resolution:0")
        self.log.debug(u"escalation_query: {}".format(self.escalation_query))

    def _escalate(self):
        """Query the CbProtect server for approval requests, and raise them to Resilient"""
        self.log.info(u"Getting list of open approval requests")

        try:
            # This just queries for all requests that match the escalation conditions.
            # For cases with many thousands of open requests, a more scalable approach could
            # use "paged" queries, i.e. send a "limit" and then process each page of results.
            results = self.bit9_client.query_approval_request(
                self.escalation_query)

            # Query results should be a list
            if not isinstance(results, list):
                self.log.warn(u"Query produced unexpected value: %s", results)
                return

            self.log.info("%d results", len(results))
            self.log.debug(results)

            r_incidents = []
            if len(results) > 0:
                # Some (many!) of these approval requests will already have been escalated to Resilient.
                # For efficiency, find them and filter them out from this batch.
                # Then we're left only with "un-escalated" incidents.
                req_ids = [result["id"] for result in results]
                query_uri = u"/incidents/query?return_level=normal&field_handle={}".format(
                    REQUEST_ID_FIELDNAME)
                query = {
                    'filters': [{
                        'conditions': [{
                            'field_name':
                            'properties.{}'.format(REQUEST_ID_FIELDNAME),
                            'method':
                            'in',
                            'value':
                            req_ids
                        }, {
                            'field_name': 'plan_status',
                            'method': 'equals',
                            'value': 'A'
                        }]
                    }]
                }
                self.log.debug(query)
                try:
                    r_incidents = self.rest_client().post(query_uri, query)
                except SimpleHTTPException:
                    # Some versions of Resilient 30.2 onward have a bug that prevents query for numeric fields.
                    # To work around this issue, let's try a different query, and filter the results. (Expensive!)
                    query_uri = u"/incidents/query?return_level=normal&field_handle={}".format(
                        REQUEST_ID_FIELDNAME)
                    query = {
                        'filters': [{
                            'conditions': [{
                                'field_name':
                                'properties.{}'.format(REQUEST_ID_FIELDNAME),
                                'method':
                                'has_a_value'
                            }, {
                                'field_name': 'plan_status',
                                'method': 'equals',
                                'value': 'A'
                            }]
                        }]
                    }
                    self.log.debug(query)
                    r_incidents_tmp = self.rest_client().post(query_uri, query)
                    r_incidents = [
                        r_inc for r_inc in r_incidents_tmp
                        if r_inc["properties"].get(REQUEST_ID_FIELDNAME) in
                        req_ids
                    ]

            escalated_ids = [
                r_inc["properties"].get(REQUEST_ID_FIELDNAME)
                for r_inc in r_incidents
            ]

            unescalated_requests = [
                result for result in results
                if str(result["id"]) not in escalated_ids
            ]

            # Process each approval-request in the batch
            for req in unescalated_requests:
                self.fire(ProcessApprovalRequest(request=req))

            self.log.info(u"Processed all approval requests")

        except Exception as err:
            raise err
        finally:
            # We always want to reset the timer to wake up, no matter failure or success
            self.fire(PollCompleted())

    """Queries resilient for if an incident has already been created for the approval request"""

    def _find_resilient_incident_for_req(self, req_id):
        r_incidents = []
        query_uri = "/incidents/query?return_level=partial"
        query = {
            'filters': [{
                'conditions': [{
                    'field_name':
                    'properties.{}'.format(REQUEST_ID_FIELDNAME),
                    'method':
                    'equals',
                    'value':
                    req_id
                }, {
                    'field_name': 'plan_status',
                    'method': 'equals',
                    'value': 'A'
                }]
            }],
            "sorts": [{
                "field_name": "create_date",
                "type": "desc"
            }]
        }
        try:
            r_incidents = self.rest_client().post(query_uri, query)
        except SimpleHTTPException:
            # Some versions of Resilient 30.2 onward have a bug that prevents query for numeric fields.
            # To work around this issue, let's try a different query, and filter the results. (Expensive!)
            query_uri = u"/incidents/query?return_level=normal&field_handle={}".format(
                REQUEST_ID_FIELDNAME)
            query = {
                'filters': [{
                    'conditions': [{
                        'field_name':
                        'properties.{}'.format(REQUEST_ID_FIELDNAME),
                        'method':
                        'has_a_value'
                    }, {
                        'field_name': 'plan_status',
                        'method': 'equals',
                        'value': 'A'
                    }]
                }]
            }
            self.log.debug(query)
            r_incidents_tmp = self.rest_client().post(query_uri, query)
            r_incidents = [
                r_inc for r_inc in r_incidents_tmp
                if r_inc["properties"].get(REQUEST_ID_FIELDNAME) == req_id
            ]
        if len(r_incidents) > 0:
            return r_incidents[0]
        return None

    @handler("ProcessApprovalRequest")
    def _process_approval_request(self, event):
        # Process one approval request
        log = self.log
        request = event.request
        request_id = request["id"]

        # special "test the process by escalating a single request" mode
        test_single_request = self.options.get("test_single_request")
        if test_single_request:
            if str(request_id) not in str(test_single_request).split(","):
                log.info(u"Skipping request %s, test", request_id)
                return

        # Find the Resilient incident corresponding to this CbProtect approval request (if available)
        resilient_incident = self._find_resilient_incident_for_req(request_id)
        if resilient_incident:
            log.info(u"Skipping request %s, already escalated", request_id)
            return

        log.info(u"Processing request %s", request_id)
        try:
            # Create a new Resilient incident from this approval request
            # using a JSON (JINJA2) template file
            template_file_path = self.options.get("template_file")
            if template_file_path and not os.path.exists(template_file_path):
                log.warn(u"Template file '%s' not found.", template_file_path)
                template_file_path = None
            if not template_file_path:
                # Use the template file installed by this package
                template_file_path = resource_filename(
                    Requirement("fn-cb-protection"),
                    "fn_cb_protection/data/template.jinja")
                if not os.path.exists(template_file_path):
                    raise Exception(u"Template file '{}' not found".format(
                        template_file_path))

            log.info(u"Template file: %s", template_file_path)
            with open(template_file_path, "r") as definition:
                escalate_template = definition.read()

            # Render the template.  Be sure to set the CbProtect ID in the result!
            new_resilient_inc = render_json(escalate_template, request)
            new_resilient_inc["properties"][REQUEST_ID_FIELDNAME] = request_id

            log.debug(new_resilient_inc)
            inc = self.rest_client().post("/incidents", new_resilient_inc)
            rs_inc_id = inc["id"]
            message = u"Created incident {} for CbProtect {}".format(
                rs_inc_id, request_id)
            log.info(message)

        except Exception as exc:
            log.exception(exc)
            raise