Beispiel #1
0
    def run(self, event):
        """Run the alert processor!

        Args:
            event (dict): Lambda invocation event containing at least the rule name and alert ID.

        Returns:
            dict: Maps output (str) to whether it sent successfully (bool).
                An empty dict is returned if the Alert was improperly formatted.
        """
        # Grab the alert record from Dynamo (if needed).
        if set(event) == {'AlertID', 'RuleName'}:
            LOGGER.info('Retrieving %s from alerts table', event)
            alert_record = self.alerts_table.get_alert_record(event['RuleName'], event['AlertID'])
            if not alert_record:
                LOGGER.error('%s does not exist in the alerts table', event)
                return {}
        else:
            alert_record = event

        # Convert record to an Alert instance.
        try:
            alert = Alert.create_from_dynamo_record(alert_record)
        except AlertCreationError:
            LOGGER.exception('Invalid alert %s', event)
            return {}

        # Remove normalization key from the record.
        # TODO: Consider including this in at least some outputs, e.g. default Athena firehose
        if NORMALIZATION_KEY in alert.record:
            del alert.record[NORMALIZATION_KEY]

        result = self._send_to_outputs(alert)
        self._update_table(alert, result)
        return result
Beispiel #2
0
    def _establish_session(self, username, password):
        """Establish a cookie based Jira session via basic user auth.

        Args:
            username (str): The Jira username used for establishing the session
            password (str): The Jira password used for establishing the session

        Returns:
            str: Header value intended to be passed with every subsequent Jira request
                 or False if unsuccessful
        """
        login_url = os.path.join(self._base_url, self.LOGIN_ENDPOINT)
        auth_info = {'username': username, 'password': password}

        try:
            resp = self._post_request_retry(login_url,
                                            data=auth_info,
                                            headers=self._get_default_headers(),
                                            verify=False)
        except OutputRequestFailure:
            LOGGER.error("Failed to authenticate to Jira")
            return False

        resp_dict = resp.json()
        if not resp_dict:
            return False

        return '{}={}'.format(resp_dict['session']['name'],
                              resp_dict['session']['value'])
Beispiel #3
0
    def _create_dispatcher(self, output):
        """Create a dispatcher for the given output.

        Args:
            output (str): Alert output, e.g. "aws-sns:topic-name"

        Returns:
            OutputDispatcher: Based on the output type.
                Returns None if the output is invalid or not defined in the config.
        """
        try:
            service, descriptor = output.split(':')
        except ValueError:
            LOGGER.error(
                'Improperly formatted output [%s]. Outputs for rules must '
                'be declared with both a service and a descriptor for the '
                'integration (ie: \'slack:my_channel\')', output)
            return None

        if service not in self.config or descriptor not in self.config[service]:
            LOGGER.error('The output \'%s\' does not exist!', output)
            return None

        return StreamAlertOutput.create_dispatcher(service, self.region,
                                                   self.account_id,
                                                   self.prefix, self.config)
Beispiel #4
0
    def dispatch(self, **kwargs):
        """Send alert to a Kinesis Firehose Delivery Stream

        Keyword Args:
            descriptor (str): Service descriptor (ie: slack channel, pd integration)
            rule_name (str): Name of the triggered rule
            alert (dict): Alert relevant to the triggered rule

        Returns:
            bool: Indicates a successful or failed dispatch of the alert
        """
        @backoff.on_exception(backoff.fibo,
                              ClientError,
                              max_tries=self.MAX_BACKOFF_ATTEMPTS,
                              jitter=backoff.full_jitter,
                              on_backoff=backoff_handler,
                              on_success=success_handler,
                              on_giveup=giveup_handler)
        def _firehose_request_wrapper(json_alert, delivery_stream):
            """Make the PutRecord request to Kinesis Firehose with backoff

            Args:
                json_alert (str): The JSON dumped alert body
                delivery_stream (str): The Firehose Delivery Stream to send to

            Returns:
                dict: Firehose response in the format below
                    {'RecordId': 'string'}
            """
            return self.__aws_client__.put_record(
                DeliveryStreamName=delivery_stream,
                Record={'Data': json_alert})

        if self.__aws_client__ is None:
            self.__aws_client__ = boto3.client('firehose',
                                               region_name=self.region)

        json_alert = json.dumps(kwargs['alert'], separators=(',', ':')) + '\n'
        if len(json_alert) > self.MAX_RECORD_SIZE:
            LOGGER.error('Alert too large to send to Firehose: \n%s...',
                         json_alert[0:1000])
            return False

        delivery_stream = self.config[self.__service__][kwargs['descriptor']]
        LOGGER.info('Sending alert [%s] to aws-firehose:%s',
                    kwargs['rule_name'], delivery_stream)

        resp = _firehose_request_wrapper(json_alert, delivery_stream)

        if resp.get('RecordId'):
            LOGGER.info(
                'Alert [%s] successfully sent to aws-firehose:%s with RecordId:%s',
                kwargs['rule_name'], delivery_stream, resp['RecordId'])

        return self._log_status(resp, kwargs['descriptor'])
    def _log_status(cls, success):
        """Log the status of sending the alerts

        Args:
            success (bool or dict): Indicates if the dispatching of alerts was successful
        """
        if success:
            LOGGER.info('Successfully sent alert to %s', cls.__service__)
        else:
            LOGGER.error('Failed to send alert to %s', cls.__service__)

        return bool(success)
Beispiel #6
0
    def _log_status(cls, success, descriptor):
        """Log the status of sending the alerts

        Args:
            success (bool or dict): Indicates if the dispatching of alerts was successful
            descriptor (str): Service descriptor
        """
        if success:
            LOGGER.info('Successfully sent alert to %s:%s', cls.__service__,
                        descriptor)
        else:
            LOGGER.error('Failed to send alert to %s:%s', cls.__service__,
                         descriptor)
Beispiel #7
0
    def get_dispatcher(cls, service):
        """Returns the subclass that should handle this particular service

        Args:
            service (str): The service identifier for this output

        Returns:
            OutputDispatcher: Subclass of OutputDispatcher to use for sending alerts
        """
        try:
            return cls._outputs[service]
        except KeyError:
            LOGGER.error('Designated output service [%s] does not exist', service)
Beispiel #8
0
    def dispatch(self, **kwargs):
        """Send alert to Jira

        Args:
            **kwargs: consists of any combination of the following items:
                descriptor (str): Service descriptor (ie: slack channel, pd integration)
                rule_name (str): Name of the triggered rule
                alert (dict): Alert relevant to the triggered rule
        """
        creds = self._load_creds(kwargs['descriptor'])
        if not creds:
            return self._log_status(False, kwargs['descriptor'])

        issue_id = None
        comment_id = None
        issue_summary = 'StreamAlert {}'.format(kwargs['rule_name'])
        alert_body = '{{code:JSON}}{}{{code}}'.format(
            json.dumps(kwargs['alert']))
        self._base_url = creds['url']
        self._auth_cookie = self._establish_session(creds['username'],
                                                    creds['password'])

        # Validate successful authentication
        if not self._auth_cookie:
            return self._log_status(False, kwargs['descriptor'])

        # If aggregation is enabled, attempt to add alert to an existing issue. If a
        # failure occurs in this block, creation of a new Jira issue will be attempted.
        if creds.get('aggregate', '').lower() == 'yes':
            issue_id = self._get_existing_issue(issue_summary,
                                                creds['project_key'])
            if issue_id:
                comment_id = self._create_comment(issue_id, alert_body)
                if comment_id:
                    LOGGER.debug(
                        'Sending alert to an existing Jira issue %s with comment %s',
                        issue_id, comment_id)
                    return self._log_status(True, kwargs['descriptor'])
                else:
                    LOGGER.error(
                        'Encountered an error when adding alert to existing '
                        'Jira issue %s. Attempting to create new Jira issue.',
                        issue_id)

        # Create a new Jira issue
        issue_id = self._create_issue(issue_summary, creds['project_key'],
                                      creds['issue_type'], alert_body)
        if issue_id:
            LOGGER.debug('Sending alert to a new Jira issue %s', issue_id)

        return self._log_status(issue_id or comment_id, kwargs['descriptor'])
Beispiel #9
0
    def dispatch(self, alert, descriptor):
        """Send alert to Jira

        Args:
            alert (Alert): Alert instance which triggered a rule
            descriptor (str): Output descriptor

        Returns:
            bool: True if alert was sent successfully, False otherwise
        """
        creds = self._load_creds(descriptor)
        if not creds:
            return self._log_status(False, descriptor)

        issue_id = None
        comment_id = None
        issue_summary = 'StreamAlert {}'.format(alert.rule_name)
        alert_body = '{{code:JSON}}{}{{code}}'.format(
            json.dumps(alert.output_dict(), sort_keys=True))
        self._base_url = creds['url']
        self._auth_cookie = self._establish_session(creds['username'], creds['password'])

        # Validate successful authentication
        if not self._auth_cookie:
            return self._log_status(False, descriptor)

        # If aggregation is enabled, attempt to add alert to an existing issue. If a
        # failure occurs in this block, creation of a new Jira issue will be attempted.
        if creds.get('aggregate', '').lower() == 'yes':
            issue_id = self._get_existing_issue(issue_summary, creds['project_key'])
            if issue_id:
                comment_id = self._create_comment(issue_id, alert_body)
                if comment_id:
                    LOGGER.debug('Sending alert to an existing Jira issue %s with comment %s',
                                 issue_id,
                                 comment_id)
                    return self._log_status(True, descriptor)
                else:
                    LOGGER.error('Encountered an error when adding alert to existing '
                                 'Jira issue %s. Attempting to create new Jira issue.',
                                 issue_id)

        # Create a new Jira issue
        issue_id = self._create_issue(issue_summary,
                                      creds['project_key'],
                                      creds['issue_type'],
                                      alert_body)
        if issue_id:
            LOGGER.debug('Sending alert to a new Jira issue %s', issue_id)

        return self._log_status(issue_id or comment_id, descriptor)
Beispiel #10
0
    def _dispatch(self, alert, descriptor):
        """Send alert to a Kinesis Firehose Delivery Stream

        Args:
            alert (Alert): Alert instance which triggered a rule
            descriptor (str): Output descriptor

        Returns:
            bool: True if alert was sent successfully, False otherwise
        """
        @backoff.on_exception(backoff.fibo,
                              ClientError,
                              max_tries=self.MAX_BACKOFF_ATTEMPTS,
                              jitter=backoff.full_jitter,
                              on_backoff=backoff_handler(),
                              on_success=success_handler(),
                              on_giveup=giveup_handler())
        def _firehose_request_wrapper(json_alert, delivery_stream):
            """Make the PutRecord request to Kinesis Firehose with backoff

            Args:
                json_alert (str): The JSON dumped alert body
                delivery_stream (str): The Firehose Delivery Stream to send to

            Returns:
                dict: Firehose response in the format below
                    {'RecordId': 'string'}
            """
            self.__aws_client__.put_record(DeliveryStreamName=delivery_stream,
                                           Record={'Data': json_alert})

        if self.__aws_client__ is None:
            self.__aws_client__ = boto3.client('firehose',
                                               region_name=self.region)

        json_alert = json.dumps(alert.output_dict(),
                                separators=(',', ':')) + '\n'
        if len(json_alert) > self.MAX_RECORD_SIZE:
            LOGGER.error('Alert too large to send to Firehose: \n%s...',
                         json_alert[0:1000])
            return False

        delivery_stream = self.config[self.__service__][descriptor]
        LOGGER.info('Sending %s to aws-firehose:%s', alert, delivery_stream)

        _firehose_request_wrapper(json_alert, delivery_stream)
        LOGGER.info('%s successfully sent to aws-firehose:%s', alert,
                    delivery_stream)

        return True
    def _check_http_response(cls, response):
        """Method for checking for a valid HTTP response code

        Args:
            response (requests.Response): Response object from requests

        Returns:
            bool: Indicator of whether or not this request was successful
        """
        success = response is not None and (200 <= response.status_code <= 299)
        if not success:
            LOGGER.error('Encountered an error while sending to %s:\n%s',
                         cls.__service__, response.content)
        return success
Beispiel #12
0
def _load_output_config(config_path='conf/outputs.json'):
    """Load the outputs configuration file from disk

    Returns:
        dict: the output configuration settings
    """
    with open(config_path) as outputs:
        try:
            config = json.load(outputs)
        except ValueError:
            LOGGER.error('The \'%s\' file could not be loaded into json',
                         config_path)
            return

    return config
Beispiel #13
0
    def _kms_decrypt(self, data):
        """Decrypt data with AWS KMS.

        Args:
            data (str): An encrypted ciphertext data blob

        Returns:
            str: Decrypted json string
        """
        try:
            client = boto3.client('kms', region_name=self.region)
            response = client.decrypt(CiphertextBlob=data)
            return response['Plaintext']
        except ClientError as err:
            LOGGER.error('an error occurred during credentials decryption: %s', err.response)
Beispiel #14
0
    def _dispatch(self, alert, descriptor):
        """Send ban hash command to CarbonBlack

        Args:
            alert (Alert): Alert instance which triggered a rule
            descriptor (str): Output descriptor

        Returns:
            bool: True if alert was sent successfully, False otherwise
        """
        if not alert.context:
            LOGGER.error('[%s] Alert must contain context to run actions', self.__service__)
            return False

        creds = self._load_creds(descriptor)
        if not creds:
            return False

        client = CbResponseAPI(**creds)

        # Get md5 hash 'value' from streamalert's rule processor
        action = alert.context.get('carbonblack', {}).get('action')
        if action == 'ban':
            binary_hash = alert.context.get('carbonblack', {}).get('value')
            # The binary should already exist in CarbonBlack
            binary = client.select(Binary, binary_hash)
            # Determine if the binary is currenty listed as banned
            if binary.banned:
                # Determine if the banned action is enabled, if true exit
                if binary.banned.enabled:
                    return True
                # If the binary is banned and disabled, begin the banning hash operation
                banned_hash = client.select(BannedHash, binary_hash)
                banned_hash.enabled = True
                banned_hash.save()
            else:
                # Create a new BannedHash object to be saved
                banned_hash = client.create(BannedHash)
                # Begin the banning hash operation
                banned_hash.md5hash = binary.md5
                banned_hash.text = "Banned from StreamAlert"
                banned_hash.enabled = True
                banned_hash.save()

            return banned_hash.enabled is True
        else:
            LOGGER.error('[%s] Action not supported: %s', self.__service__, action)
            return False
Beispiel #15
0
def run(alert, region, function_name, config):
    """Send an Alert to its described outputs.

    Args:
        alert (dict): dictionary representating an alert with the
            following structure:

            {
                'record': record,
                'rule_name': rule.rule_name,
                'rule_description': rule.rule_function.__doc__,
                'log_source': str(payload.log_source),
                'log_type': payload.type,
                'outputs': rule.outputs,
                'source_service': payload.service,
                'source_entity': payload.entity
            }

        region (str): The AWS region of the currently executing Lambda function
        function_name (str): The name of the lambda function
        config (dict): The loaded configuration for outputs from conf/outputs.json

    Yields:
        (bool, str): Dispatch status and name of the output to the handler
    """
    if not validate_alert(alert):
        LOGGER.error('Invalid alert format:\n%s', json.dumps(alert, indent=2))
        return

    LOGGER.debug('Sending alert to outputs:\n%s', json.dumps(alert, indent=2))

    # strip out unnecessary keys and sort
    alert = _sort_dict(alert)

    outputs = alert['outputs']
    # Get the output configuration for this rule and send the alert to each
    for output in set(outputs):
        try:
            service, descriptor = output.split(':')
        except ValueError:
            LOGGER.error(
                'Improperly formatted output [%s]. Outputs for rules must '
                'be declared with both a service and a descriptor for the '
                'integration (ie: \'slack:my_channel\')', output)
            continue

        if service not in config or descriptor not in config[service]:
            LOGGER.error('The output \'%s\' does not exist!', output)
            continue

        # Retrieve the proper class to handle dispatching the alerts of this services
        output_dispatcher = get_output_dispatcher(service, region,
                                                  function_name, config)

        if not output_dispatcher:
            continue

        LOGGER.debug('Sending alert to %s:%s', service, descriptor)

        sent = False
        try:
            sent = output_dispatcher.dispatch(descriptor=descriptor,
                                              rule_name=alert['rule_name'],
                                              alert=alert)

        except Exception as err:  # pylint: disable=broad-except
            LOGGER.exception(
                'An error occurred while sending alert '
                'to %s:%s: %s. alert:\n%s', service, descriptor, err,
                json.dumps(alert, indent=2))

        # Yield back the result to the handler
        yield sent, output
Beispiel #16
0
    def _dispatch(self, alert, descriptor):
        """Send incident to Pagerduty Incidents API v2

        Args:
            alert (Alert): Alert instance which triggered a rule
            descriptor (str): Output descriptor

        Returns:
            bool: True if alert was sent successfully, False otherwise
        """
        creds = self._load_creds(descriptor)
        if not creds:
            return False

        # Cache base_url
        self._base_url = creds['api']

        # Preparing headers for API calls
        self._headers = {
            'Accept': 'application/vnd.pagerduty+json;version=2',
            'Content-Type': 'application/json',
            'Authorization': 'Token token={}'.format(creds['token'])
        }

        # Get user email to be added as From header and verify
        user_email = creds['email_from']
        if not self._user_verify(user_email, False):
            LOGGER.error('Could not verify header From: %s, %s', user_email,
                         self.__service__)
            return False

        # Add From to the headers after verifying
        self._headers['From'] = user_email

        # Cache default escalation policy
        self._escalation_policy_id = creds['escalation_policy_id']

        # Extracting context data to assign the incident
        rule_context = alert.context
        if rule_context:
            rule_context = rule_context.get(self.__service__, {})

        # Use the priority provided in the context, use it or the incident will be low priority
        incident_priority = self._priority_verify(rule_context)

        # Incident assignment goes in this order:
        #  Provided user -> provided policy -> default policy
        assigned_key, assigned_value = self._incident_assignment(rule_context)

        # Start preparing the incident JSON blob to be sent to the API
        incident_title = 'StreamAlert Incident - Rule triggered: {}'.format(
            alert.rule_name)
        incident_body = {
            'type': 'incident_body',
            'details': alert.rule_description
        }
        # Using the service ID for the PagerDuty API
        incident_service = {
            'id': creds['service_id'],
            'type': 'service_reference'
        }
        incident_data = {
            'incident': {
                'type': 'incident',
                'title': incident_title,
                'service': incident_service,
                'priority': incident_priority,
                'body': incident_body,
                assigned_key: assigned_value
            }
        }
        incidents_url = self._get_endpoint(self._base_url,
                                           self.INCIDENTS_ENDPOINT)

        try:
            incident = self._post_request_retry(incidents_url, incident_data,
                                                self._headers, True)
        except OutputRequestFailure:
            incident = False

        if not incident:
            LOGGER.error('Could not create main incident, %s',
                         self.__service__)
            return False

        # Extract the json blob from the response, returned by self._post_request_retry
        incident_json = incident.json()
        if not incident_json:
            return False

        # Extract the incident id from the incident that was just created
        incident_id = incident_json.get('incident', {}).get('id')

        # Create alert to hold all the incident details
        with_record = rule_context.get('with_record', True)
        event_data = events_v2_data(alert, creds['integration_key'],
                                    with_record)
        event = self._create_event(event_data)
        if not event:
            LOGGER.error('Could not create incident event, %s',
                         self.__service__)
            return False

        # Lookup the incident_key returned as dedup_key to get the incident id
        incident_key = event.get('dedup_key')

        if not incident_key:
            LOGGER.error('Could not get incident key, %s', self.__service__)
            return False

        # Keep that id to be merged later with the created incident
        event_incident_id = self._get_event_incident_id(incident_key)

        # Merge the incident with the event, so we can have a rich context incident
        # assigned to a specific person, which the PagerDuty REST API v2 does not allow
        merging_url = '{}/{}/merge'.format(incidents_url, incident_id)
        merged = self._merge_incidents(merging_url, event_incident_id)

        # Add a note to the combined incident to help with triage
        if not merged:
            LOGGER.error('Could not add note to incident, %s',
                         self.__service__)
        else:
            merged_id = merged.get('incident', {}).get('id')
            note = rule_context.get('note', 'Creating SOX Incident')
            self._add_incident_note(merged_id, note)

        return True
Beispiel #17
0
def validate_alert(alert):
    """Helper function to perform simple validation of an alert's keys and structure

    Args:
        alert (dict): the alert record to test that should be in the form of a dict

    Returns:
        bool: whether or not the alert has the proper structure
    """

    if not isinstance(alert, dict):
        LOGGER.error('The alert must be a map (dict)')
        return False

    alert_keys = {
        'record', 'rule_name', 'rule_description', 'log_type', 'log_source',
        'outputs', 'source_service', 'source_entity', 'context'
    }
    if not set(alert.keys()) == alert_keys:
        LOGGER.error('The alert object must contain the following keys: %s',
                     ', '.join(alert_keys))
        return False

    valid = True

    for key in alert_keys:
        if key == 'record':
            if not isinstance(alert['record'], dict):
                LOGGER.error('The alert record must be a map (dict)')
                return False

        elif key == 'context':
            if not isinstance(alert['context'], dict):
                LOGGER.error('The alert context must be a map (dict)')
                return False

        elif key == 'outputs':
            if not isinstance(alert[key], list):
                LOGGER.error(
                    'The value of the \'outputs\' key must be an array (list) that '
                    'contains at least one configured output.')
                valid = False
                continue

            for entry in alert[key]:
                if not isinstance(entry, (str, unicode)):
                    LOGGER.error(
                        'The value of each entry in the \'outputs\' list '
                        'must be a string (str).')
                    valid = False

        elif not isinstance(alert[key], (str, unicode)):
            LOGGER.error(
                'The value of the \'%s\' key must be a string (str), not %s',
                key, type(alert[key]))
            valid = False

    return valid
Beispiel #18
0
    def dispatch(self, **kwargs):
        """Send incident to Pagerduty Incidents API v2
        Keyword Args:
            **kwargs: consists of any combination of the following items:
                descriptor (str): Service descriptor (ie: slack channel, pd integration)
                rule_name (str): Name of the triggered rule
                alert (dict): Alert relevant to the triggered rule
                alert['context'] (dict): Provides user or escalation policy
        """
        creds = self._load_creds(kwargs['descriptor'])
        if not creds:
            return self._log_status(False, kwargs['descriptor'])

        # Cache base_url
        self._base_url = creds['api']

        # Preparing headers for API calls
        self._headers = {
            'Accept': 'application/vnd.pagerduty+json;version=2',
            'Content-Type': 'application/json',
            'Authorization': 'Token token={}'.format(creds['token'])
        }

        # Get user email to be added as From header and verify
        user_email = creds['email_from']
        if not self._user_verify(user_email, False):
            LOGGER.error('Could not verify header From: %s, %s', user_email, self.__service__)
            return self._log_status(False, kwargs['descriptor'])

        # Add From to the headers after verifying
        self._headers['From'] = user_email

        # Cache default escalation policy
        self._escalation_policy = creds['escalation_policy']

        # Extracting context data to assign the incident
        rule_context = kwargs['alert'].get('context', {})
        if rule_context:
            rule_context = rule_context.get(self.__service__, {})

        # Use the priority provided in the context, use it or the incident will be low priority
        incident_priority = self._priority_verify(rule_context)

        # Incident assignment goes in this order:
        #  Provided user -> provided policy -> default policy
        assigned_key, assigned_value = self._incident_assignment(rule_context)

        # Start preparing the incident JSON blob to be sent to the API
        incident_title = 'StreamAlert Incident - Rule triggered: {}'.format(kwargs['rule_name'])
        incident_body = {
            'type': 'incident_body',
            'details': kwargs['alert']['rule_description']
        }
        # We need to get the service id from the API
        incident_service = self._service_verify(creds['service_name'])
        incident_data = {
            'incident': {
                'type': 'incident',
                'title': incident_title,
                'service': incident_service,
                'priority': incident_priority,
                'body': incident_body,
                assigned_key: assigned_value
            }
        }
        incidents_url = self._get_endpoint(self._base_url, self.INCIDENTS_ENDPOINT)

        try:
            incident = self._post_request_retry(incidents_url, incident_data, self._headers, True)
        except OutputRequestFailure:
            incident = False

        if not incident:
            LOGGER.error('Could not create main incident, %s', self.__service__)
            return self._log_status(False, kwargs['descriptor'])

        # Extract the json blob from the response, returned by self._post_request_retry
        incident_json = incident.json()
        if not incident_json:
            return self._log_status(False, kwargs['descriptor'])

        # Extract the incident id from the incident that was just created
        incident_id = incident_json.get('incident', {}).get('id')

        # Create alert to hold all the incident details
        with_record = rule_context.get('with_record', True)
        event_data = events_v2_data(creds['integration_key'], with_record, **kwargs)
        event = self._create_event(event_data)
        if not event:
            LOGGER.error('Could not create incident event, %s', self.__service__)
            return self._log_status(False, kwargs['descriptor'])

        # Lookup the incident_key returned as dedup_key to get the incident id
        incident_key = event.get('dedup_key')

        if not incident_key:
            LOGGER.error('Could not get incident key, %s', self.__service__)
            return self._log_status(False, kwargs['descriptor'])

        # Keep that id to be merged later with the created incident
        event_incident_id = self._get_event_incident_id(incident_key)

        # Merge the incident with the event, so we can have a rich context incident
        # assigned to a specific person, which the PagerDuty REST API v2 does not allow
        merging_url = '{}/{}/merge'.format(incidents_url, incident_id)
        merged = self._merge_incidents(merging_url, event_incident_id)

        # Add a note to the combined incident to help with triage
        if not merged:
            LOGGER.error('Could not add note to incident, %s', self.__service__)
        else:
            merged_id = merged.get('incident', {}).get('id')
            note = rule_context.get('note', 'Creating SOX Incident')
            self._add_incident_note(merged_id, note)

        return self._log_status(incident_id, kwargs['descriptor'])
    def dispatch(self, **kwargs):
        """Send incident to Pagerduty Incidents API v2
        Keyword Args:
            **kwargs: consists of any combination of the following items:
                descriptor (str): Service descriptor (ie: slack channel, pd integration)
                rule_name (str): Name of the triggered rule
                alert (dict): Alert relevant to the triggered rule
                alert['context'] (dict): Provides user or escalation policy
        """
        creds = self._load_creds(kwargs['descriptor'])
        if not creds:
            return self._log_status(False)

        # Cache base_url
        self._base_url = creds['api']

        # Preparing headers for API calls
        self._headers = {
            'Authorization': 'Token token={}'.format(creds['token']),
            'Accept': 'application/vnd.pagerduty+json;version=2'
        }

        # Get user email to be added as From header and verify
        user_email = creds['email_from']
        if not self._user_verify(user_email, False):
            LOGGER.error('Could not verify header From: %s, %s', user_email,
                         self.__service__)
            return self._log_status(False)

        # Add From to the headers after verifying
        self._headers['From'] = user_email

        # Cache default escalation policy
        self._escalation_policy = creds['escalation_policy']

        # Extracting context data to assign the incident
        rule_context = kwargs['alert'].get('context', {})
        if rule_context:
            rule_context = rule_context.get(self.__service__, {})

        # Use the priority provided in the context, use it or the incident will be low priority
        incident_priority = self._priority_verify(rule_context)

        # Incident assignment goes in this order:
        #  Provided user -> provided policy -> default policy
        assigned_key, assigned_value = self._incident_assignment(rule_context)

        # Start preparing the incident JSON blob to be sent to the API
        incident_title = 'StreamAlert Incident - Rule triggered: {}'.format(
            kwargs['rule_name'])
        incident_body = {
            'type': 'incident_body',
            'details': kwargs['alert']['rule_description']
        }
        # We need to get the service id from the API
        incident_service = self._service_verify(creds['service_name'])
        incident = {
            'incident': {
                'type': 'incident',
                'title': incident_title,
                'service': incident_service,
                'priority': incident_priority,
                'body': incident_body,
                assigned_key: assigned_value
            }
        }
        incidents_url = self._get_endpoint(self._base_url,
                                           self.INCIDENTS_ENDPOINT)

        try:
            success = self._post_request_retry(incidents_url, incident,
                                               self._headers, True)
        except OutputRequestFailure:
            success = False

        return self._log_status(success)