diff options
author | Rodrigo Guzman <rodrigo.guzman@onbondstreet.com> | 2017-04-04 07:47:18 -0400 |
---|---|---|
committer | Ashley Camba <ashwoods@gmail.com> | 2017-12-11 10:45:41 +0100 |
commit | 866135294b94e5159d4dcd3895636b532948b3c5 (patch) | |
tree | cbe9c980b3cc3db4a12e55af2b08fd2797cbb9ad | |
parent | aacb9f67267dc6b22945f6c29c11ee8bf7017edd (diff) | |
download | raven-866135294b94e5159d4dcd3895636b532948b3c5.tar.gz |
Add processor that sanitizes configurable keys
The logic of the new processor is almost identical to the logic of the
existing SanitizePasswordsProcessor, so this commit also changes the
implementation of SanitizePasswordsProcessor to avoid code-duplication.
It now inherits from the new SanitizeKeysProcessor.
-rw-r--r-- | raven/base.py | 1 | ||||
-rw-r--r-- | raven/processors.py | 49 | ||||
-rw-r--r-- | tests/processors/tests.py | 112 |
3 files changed, 144 insertions, 18 deletions
diff --git a/raven/base.py b/raven/base.py index 4fa20f6..9be3a0f 100644 --- a/raven/base.py +++ b/raven/base.py @@ -184,6 +184,7 @@ class Client(object): self.site = o.get('site') self.include_versions = o.get('include_versions', True) self.processors = o.get('processors') + self.sanitize_keys = o.get('sanitize_keys') if self.processors is None: self.processors = defaults.PROCESSORS diff --git a/raven/processors.py b/raven/processors.py index dd3c769..c5b3b41 100644 --- a/raven/processors.py +++ b/raven/processors.py @@ -64,32 +64,25 @@ class RemoveStackLocalsProcessor(Processor): frame.pop('vars', None) -class SanitizePasswordsProcessor(Processor): +class SanitizeKeysProcessor(Processor): """ Asterisk out things that look like passwords, credit card numbers, and API keys in frames, http, and basic extra data. """ MASK = '*' * 8 - FIELDS = frozenset([ - 'password', - 'secret', - 'passwd', - 'authorization', - 'api_key', - 'apikey', - 'sentry_dsn', - 'access_token', - ]) - VALUES_RE = re.compile(r'^(?:\d[ -]*?){13,16}$') + + def __init__(self, client): + super(SanitizeKeysProcessor, self).__init__(client) + fields = getattr(client, 'sanitize_keys') + if fields is None: + raise ValueError('The sanitize_keys setting must be present to use SanitizeKeysProcessor') + self.fields = fields def sanitize(self, key, value): if value is None: return - if isinstance(value, string_types) and self.VALUES_RE.match(value): - return self.MASK - if not key: # key can be a NoneType return value @@ -101,7 +94,7 @@ class SanitizePasswordsProcessor(Processor): key = text_type(key) key = key.lower() - for field in self.FIELDS: + for field in self.fields: if field in key: # store mask as a fixed length for security return self.MASK @@ -147,3 +140,27 @@ class SanitizePasswordsProcessor(Processor): sanitized_keyvals.append(keyval) return delimiter.join('='.join(keyval) for keyval in sanitized_keyvals) + + +class SanitizePasswordsProcessor(SanitizeKeysProcessor): + FIELDS = frozenset([ + 'password', + 'secret', + 'passwd', + 'authorization', + 'api_key', + 'apikey', + 'sentry_dsn', + 'access_token', + ]) + VALUES_RE = re.compile(r'^(?:\d[ -]*?){13,16}$') + + def __init__(self, client): + super(SanitizeKeysProcessor, self).__init__(client) # run the __init__ method of Processor, not SanitizeKeysProcessor + self.fields = self.FIELDS + + def sanitize(self, key, value): + value = super(SanitizePasswordsProcessor, self).sanitize(key, value) + if isinstance(value, string_types) and self.VALUES_RE.match(value): + return self.MASK + return value diff --git a/tests/processors/tests.py b/tests/processors/tests.py index c4c5f52..4c6cb59 100644 --- a/tests/processors/tests.py +++ b/tests/processors/tests.py @@ -4,8 +4,8 @@ from mock import Mock import raven from raven.utils.testutils import TestCase -from raven.processors import SanitizePasswordsProcessor, \ - RemovePostDataProcessor, RemoveStackLocalsProcessor +from raven.processors import SanitizeKeysProcessor, \ + SanitizePasswordsProcessor, RemovePostDataProcessor, RemoveStackLocalsProcessor VARS = { @@ -21,6 +21,8 @@ VARS = { 'api_key': 'secret_key', 'apiKey': 'secret_key', 'access_token': 'oauth2 access token', + 'custom_key1': 'you should not see this', + 'custom_key2': 'you should not see this', } @@ -33,6 +35,8 @@ def get_stack_trace_data_real(exception_class=TypeError, **kwargs): api_key = "I'm hideous!" # NOQA F841 apiKey = "4567000012345678" # NOQA F841 access_token = "secret stuff!" # NOQA F841 + custom_key1 = "you shouldn't see this" # NOQA F841 + custom_key2 = "you shouldn't see this" # NOQA F841 # TypeError: unsupported operand type(s) for /: 'str' and 'str' raise exception_class() @@ -77,6 +81,110 @@ def get_extra_data(): return data +class SanitizeKeysProcessorTest(TestCase): + + def setUp(self): + client = Mock(sanitize_keys=['custom_key1', 'custom_key2']) + self.proc = SanitizeKeysProcessor(client) + + def _check_vars_sanitized(self, vars, MASK): + """ + Helper to check that keys have been sanitized. + """ + self.assertTrue('custom_key1' in vars) + self.assertEquals(vars['custom_key1'], MASK) + self.assertTrue('custom_key2' in vars) + self.assertEquals(vars['custom_key2'], MASK) + + def test_stacktrace(self, *args, **kwargs): + data = get_stack_trace_data_real() + result = self.proc.process(data) + + self.assertTrue('exception' in result) + exception = result['exception'] + self.assertTrue('values' in exception) + values = exception['values'] + stack = values[-1]['stacktrace'] + self.assertTrue('frames' in stack) + self.assertEquals(len(stack['frames']), 2) + frame = stack['frames'][1] # frame of will_throw_type_error() + self.assertTrue('vars' in frame) + self._check_vars_sanitized(frame['vars'], self.proc.MASK) + + def test_http(self): + data = get_http_data() + result = self.proc.process(data) + + self.assertTrue('request' in result) + http = result['request'] + for n in ('data', 'env', 'headers', 'cookies'): + self.assertTrue(n in http) + self._check_vars_sanitized(http[n], self.proc.MASK) + + def test_extra(self): + data = get_extra_data() + result = self.proc.process(data) + + self.assertTrue('extra' in result) + extra = result['extra'] + self._check_vars_sanitized(extra, self.proc.MASK) + + def test_querystring_as_string(self): + data = get_http_data() + data['request']['query_string'] = 'foo=bar&custom_key1=nope&custom_key2=nope' + result = self.proc.process(data) + + self.assertTrue('request' in result) + http = result['request'] + self.assertEquals( + http['query_string'], + 'foo=bar&custom_key1=%(m)s&custom_key2=%(m)s' % {'m': self.proc.MASK}) + + def test_querystring_as_string_with_partials(self): + data = get_http_data() + data['request']['query_string'] = 'foo=bar&custom_key1&baz=bar' + result = self.proc.process(data) + + self.assertTrue('request' in result) + http = result['request'] + self.assertEquals(http['query_string'], 'foo=bar&custom_key1&baz=bar' % {'m': self.proc.MASK}) + + def test_cookie_as_string(self): + data = get_http_data() + data['request']['cookies'] = 'foo=bar;custom_key1=nope;custom_key2=nope;' + result = self.proc.process(data) + + self.assertTrue('request' in result) + http = result['request'] + self.assertEquals( + http['cookies'], + 'foo=bar;custom_key1=%(m)s;custom_key2=%(m)s;' % {'m': self.proc.MASK}) + + def test_cookie_as_string_with_partials(self): + data = get_http_data() + data['request']['cookies'] = 'foo=bar;custom_key1;baz=bar' + result = self.proc.process(data) + + self.assertTrue('request' in result) + http = result['request'] + self.assertEquals(http['cookies'], 'foo=bar;custom_key1;baz=bar' % dict(m=self.proc.MASK)) + + def test_cookie_header(self): + data = get_http_data() + data['request']['headers']['Cookie'] = 'foo=bar;custom_key1=nope;custom_key2=nope;' + result = self.proc.process(data) + + self.assertTrue('request' in result) + http = result['request'] + self.assertEquals( + http['headers']['Cookie'], + 'foo=bar;custom_key1=%(m)s;custom_key2=%(m)s;' % {'m': self.proc.MASK}) + + def test_sanitize_non_ascii(self): + result = self.proc.sanitize('__repr__: жили-были', '42') + self.assertEquals(result, '42') + + class SanitizePasswordsProcessorTest(TestCase): def _check_vars_sanitized(self, vars, proc): |