def _sentinel_get_incident_comments_function(self, event, *args, **kwargs):
        """Function: Get Comments from a Sentinel Incident"""
        try:
            validate_fields(["sentinel_profile", "sentinel_incident_id"],
                            kwargs)

            yield StatusMessage("Starting 'sentinel_get_incident_comments'")

            rc = ResultPayload(PACKAGE_NAME, **kwargs)

            # Get the function parameters:
            incident_id = kwargs.get("incident_id")  # int
            sentinel_incident_id = kwargs.get("sentinel_incident_id")  # text
            sentinel_profile = kwargs.get("sentinel_profile")  # text

            log = logging.getLogger(__name__)
            log.info("incident_id: %s", incident_id)
            log.info("sentinel_incident_id: %s", sentinel_incident_id)
            log.info("sentinel_profile: %s", sentinel_profile)

            sentinel_api = SentinelAPI(self.options['tenant_id'],
                                       self.options['client_id'],
                                       self.options['app_secret'], self.opts,
                                       self.options)

            resilient_api = ResilientCommon(self.rest_client())

            profile_data = self.sentinel_profiles.get_profile(sentinel_profile)
            result, status, reason = sentinel_api.get_comments(
                profile_data, sentinel_incident_id)

            new_comments = []
            if status:
                new_comments = resilient_api.filter_resilient_comments(
                    incident_id, result['value'])

            yield StatusMessage("Finished 'sentinel_get_incident_comments'")

            results = rc.done(status, {"value": new_comments}, reason=reason)

            # Produce a FunctionResult with the results
            yield FunctionResult(results)
        except Exception:
            yield FunctionError()
Example #2
0
class SentinelPollerComponent(ResilientComponent):
    """
    Event-driven polling for Sentinel Incidents
    """

    # 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 = POLLER_CHANNEL

    def __init__(self, opts):
        """constructor provides access to the configuration options"""
        super(SentinelPollerComponent, self).__init__(opts)
        self.jinja_env = JinjaEnvironment()
        self.options = opts.get(PACKAGE_NAME, {})
        self.sentinel_profiles = SentinelProfiles(opts, self.options)

        self._load_options(opts)
        if self.polling_interval == 0:
            LOG.info(
                u"Sentinel poller interval is not configured.  Automated escalation is disabled."
            )
            return

        LOG.info(u"Sentinel poller initiated, polling interval %s",
                 self.polling_interval)
        Timer(self.polling_interval, Poll(), persist=False).register(self)

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

    @handler("Poll")
    def _poll(self, event):
        """Handle the timer"""
        LOG.info(u"Sentinel start polling.")
        self._escalate()

    @handler("PollCompleted")
    def _poll_completed(self, event):
        """Set up the next timer"""
        LOG.info(u"Sentinel poller complete.")
        Timer(self.polling_interval, Poll(), persist=False).register(self)

    def _load_options(self, opts):
        """Read options from config"""
        self.opts = opts
        self.options = opts.get(PACKAGE_NAME, {})

        # Validate required fields in app.config are set
        required_fields = [
            "azure_url", "client_id", "tenant_id", "app_secret",
            "sentinel_profiles"
        ]
        validate_fields(required_fields, self.options)

        self.polling_interval = int(self.options.get("polling_interval", 0))
        if not self.polling_interval:
            return

        # Create api client
        self.sentinel_client = SentinelAPI(self.options['tenant_id'],
                                           self.options['client_id'],
                                           self.options['app_secret'],
                                           self.opts, self.options)

        self.resilient_common = ResilientCommon(self.rest_client())

    def _escalate(self):
        """ This is the main logic of the poller
            Search for Sentinel Incidents and create associated cases in Resilient SOAR
        """
        try:
            # call Sentinel for each profile to get the incident list
            for profile_name, profile_data in self.sentinel_profiles.get_profiles(
            ).items():
                poller_start = datetime.datetime.utcnow()
                try:
                    LOG.info("polling profile: %s", profile_name)
                    result, status, reason = self.sentinel_client.query_incidents(
                        profile_data)

                    while status:
                        self._parse_results(result, profile_name, profile_data)

                        # more results? continue
                        if result.get("nextLink"):
                            LOG.debug("running nextLink")
                            result, status, reason = self.sentinel_client.query_next_incidents(
                                profile_data, result.get("nextLink"))
                        else:
                            break

                    # filter the incident list returned based on the criteria in a filter
                    if not status:
                        LOG.error("Error querying for incidents: %s, %s",
                                  reason, result)

                finally:
                    # set the last poller time for next cycle
                    profile_data['last_poller_time'] = poller_start

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

    def _parse_results(self, result, profile_name, profile_data):
        """[create resilient incidents if the result set hasn't already by created]

        Args:
            result ([dict]): [results for getting sentinel incidents]
            profile_name ([str]): [name of profile running]
            profile_data ([dict]): [profile settings]
        """
        for sentinel_incident in result.get("value", []):
            # determine if an incident already exists, used to know if create or update
            sentinel_incident_id, sentinel_incident_number = get_sentinel_incident_ids(
                sentinel_incident)
            resilient_incident = self.resilient_common.find_incident(
                sentinel_incident_id)

            new_incident_filters = get_profile_filters(
                profile_data['new_incident_filters'])
            result_resilient_incident = self._create_update_incident(
                profile_name, profile_data, sentinel_incident,
                resilient_incident, new_incident_filters)

            if result_resilient_incident:
                incident_id = result_resilient_incident['id']
                # get the sentinel comments and add to Resilient.
                # need to ensure not adding the comment more than once
                result, status, reason = self.sentinel_client.get_comments(
                    profile_data, sentinel_incident_id)
                new_comments = []
                if status:
                    new_comments = self.resilient_common.filter_resilient_comments(
                        incident_id, result['value'])
                    for comment in new_comments:
                        self.resilient_common.create_incident_comment(
                            incident_id, comment['name'],
                            comment['properties']['message'])
                else:
                    LOG.error("Error getting comments: %s", reason)


    def _create_update_incident(self, profile_name, profile_data,\
                                sentinel_incident, resilient_incident, new_incident_filters):
        """[perform the operations on the sentinel incident: create, update or close]

        Args:
            profile_name ([str]): [incident profile]
            profile_data ([dict]): [profile data]
            resilient_incident ([dict]): [existing resilient or none]
            new_incident_filters ([dict]): [filter to apply to new incidents]

        Returns:
            resilient_incident ([dict])
        """
        sentinel_incident_id, sentinel_incident_number = get_sentinel_incident_ids(
            sentinel_incident)
        # resilient incident found
        updated_resilient_incident = None
        if resilient_incident:
            resilient_incident_id = resilient_incident['id']
            if resilient_incident["plan_status"] == "C":
                LOG.info(
                    "Bypassing update to closed incident %s from Sentinel incident %s",
                    resilient_incident_id, sentinel_incident_number)
            elif sentinel_incident['properties']['status'] == "Closed":
                # close the incident
                incident_payload = self.jinja_env.make_payload_from_template(
                    profile_data.get("close_incident_template"),
                    DEFAULT_INCIDENT_CLOSE_TEMPLATE, sentinel_incident)
                updated_resilient_incident = self.resilient_common.close_incident(
                    resilient_incident_id, incident_payload)
                _ = self.resilient_common.create_incident_comment(
                    resilient_incident_id, None,
                    "Close synchronized from Sentinel")
                LOG.info("Closed incident %s from Sentinel incident %s",
                         resilient_incident_id, sentinel_incident_number)
            else:
                # update an incident incident
                incident_payload = self.jinja_env.make_payload_from_template(
                    profile_data.get("update_incident_template"),
                    DEFAULT_INCIDENT_UPDATE_TEMPLATE, sentinel_incident)
                updated_resilient_incident = self.resilient_common.update_incident(
                    resilient_incident_id, incident_payload)
                _ = self.resilient_common.create_incident_comment(
                    resilient_incident_id, None,
                    "Updates synchronized from Sentinel")
                LOG.info("Updated incident %s from Sentinel incident %s",
                         resilient_incident_id, sentinel_incident_number)
        else:
            # apply filters to only escalate certain incidents
            if check_incident_filters(sentinel_incident, new_incident_filters):
                # add in the profile to track
                sentinel_incident['resilient_profile'] = profile_name

                # create a new incident
                incident_payload = self.jinja_env.make_payload_from_template(
                    profile_data.get("create_incident_template"),
                    DEFAULT_INCIDENT_CREATION_TEMPLATE, sentinel_incident)
                updated_resilient_incident = self.resilient_common.create_incident(
                    incident_payload)
                LOG.info("Created incident %s from Sentinel incident %s",
                         updated_resilient_incident['id'],
                         sentinel_incident_number)
            else:
                LOG.info(
                    "Sentinel incident %s bypassed due to new_incident_filters",
                    sentinel_incident_number)
                updated_resilient_incident = None

        return updated_resilient_incident