Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[lambda][output][cli] modular outputs refactor #97

Merged
merged 10 commits into from
Apr 11, 2017

Conversation

ryandeivert
Copy link
Contributor

@ryandeivert ryandeivert commented Apr 6, 2017

to @airbnb/streamalert-maintainers
size: large
resolves: #6

Changes

  • stream_alert_cli.py can now configure new integrations for supported services
    • currently supported services are: aws-s3, slack, pagerduty, phantom
  • OutputBase class handles all shared operations for each services
  • subclasses of OutputBase (such as SlackOutput) are designed to handle service-specific takes such as dispatching the actual alert
  • new output classes for each services dictate what is required for each integration
  • outputs.json file in root config is currently an empty json map, but is used to house all new output configurations added by the user through the cli
  • credential storage is now handled by S3 and encrypted creds will not be packaged directly
    • kms is used to encrypt the creds prior to sending to S3
  • storing creds in encrypted_credentials directory is deprecated and will be going away in a future commit
  • credential storage has been switched to a json map to allow for better flexibility and ability to store arbitrary key/value pairs
  • credentials will be cached in tmp and loaded from there if they exist
    • creds in tmp must still be decrypted using kms if they are cached (will not store decrypted creds locally)
  • some terraform updates to enable getting objects from the encrypted secrets bucket
  • will prevent user from adding same output configuration twice
  • Some notes on how boto3 exceptions are handled:
    • when configuring a new output: if the credential info cannot be written to S3, we will log an error and raise the exception
    • when sending alerts to outputs: if the credential info cannot be read from S3, we will log an error and continue. This is very much so by design, since we want to try to send alerts to the other output options even if there is a failure with one.
  • various typo fixes in other code not related to this refactor

Why

  • Creates a better user experience when adding an integration for a supported service.
  • Enables 1-to-many ability for outputs. Now, instead of only being able to send to one destination within a service (say, slack), users can configure as many as they want and choose which to send to for which rules.
  • Makes adding new services for output much easier through a common base class.
  • Encrypted credentials do not have to be packaged with the lambda function and are stored securely on S3.

Copy link
Contributor

@jacknagz jacknagz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good start!



class OutputBase(object):
"""StreamOutputBase is the base class to handle routing alters to outputs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StreamOutputBase should be OutputBase in this comment

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: routing alerts*

I would add some more background on how to use this base class, along with going into the class variables just below this comment. Also describe the public methods and attributes available

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed base name and fixed typo. working on documentation also

alert [dict]: The alert relevant to the triggered rule
"""
creds = self._load_creds(descriptor)
message = "StreamAlert Rule Triggered - {}".format(rule_name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: no need for double quotes

"""
creds = self._load_creds(descriptor)
message = "StreamAlert Rule Triggered - {}".format(rule_name)
values_json = json.dumps({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit 2: double quote usage in this variable


Args:
cred_location [string]: tmp path on disk to to store the encrypted blob
descriptor [string]: service destination (ie: slack channel, pd integration)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a Returns: description here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added.

"""
LOGGER.error('unable to send alert for service %s', self.__service__)

def push_creds_to_s3(self, user_input):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be in the cli library?

return resp and (200 <= resp.getcode() <= 299)

@classmethod
def get_user_defined_properties(cls):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add more background on usage here?

pass

@classmethod
def get_default_properties(cls):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also add usage here, what should the orderdict contain?


def dispatch(self, descriptor, rule_name, alert):
"""Send alerts to the given service. This base class just
logs an error if not implemented on the inheriting class
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as mentioned in the class declaration, we should enforce it with ABC

cred_requirement=True))
])

def dispatch(self, descriptor, _, alert):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we just use kwargs instead of negating certain positional arguments with _?

url = os.path.join(creds['url'])

slack_message = json.dumps({'text': '```{}```'.format(
json.dumps(alert, indent=4)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

Copy link
Contributor

@austinbyers austinbyers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! I love the generic base class for outputs, that's exactly what I envisioned.

Overall:

  • Use logging.exception when appropriate
  • Be careful that the docstring exactly matches the actual arguments and return types


def run(self, alerts):
"""Send an Alert to its described outputs.
logging.error('an error occurred while decoding message to JSON: %s', err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use logging.exception (which is basically a wrapper around logging.error that automatically adds the exception context). Also use the LOGGER object?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes to all this - good catches

def run(self, alerts):
"""Send an Alert to its described outputs.
logging.error('an error occurred while decoding message to JSON: %s', err)
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not want to bubble this error all the way up so that the Lambda invocation fails?

Group alerts into a dictionary by their rule name, with
an array of alerts as the value.
if not 'default' in alert:
logging.info('malformed alert: %s', alert)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logging.warn

{
'default': [alert]
}
Args:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The args docstring (alerts) does not match the actual arguments (message, context)

try:
output_dispatcher.dispatch(descriptor, rule_name, alert)
except BaseException as err:
LOGGER.error('an error occurred while sending alert to %s: %s',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOGGER.exception('an error occurred while sending alert to %s', service)
The exception context will be logged automatically

if service in config and props['descriptor'].value in config[service]:
LOGGER_CLI.error('this descriptor is already configured for %s. '
'please select a new and unique descriptor', service)
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return False?

@@ -19,9 +19,9 @@
import base64
import hashlib
import os
import pip
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe pip is a non-standard package (doesn't come with Python), so it would actually go after boto3. Not sure though

return user_input(requested_info, mask)
else:
while not response:
response = getpass(prompt=prompt)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

++ for hidden credential input 🔑

// export AWS_SECRET_ACCESS_KEY="secret-key"
// export AWS_DEFAULT_REGION="region"
// export AWS_SECRET_ACCESS_KEY="secret-key"
// export AWS_DEFAULT_REGION="region"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Align the exports. Also, why not put the region into the provider block? Then you could make it a terraform variable. The only reason the access key isn't a variable is to discourage people from saving it on disk

{
"Action": [
"s3:GetObject",
"s3:ListBucket"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you actually need s3:ListBucket. Unless you're literally paginating over all the items in the bucket, GetObject should suffice

@ryandeivert ryandeivert force-pushed the ryandeivert-modular-outputs-refactor branch from ce59f49 to 9c913d2 Compare April 10, 2017 21:58
@ryandeivert ryandeivert force-pushed the ryandeivert-modular-outputs-refactor branch from ac587cc to cacfd95 Compare April 10, 2017 22:32
[cli] restricting colon characters along with spaces in user input for service descriptors

[cli] migrating some code from the runner to the new outputs file for the cli

[rules][testing] updating rules to conform to new outputs configuration style. also updating tests to go with this change

[lambda][alert] remove encrypted_credentials folder

[tf] fix bug in streamalert.secrets iam policy

[lambda][rule] send a single alert to SNS without wrapping in a list

[lambda] bug fixes: make it work

* load the outputs.json config
* package in the conf directory
* load the conf
* read s3 buckets from the conf
* fix bugs in request helper
* more

[lambda][output] masking slack url during input and restricting the use of colons (:) in unmasked input

[lambda][output][cli] fixing various nits
@ryandeivert ryandeivert force-pushed the ryandeivert-modular-outputs-refactor branch from cacfd95 to 3afa054 Compare April 10, 2017 22:35
@ryandeivert ryandeivert merged commit 3f02f7a into master Apr 11, 2017
@ryandeivert ryandeivert deleted the ryandeivert-modular-outputs-refactor branch April 11, 2017 22:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve: Make 'outputs' modular
3 participants