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)
def configure_output(options): """Configure a new output for this service Args: options (argparser): Basically a namedtuple with the service setting """ account_config = CONFIG['global']['account'] region = account_config['region'] prefix = account_config['prefix'] kms_key_alias = account_config['kms_key_alias'] # Verify that the word alias is not in the config. # It is interpolated when the API call is made. if 'alias/' in kms_key_alias: kms_key_alias = kms_key_alias.split('/')[1] # Retrieve the proper service class to handle dispatching the alerts of this services output = StreamAlertOutput.get_dispatcher(options.service) # If an output for this service has not been defined, the error is logged # prior to this if not output: return # get dictionary of OutputProperty items to be used for user prompting props = output.get_user_defined_properties() for name, prop in props.iteritems(): # pylint: disable=protected-access props[name] = prop._replace(value=user_input( prop.description, prop.mask_input, prop.input_restrictions)) output_config = CONFIG['outputs'] service = output.__service__ # If it exists already, ask for user input again for a unique configuration if config_outputs.output_exists(output_config, props, service): return configure_output(options) secrets_bucket = '{}.streamalert.secrets'.format(prefix) secrets_key = output.output_cred_name(props['descriptor'].value) # Encrypt the creds and push them to S3 # then update the local output configuration with properties if config_outputs.encrypt_and_push_creds_to_s3(region, secrets_bucket, secrets_key, props, kms_key_alias): updated_config = output.format_output_config(output_config, props) output_config[service] = updated_config CONFIG.write() LOGGER_CLI.info( 'Successfully saved \'%s\' output configuration for service \'%s\'', props['descriptor'].value, options.service) else: LOGGER_CLI.error( 'An error occurred while saving \'%s\' ' 'output configuration for service \'%s\'', props['descriptor'].value, options.service)
def test_output_loading(): """OutputDispatcher - Loading Output Classes""" loaded_outputs = set(StreamAlertOutput.get_all_outputs()) # Add new outputs to this list to make sure they're loaded properly expected_outputs = { 'aws-firehose', 'aws-lambda', 'aws-s3', 'jira', 'pagerduty', 'pagerduty-v2', 'pagerduty-incident', 'phantom', 'slack' } assert_items_equal(loaded_outputs, expected_outputs)
def test_create_dispatcher(): """StreamAlertOutput - Create Dispatcher""" dispatcher = StreamAlertOutput.create_dispatcher( 'aws-s3', REGION, FUNCTION_NAME, CONFIG ) assert_is_instance(dispatcher, S3Output)
def _setup_output_subparser(subparser): """Add the output subparser: manage.py output SERVICE""" outputs = sorted(StreamAlertOutput.get_all_outputs().keys()) # Output parser arguments subparser.add_argument( 'service', choices=outputs, metavar='SERVICE', help= 'Create a new StreamAlert output for one of the available services: {}' .format(', '.join(outputs)))
def test_user_defined_properties(): """OutputDispatcher - User Defined Properties""" for output in StreamAlertOutput.get_all_outputs().values(): props = output.get_user_defined_properties() # The user defined properties should at a minimum contain a descriptor assert_is_not_none(props.get('descriptor'))
def test_get_dispatcher_bad(log_mock): """StreamAlertOutput - Get Invalid Dispatcher""" dispatcher = StreamAlertOutput.get_dispatcher('aws-s4') assert_is_none(dispatcher) log_mock.assert_called_with( 'Designated output service [%s] does not exist', 'aws-s4')
def test_get_dispatcher_good(): """StreamAlertOutput - Get Valid Dispatcher""" dispatcher = StreamAlertOutput.get_dispatcher('aws-s3') assert_is_not_none(dispatcher)
def output_handler(options, config): """Configure a new output for this service Args: options (argparse.Namespace): Basically a namedtuple with the service setting Returns: bool: False if errors occurred, True otherwise """ account_config = config['global']['account'] region = account_config['region'] prefix = account_config['prefix'] kms_key_alias = account_config['kms_key_alias'] # Verify that the word alias is not in the config. # It is interpolated when the API call is made. if 'alias/' in kms_key_alias: kms_key_alias = kms_key_alias.split('/')[1] # Retrieve the proper service class to handle dispatching the alerts of this services output = StreamAlertOutput.get_dispatcher(options.service) # If an output for this service has not been defined, the error is logged # prior to this if not output: return False # get dictionary of OutputProperty items to be used for user prompting props = output.get_user_defined_properties() for name, prop in props.iteritems(): # pylint: disable=protected-access props[name] = prop._replace(value=user_input( prop.description, prop.mask_input, prop.input_restrictions)) output_config = config['outputs'] service = output.__service__ # If it exists already, ask for user input again for a unique configuration if output_exists(output_config, props, service): return output_handler(options, config) provider = OutputCredentialsProvider(service, config=config, region=region, prefix=prefix) result = provider.save_credentials(props['descriptor'].value, kms_key_alias, props) if not result: LOGGER.error( 'An error occurred while saving \'%s\' ' 'output configuration for service \'%s\'', props['descriptor'].value, options.service) return False updated_config = output.format_output_config(output_config, props) output_config[service] = updated_config config.write() LOGGER.info( 'Successfully saved \'%s\' output configuration for service \'%s\'', props['descriptor'].value, options.service) return True
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 dispatcher = StreamAlertOutput.create_dispatcher( service, region, function_name, config) if not dispatcher: continue LOGGER.debug('Sending alert to %s:%s', service, descriptor) sent = False try: sent = 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