Exemplo n.º 1
0
    def _sentinel_add_incident_comment_function(self, event, *args, **kwargs):
        """Function: Create a comment for a given Sentinel incident"""
        try:
            yield StatusMessage("Starting 'sentinel_add_incident_comment'")
            validate_fields([
                "sentinel_profile", "sentinel_incident_id",
                "sentinel_incident_comment"
            ], kwargs)

            rc = ResultPayload(PACKAGE_NAME, **kwargs)

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

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

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

            # do not resync comments originating from Sentinel
            if FROM_SENTINEL_COMMENT_HDR in sentinel_incident_comment or SENT_TO_SENTINEL_HDR in sentinel_incident_comment:
                yield StatusMessage(
                    "Bypassing synchronization of note: {}".format(
                        sentinel_incident_comment))
                result = {}
                reason = None
                status = False
            else:
                profile_data = self.sentinel_profiles.get_profile(
                    sentinel_profile)
                result, status, reason = sentinel_api.create_comment(
                    profile_data, sentinel_incident_id,
                    clean_html(sentinel_incident_comment))
                if status:
                    yield StatusMessage("Sentinel comment added to incident: {}"\
                        .format(sentinel_incident_id))
                else:
                    yield StatusMessage("Sentinel comment failure for incident {}: {}"\
                        .format(sentinel_incident_id, reason))

            yield StatusMessage("Finished 'sentinel_add_incident_comment'")

            results = rc.done(status, result, reason=reason)

            # Produce a FunctionResult with the results
            yield FunctionResult(results)
        except Exception:
            yield FunctionError()
Exemplo n.º 2
0
    def _sentinel_update_incident_function(self, event, *args, **kwargs):    # pylint: disable=unused-argument
        """Ingests data of any type that can be sent to a Resilient message destination"""
        # dismiss none Action events
        if not isinstance(event, ActionMessage):
            return

        # make sure to only handle incident changes
        if event.message['object_type'] != INCIDENT_TYPE:
            return

        # get the incident data
        resilient_incident = event.message['incident']

        validate_fields(["sentinel_profile", SENTINEL_INCIDENT_NUMBER], resilient_incident['properties'])

        # Get the function parameters:
        sentinel_profile = resilient_incident['properties'].get("sentinel_profile")  # text
        sentinel_incident_id = resilient_incident['properties'].get(SENTINEL_INCIDENT_NUMBER)  # text

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

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

        profile_data = self.sentinel_profiles.get_profile(sentinel_profile)

        # is this SOAR incident active or closed?
        if resilient_incident["plan_status"] == "A":
          template = profile_data.get("sentinel_update_incident_template")
          default_template = DEFAULT_SENTINEL_UPDATE_INCIDENT_TEMPLATE
        else:
          template = profile_data.get("sentinel_close_incident_template")
          default_template = DEFAULT_SENTINEL_CLOSE_INCIDENT_TEMPLATE


        incident_payload = self.jinja_env.make_payload_from_template(
                                template,
                                default_template,
                                resilient_incident)

        result, status, reason = sentinel_api.create_update_incident(
                                                profile_data,
                                                sentinel_incident_id,
                                                incident_payload
                                              )

        if status:
            log.info("Sentinel incident updated. incident: %s", result['properties']['incidentNumber'])
        else:
            log.error("Sentinel incident failure for incident %s: %s", sentinel_incident_id, reason)
Exemplo n.º 3
0
    def _sentinel_get_incident_entities_function(self, event, *args, **kwargs):
        """Function: Get the Entities associated with a Sentinel Incident"""
        try:
            validate_fields(["sentinel_profile", "sentinel_incident_id"],
                            kwargs)

            yield StatusMessage("Starting 'sentinel_get_incident_entities'")

            rc = ResultPayload(PACKAGE_NAME, **kwargs)

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

            log = logging.getLogger(__name__)
            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)

            profile_data = self.sentinel_profiles.get_profile(sentinel_profile)
            # read all entities associated with a Sentinel incident
            result, status, reason = sentinel_api.get_incident_entities(
                profile_data, sentinel_incident_id)

            log.debug(result)
            # iterate over the alerts and get all the entities
            entities = {}
            if status:
                for alert in result['value']:
                    log.debug("Alert: %s", alert['name'])
                    entity_result, entity_status, entity_reason = \
                        sentinel_api.get_incident_alert_entities(alert['properties']['relatedResourceId'])
                    # organize entities using the key of the alert_id
                    if entity_status:
                        entities[
                            alert['name']] = entity_result['value']['entities']
                    else:
                        reason = entity_reason

            yield StatusMessage("Finished 'sentinel_get_incident_entities'")

            results = rc.done(status, entities, reason=reason)

            # Produce a FunctionResult with the results
            yield FunctionResult(results)
        except Exception:
            yield FunctionError()
    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()
def selftest_function(opts):
    """
    Placeholder for selftest function. An example use would be to test package api connectivity.
    Suggested return values are be unimplemented, success, or failure.
    """
    app_configs = opts.get(PACKAGE_NAME, {})

    validate_fields(["tenant_id", "client_id", "app_secret"], app_configs)

    sentinel_api = SentinelAPI(app_configs['tenant_id'],
                               app_configs['client_id'],
                               app_configs['app_secret'], opts, app_configs)

    reason = None
    try:
        state = "success" if sentinel_api._authenticate() else "failure"
    except IntegrationError as err:
        state = "failure"
        reason = str(err)

    return {"state": state, "reason": reason}
Exemplo n.º 6
0
    def _sentinel_get_incident_alerts_function(self, event, *args, **kwargs):
        """Function: Get the alerts associated with a Sentinel Incident"""
        try:
            validate_fields(["sentinel_profile", "sentinel_incident_id"],
                            kwargs)

            yield StatusMessage("Starting 'sentinel_get_incident_alerts'")

            rc = ResultPayload(PACKAGE_NAME, **kwargs)

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

            log = logging.getLogger(__name__)
            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)

            profile_data = self.sentinel_profiles.get_profile(sentinel_profile)
            # read all alerts associated with a Sentinel incident
            result, status, reason = sentinel_api.get_incident_alerts(
                profile_data, sentinel_incident_id)

            log.debug(result)  #TODO

            yield StatusMessage("Finished 'sentinel_get_incident_alerts'")

            results = rc.done(status, result, reason=reason)

            # Produce a FunctionResult with the results
            yield FunctionResult(results)
        except Exception:
            yield FunctionError()
Exemplo n.º 7
0
    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())
Exemplo n.º 8
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