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()
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)
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}
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()
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())
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