Example #1
0
    def _bit9_file_instance_update_function(self, event, *args, **kwargs):
        """Function: Update the approval state of a file instance"""
        try:
            validate_fields(
                ["bit9_file_instance_id", "bit9_file_instance_localstate"],
                kwargs)
            # Get the function parameters:
            bit9_file_instance_id = kwargs.get(
                "bit9_file_instance_id")  # number
            bit9_file_instance_localstate = kwargs.get(
                "bit9_file_instance_localstate")  # number

            log.info(u"bit9_file_instance_id: %s", bit9_file_instance_id)
            log.info(u"bit9_file_instance_localstate: %s",
                     bit9_file_instance_localstate)

            payload = {"localState": bit9_file_instance_localstate}

            bit9_client = CbProtectClient(self.options)
            results = bit9_client.update_file_instance(bit9_file_instance_id,
                                                       payload)

            log.info("Done")
            log.debug(results)

            # Produce a FunctionResult with the results
            yield FunctionResult(results)
        except Exception as err:
            log.error(err)
            yield FunctionError(err)
Example #2
0
    def _bit9_file_delete_function(self, event, *args, **kwargs):
        """Function: Delete a file from all systems or a specific system"""
        try:
            validate_fields(["bit9_file_action"], kwargs)
            # Get the function parameters:
            yield StatusMessage("Deleting file...")
            bit9_file_action = self.get_select_param(
                kwargs.get("bit9_file_action"))  # select
            bit9_computer_id = kwargs.get("bit9_computer_id")  # number

            bit9_file_catalog_id = kwargs.get("bit9_file_catalog_id")  # number
            bit9_file_hash = kwargs.get("bit9_file_hash")  # text
            bit9_file_name = kwargs.get("bit9_file_name")  # text
            if not bit9_file_catalog_id and not bit9_file_hash and not bit9_file_name:
                raise FunctionError(
                    "bit9_file_catalog_id, bit9_file_hash, or bit9_file_name mush be set in order to "
                    "run this function.")

            log.info("bit9_file_action: %s", bit9_file_action)
            log.info("bit9_computer_id: %s", bit9_computer_id)
            log.info("bit9_file_catalog_id: %s", bit9_file_catalog_id)
            log.info("bit9_file_hash: %s", bit9_file_hash)
            log.info("bit9_file_name: %s", bit9_file_name)

            # Supported file actions
            supported_file_actions = {
                "DeleteFileByName": 8,
                "DeleteFileByHash": 9
            }

            payload = {}
            if bit9_file_catalog_id:
                payload["fileCatalogId"] = bit9_file_catalog_id
            if bit9_computer_id is not None:
                payload["computerId"] = bit9_computer_id
            if bit9_file_action in supported_file_actions:
                payload["action"] = supported_file_actions.get(
                    bit9_file_action)
            else:
                raise FunctionError(
                    u"{} is not one of the support file actions: {}".format(
                        bit9_file_name, list(supported_file_actions.keys())))
            if bit9_file_hash:
                payload["hash"] = bit9_file_hash
            if bit9_file_name:
                payload["fileName"] = bit9_file_name

            log.debug("uPayload: {}".format(payload))
            bit9_client = CbProtectClient(self.options)
            results = bit9_client.delete_file(payload)

            log.info("Done")
            yield StatusMessage("File deleted...")
            log.debug(results)

            # Produce a FunctionResult with the results
            yield FunctionResult(results)
        except Exception as err:
            log.error(err)
            yield FunctionError(err)
    def _bit9_approval_request_get_function(self, event, *args, **kwargs):
        """Function: Get an approval request by ID"""
        try:
            validate_fields(["bit9_approval_request_id"], kwargs)
            # Get the function parameters:
            bit9_approval_request_id = kwargs.get(
                "bit9_approval_request_id")  # number

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

            bit9_client = CbProtectClient(self.options)
            results = bit9_client.get_approval_request(
                bit9_approval_request_id)

            results[
                "details_url"] = u"https://{}/approval-request-details.php?request_id={}".format(
                    bit9_client.server, bit9_approval_request_id)
            log.info(u"Request Status :%d", results.get("status"))
            log.debug(results)

            # Produce a FunctionResult with the results
            yield FunctionResult(results)
        except Exception as err:
            log.error(err)
            yield FunctionError(err)
Example #4
0
    def _bit9_file_rule_query_function(self, event, *args, **kwargs):
        """Function: Return file rules 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_file_rule(bit9_query)

            # Query results should be a list
            if isinstance(results, list):
                log.info("%d results", len(results))
                results = {"count": len(results), "items": results}
                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)
    def _bit9_file_rule_update_function(self, event, *args, **kwargs):
        """Function: Create or update a File Rule"""
        try:
            # Get the function parameters:
            bit9_file_rule_id = kwargs.get("bit9_file_rule_id")  # number
            bit9_file_catalog_id = kwargs.get("bit9_file_catalog_id")  # number
            bit9_file_rule_name = kwargs.get("bit9_file_rule_name")  # text
            bit9_file_rule_description = kwargs.get("bit9_file_rule_description")  # text
            bit9_file_rule_filestate = kwargs.get("bit9_file_rule_filestate")  # number
            bit9_file_rule_sourcetype = kwargs.get("bit9_file_rule_sourcetype")  # number
            bit9_file_rule_policyids = kwargs.get("bit9_file_rule_policyids")  # text
            bit9_file_rule_hash = kwargs.get("bit9_file_rule_hash")  # text

            log.info(u"bit9_file_rule_id: %s", bit9_file_rule_id)
            log.info(u"bit9_file_catalog_id: %s", bit9_file_catalog_id)
            log.info(u"bit9_file_rule_name: %s", bit9_file_rule_name)
            log.info(u"bit9_file_rule_description: %s", bit9_file_rule_description)
            log.info(u"bit9_file_rule_filestate: %s", bit9_file_rule_filestate)
            log.info(u"bit9_file_rule_sourcetype: %s", bit9_file_rule_sourcetype)
            log.info(u"bit9_file_rule_policyids: %s", bit9_file_rule_policyids)
            log.info(u"bit9_file_rule_hash: %s", bit9_file_rule_hash)

            # This can be called with a "file rule id", to update an existing rule.
            # Or, it can be called with no file rule id, to create a new rule for a hash or filename.
            if not (bit9_file_rule_name or bit9_file_rule_description or bit9_file_rule_filestate
                    or bit9_file_rule_sourcetype or bit9_file_rule_hash or bit9_file_rule_policyids):
                raise FunctionError("Specify at least one attribute to update: bit9_file_rule_name, "
                                    "bit9_file_rule_description, bit9_file_rule_filestate, bit9_file_rule_sourcetype, "
                                    "bit9_file_rule_hash or bit9_file_rule_policyids")

            payload = {}
            if bit9_file_catalog_id:
                payload["fileCatalogId"] = bit9_file_catalog_id
            if bit9_file_rule_name:
                payload["name"] = bit9_file_rule_name
            if bit9_file_rule_description:
                payload["description"] = bit9_file_rule_description
            if bit9_file_rule_filestate:
                payload["fileState"] = bit9_file_rule_filestate
            if bit9_file_rule_sourcetype:
                payload["sourceType"] = bit9_file_rule_sourcetype
            if bit9_file_rule_policyids:
                payload["policyIds"] = bit9_file_rule_policyids
            if bit9_file_rule_hash:
                payload["hash"] = bit9_file_rule_hash

            bit9_client = CbProtectClient(self.options)
            results = bit9_client.update_file_rule(bit9_file_rule_id, payload)

            log.info("Done")
            log.debug(results)

            # Produce a FunctionResult with the results
            yield FunctionResult(results)
        except Exception as err:
            log.error(err)
            yield FunctionError(err)
    def _bit9_file_instance_query_function(self, event, *args, **kwargs):
        """Function: Return file instance objects that match the given criteria."""
        try:
            # Get the function parameters:
            bit9_computer_id = kwargs.get("bit9_computer_id")  # number
            bit9_file_catalog_id = kwargs.get("bit9_file_catalog_id")  # number
            bit9_file_path = kwargs.get("bit9_file_path")  # text
            bit9_file_instance_localstate = kwargs.get(
                "bit9_file_instance_localstate")  # number

            log.info(u"bit9_computer_id: %s", bit9_computer_id)
            log.info(u"bit9_file_catalog_id: %s", bit9_file_catalog_id)
            log.info(u"bit9_file_path: %s", bit9_file_path)
            log.info(u"bit9_file_instance_localstate: %s",
                     bit9_file_instance_localstate)

            # At least the file catalog id or computer id must be specified.
            if bit9_computer_id is None and bit9_file_catalog_id is None:
                raise ValueError(
                    "At least the file catalog id or computer id must be specified."
                )

            # Construct the query string.
            query = []
            if bit9_computer_id:
                query.append(u"computerId:{}".format(bit9_computer_id))
            if bit9_file_catalog_id:
                query.append(u"fileCatalogId:{}".format(bit9_file_catalog_id))
            if bit9_file_path:
                query.append(u"pathName:{}".format(escape(bit9_file_path)))
            if bit9_file_instance_localstate:
                query.append(
                    u"localState:{}".format(bit9_file_instance_localstate))
            bit9_query = u"&q=".join(query)
            bit9_client = CbProtectClient(self.options)
            results = bit9_client.query_file_instance(bit9_query)

            # Query results should be a list
            if isinstance(results, list):
                log.info("%d results", len(results))
                results = {"count": len(results), "item_list": results}
                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)
Example #7
0
    def _bit9_approval_request_update_function(self, event, *args, **kwargs):
        """Function: Update an approval request"""
        try:
            validate_fields(["bit9_approval_request_id"], kwargs)
            # Get the function parameters:
            bit9_approval_request_id = kwargs.get(
                "bit9_approval_request_id")  # number
            bit9_approval_request_resolution = kwargs.get(
                "bit9_approval_request_resolution")  # number
            bit9_approval_request_resolution_comments = kwargs.get(
                "bit9_approval_request_resolution_comments")  # text
            bit9_approval_request_status = kwargs.get(
                "bit9_approval_request_status")  # number

            log.info("bit9_approval_request_id: %s", bit9_approval_request_id)
            log.info("bit9_approval_request_resolution: %s",
                     bit9_approval_request_resolution)
            log.info("bit9_approval_request_resolution_comments: %s",
                     bit9_approval_request_resolution_comments)
            log.info("bit9_approval_request_status: %s",
                     bit9_approval_request_status)

            payload = {}
            if bit9_approval_request_resolution:
                payload["resolution"] = bit9_approval_request_resolution
            if bit9_approval_request_resolution_comments:
                payload[
                    "resolutionComments"] = bit9_approval_request_resolution_comments
            if bit9_approval_request_status:
                payload["status"] = bit9_approval_request_status

            bit9_client = CbProtectClient(self.options)
            results = bit9_client.update_approval_request(
                bit9_approval_request_id, payload)

            results[
                "details_url"] = u"https://{}/approval-request-details.php?request_id={}".format(
                    bit9_client.server, bit9_approval_request_id)
            log.info("Request Status: %d", results.get("status"))
            log.debug(results)

            # Produce a FunctionResult with the results
            yield FunctionResult(results)
        except Exception as err:
            log.error(err)
            yield FunctionError(err)
Example #8
0
    def _bit9_file_rule_get_function(self, event, *args, **kwargs):
        """Function: Get a file rule by ID"""
        try:
            validate_fields(["bit9_file_rule_id"], kwargs)
            # Get the function parameters:
            bit9_file_rule_id = kwargs.get("bit9_file_rule_id")  # number

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

            bit9_client = CbProtectClient(self.options)
            results = bit9_client.get_file_rule(bit9_file_rule_id)

            log.info("Done")
            log.debug(results)

            # Produce a FunctionResult with the results
            yield FunctionResult(results)
        except Exception as err:
            log.error(err)
            yield FunctionError(err)
Example #9
0
    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 _bit9_file_catalog_get_function(self, event, *args, **kwargs):
        """Function: Get a file catalog item by ID"""
        try:
            validate_fields(["bit9_file_catalog_id"], kwargs)
            # Get the function parameters:
            bit9_file_catalog_id = kwargs.get("bit9_file_catalog_id")  # number

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

            bit9_client = CbProtectClient(self.options)
            results = bit9_client.get_file_catalog(bit9_file_catalog_id)

            results[
                "details_url"] = u"https://{}/file-details.php?antibody_id={}".format(
                    bit9_client.server, bit9_file_catalog_id)
            log.info(u"Effective State :%s", results.get("effectiveState"))
            log.debug(results)

            # Produce a FunctionResult with the results
            yield FunctionResult(results)
        except Exception as err:
            log.error(err)
            yield FunctionError(err)
Example #11
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)
Example #12
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