From 3a7d966a3626428aafda98ae80a1bf4dafabfdb0 Mon Sep 17 00:00:00 2001 From: Johnny Tordgeman Date: Mon, 25 Feb 2019 12:09:04 +0200 Subject: [PATCH 1/3] BD-294 - Dev logger refactor (#68) * px_logger refactor * log alignment --- perimeterx/px_blocker.py | 7 ++++--- perimeterx/px_cookie_validator.py | 8 ++++---- perimeterx/px_logger.py | 16 +++++++++++++--- perimeterx/px_original_token_validator.py | 2 +- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/perimeterx/px_blocker.py b/perimeterx/px_blocker.py index 745eaf9..122cf69 100644 --- a/perimeterx/px_blocker.py +++ b/perimeterx/px_blocker.py @@ -35,18 +35,18 @@ def handle_blocking(self, ctx, config): headers = {'Content-Type': content_type} if action is px_constants.ACTION_CHALLENGE: - logger.debug('Challenge page is served') + logger.debug('Enforcing action: Challenge page is served') blocking_props = ctx.block_action_data blocking_response = blocking_props elif action is px_constants.ACTION_RATELIMIT: - logger.debug('Rate limit page is served') + logger.debug('Enforcing action: Rate limit page is served') blocking_props = None blocking_response = self.ratelimit_rendered_page status = '429 Too Many Requests' else: # block - logger.debug('Block page is served') + logger.debug('Enforcing action: Block page is served') blocking_props = self.prepare_properties(ctx, config) blocking_response = self.mustache_renderer.render(px_template.get_template(px_constants.BLOCK_TEMPLATE), blocking_props) @@ -63,6 +63,7 @@ def handle_blocking(self, ctx, config): return page_response, headers, status if is_json_response: + logger.debug('Serving advanced blocking response') blocking_response = json.dumps(blocking_props) blocking_response = str(blocking_response) diff --git a/perimeterx/px_cookie_validator.py b/perimeterx/px_cookie_validator.py index 8d1458d..f178094 100644 --- a/perimeterx/px_cookie_validator.py +++ b/perimeterx/px_cookie_validator.py @@ -19,7 +19,7 @@ def verify(ctx, config): logger = config.logger try: if not ctx.px_cookies.keys(): - logger.debug('No risk cookie on the request') + logger.debug('Cookie is missing') ctx.s2s_call_reason = 'no_cookie' return False @@ -46,7 +46,7 @@ def verify(ctx, config): if not px_cookie.deserialize(): cookie = px_cookie._hmac + ":" + px_cookie._raw_cookie - logger.error('Cookie decryption failed, value: {}'.format(cookie)) + logger.debug('Cookie decryption failed, value: {}'.format(cookie)) ctx.px_cookie_raw = cookie_version + "=" + cookie ctx.s2s_call_reason = 'cookie_decryption_failed' return False @@ -67,7 +67,7 @@ def verify(ctx, config): if px_cookie.is_cookie_expired(): ctx.s2s_call_reason = 'cookie_expired' - msg = 'Cookie TTL expired, value: {}, age: {}' + msg = 'Cookie TTL is expired, value: {}, age: {}' logger.debug(msg.format(px_cookie.decoded_cookie, px_cookie.get_age())) return False @@ -86,7 +86,7 @@ def verify(ctx, config): return True except Exception, err: traceback.print_exc() - logger.debug('Unexpected exception while evaluating Risk cookie. Error: {}'.format(err)) + logger.error('Unexpected exception while evaluating Risk cookie. Error: {}'.format(err)) ctx.px_cookie_raw = px_cookie._raw_cookie ctx.s2s_call_reason = 'cookie_decryption_failed' return False diff --git a/perimeterx/px_logger.py b/perimeterx/px_logger.py index 0de8c6e..244bf50 100644 --- a/perimeterx/px_logger.py +++ b/perimeterx/px_logger.py @@ -1,11 +1,21 @@ +import logging + class Logger(object): def __init__(self, debug, app_id): self.debug_mode = debug self.app_id = app_id + # Setup logger + self.logger = logging.getLogger(__name__) + handler = logging.StreamHandler() + formatter = logging.Formatter('[PerimeterX %(levelname)s][{}]: %(message)s'.format(self.app_id)) + handler.setFormatter(formatter) + self.logger.addHandler(handler) + self.logger.setLevel(logging.DEBUG) + def debug(self, message): - if self.debug_mode: - print '[PerimeterX DEBUG][{}]: '.format(self.app_id) + message + if self.debug_mode: + self.logger.debug(message) def error(self, message): - print '[PerimeterX ERROR][{}]: '.format(self.app_id) + message + self.logger.error(message) diff --git a/perimeterx/px_original_token_validator.py b/perimeterx/px_original_token_validator.py index 507a5d0..a3bb60e 100644 --- a/perimeterx/px_original_token_validator.py +++ b/perimeterx/px_original_token_validator.py @@ -17,7 +17,7 @@ def verify(ctx, config): cookie_version, px_cookie = px_cookie_builder.build_px_cookie({version: no_version_token}, '') if not px_cookie.deserialize(): - logger.error('Original token decryption failed, value: {}'.format(px_cookie.raw_cookie)) + logger.debug('Original token decryption failed, value: {}'.format(px_cookie.raw_cookie)) ctx.original_token_error = 'decryption_failed' return False From 0cc43d09c64ec89456de166deccc440b2433fbb2 Mon Sep 17 00:00:00 2001 From: pxjohnny Date: Tue, 26 Feb 2019 14:59:41 +0200 Subject: [PATCH 2/3] Added support for bypass monitor mode --- README.md | 19 +++++++ perimeterx/px_config.py | 5 ++ perimeterx/px_request_verifier.py | 3 +- test/test_px_request_validator.py | 89 +++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ebbd3cb..1bf8017 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Table of Contents * [Custom Request Handler](#custom_request_handler) * [Additional Activity Handler](#additional_activity_handler) * [Px Disable Request](#px_disable_request) + * [Test Block Flow on Monitoring Mode](#bypass_monitor_header) ## Installation @@ -254,3 +255,21 @@ environ['px_disable_request'] = True #The enforcer shall be disabled for that re ``` +#### Test Block Flow on Monitoring Mode + +Allows you to test an enforcer’s blocking flow while you are still in Monitor Mode. + +When the header name is set(eg. `x-px-block`) and the value is set to `1`, when there is a block response (for example from using a User-Agent header with the value of `PhantomJS/1.0`) the Monitor Mode is bypassed and full block mode is applied. If one of the conditions is missing you will stay in Monitor Mode. This is done per request. +To stay in Monitor Mode, set the header value to `0`. + +The Header Name is configurable using the `bypass_monitor_header` property. + +**Default:** Empty + +```python +config = { + ... + bypass_monitor_header: 'x-px-block', + ... +} +``` diff --git a/perimeterx/px_config.py b/perimeterx/px_config.py index e430945..caa90ff 100644 --- a/perimeterx/px_config.py +++ b/perimeterx/px_config.py @@ -37,6 +37,7 @@ def __init__(self, config_dict): self._ip_headers = config_dict.get('ip_headers', []) self._proxy_url = config_dict.get('proxy_url', None) self._max_buffer_len = config_dict.get('max_buffer_len', 30) + self._bypass_monitor_header = config_dict.get('bypass_monitor_header','') sensitive_routes = config_dict.get('sensitive_routes', []) if not isinstance(sensitive_routes, list): @@ -191,6 +192,10 @@ def enrich_custom_parameters(self): def testing_mode(self): return self._testing_mode + @property + def bypass_monitor_header(self): + return self._bypass_monitor_header + def __instantiate_user_defined_handlers(self, config_dict): self._custom_request_handler = self.__set_handler('custom_request_handler', config_dict) self._get_user_ip = self.__set_handler('get_user_ip', config_dict) diff --git a/perimeterx/px_request_verifier.py b/perimeterx/px_request_verifier.py index 4ca19ca..6e7f01e 100644 --- a/perimeterx/px_request_verifier.py +++ b/perimeterx/px_request_verifier.py @@ -50,9 +50,10 @@ def handle_verification(self, ctx, request): else: logger.debug('Risk score is higher or equal than blocking score') self.report_block_traffic(ctx) + should_bypass_monitor = config.bypass_monitor_header and ctx.headers.get(config.bypass_monitor_header) == '1'; if config.additional_activity_handler: config.additional_activity_handler(ctx, config) - if config.module_mode == px_constants.MODULE_MODE_BLOCKING: + if config.module_mode == px_constants.MODULE_MODE_BLOCKING or should_bypass_monitor: data, headers, status = self.px_blocker.handle_blocking(ctx=ctx, config=config) response_function = generate_blocking_response(data, headers, status) else: diff --git a/test/test_px_request_validator.py b/test/test_px_request_validator.py index ac5dc33..410624c 100644 --- a/test/test_px_request_validator.py +++ b/test/test_px_request_validator.py @@ -71,3 +71,92 @@ def test_handle_verification_failed(self): context.score = 100 response = self.request_handler.handle_verification(context, request) self.assertEqual(response.status, '403 Forbidden') + + def test_handle_monitor(self): + config = PxConfig({'app_id': 'PXfake_app_id', + 'auth_token': '', + 'module_mode': px_constants.MODULE_MODE_MONITORING + }); + request_handler = PxRequestVerifier(config) + builder = EnvironBuilder(headers=self.headers, path='/') + env = builder.get_environ() + request = Request(env) + context = PxContext(request, request_handler.config) + context.score = 100 + response = request_handler.handle_verification(context, request) + self.assertEqual(response, True) + + def test_bypass_monitor_header_enabled(self): + config = PxConfig({'app_id': 'PXfake_app_id', + 'auth_token': '', + 'module_mode': px_constants.MODULE_MODE_MONITORING, + 'bypass_monitor_header': 'x-px-block' + }); + headers = {'X-FORWARDED-FOR': '127.0.0.1', + 'remote-addr': '127.0.0.1', + 'x-px-block': '1', + 'content_length': '100'} + request_handler = PxRequestVerifier(config) + builder = EnvironBuilder(headers=headers, path='/') + env = builder.get_environ() + request = Request(env) + context = PxContext(request, request_handler.config) + context.score = 100 + response = request_handler.handle_verification(context, request) + self.assertEqual(response.status, '403 Forbidden') + + def test_bypass_monitor_header_disabled(self): + config = PxConfig({'app_id': 'PXfake_app_id', + 'auth_token': '', + 'module_mode': px_constants.MODULE_MODE_MONITORING, + 'bypass_monitor_header': 'x-px-block' + }); + headers = {'X-FORWARDED-FOR': '127.0.0.1', + 'remote-addr': '127.0.0.1', + 'x-px-block': '0', + 'content_length': '100'} + request_handler = PxRequestVerifier(config) + builder = EnvironBuilder(headers=headers, path='/') + env = builder.get_environ() + request = Request(env) + context = PxContext(request, request_handler.config) + context.score = 100 + response = request_handler.handle_verification(context, request) + self.assertEqual(response, True) + + def test_bypass_monitor_header_configured_but_missing(self): + config = PxConfig({'app_id': 'PXfake_app_id', + 'auth_token': '', + 'module_mode': px_constants.MODULE_MODE_MONITORING, + 'bypass_monitor_header': 'x-px-block' + }); + headers = {'X-FORWARDED-FOR': '127.0.0.1', + 'remote-addr': '127.0.0.1', + 'content_length': '100'} + request_handler = PxRequestVerifier(config) + builder = EnvironBuilder(headers=headers, path='/') + env = builder.get_environ() + request = Request(env) + context = PxContext(request, request_handler.config) + context.score = 100 + response = request_handler.handle_verification(context, request) + self.assertEqual(response, True) + + def test_bypass_monitor_header_on_valid_request(self): + config = PxConfig({'app_id': 'PXfake_app_id', + 'auth_token': '', + 'module_mode': px_constants.MODULE_MODE_MONITORING, + 'bypass_monitor_header': 'x-px-block' + }); + headers = {'X-FORWARDED-FOR': '127.0.0.1', + 'remote-addr': '127.0.0.1', + 'x-px-block': '1', + 'content_length': '100'} + request_handler = PxRequestVerifier(config) + builder = EnvironBuilder(headers=headers, path='/') + env = builder.get_environ() + request = Request(env) + context = PxContext(request, request_handler.config) + context.score = 0 + response = request_handler.handle_verification(context, request) + self.assertEqual(response, True) \ No newline at end of file From 96e6613b839022e655722e84b6d7094f463bc6e9 Mon Sep 17 00:00:00 2001 From: pxjohnny Date: Tue, 26 Feb 2019 16:04:00 +0200 Subject: [PATCH 3/3] Version 3.1.0 --- CHANGELOG.md | 4 ++++ README.md | 4 ++-- perimeterx/px_constants.py | 2 +- setup-gae.py | 2 +- setup.py | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2766e7..b26d0d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## [v3.1.0](https://github.com/PerimeterX/perimeterx-python-wsgi) (2019-02-26) +- Refactor of px_logger to use native python logger +- Added support for bypass monitor mode header + ## [v3.0.2](https://github.com/PerimeterX/perimeterx-python-wsgi) (2019-02-13) - page requested pass_reason alignment - better error handling for http errors diff --git a/README.md b/README.md index 1bf8017..c23e22b 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ [PerimeterX](http://www.perimeterx.com) Python Middleware ============================================================= -> Latest stable version: [v3.0.2](https://pypi.org/project/perimeterx-python-wsgi/) +> Latest stable version: [v3.1.0](https://pypi.org/project/perimeterx-python-wsgi/) -> Latest GAE stable version: [v3.0.2](https://pypi.org/project/perimeterx-python-wsgi-gae/) +> Latest GAE stable version: [v3.1.0](https://pypi.org/project/perimeterx-python-wsgi-gae/) Table of Contents ----------------- diff --git a/perimeterx/px_constants.py b/perimeterx/px_constants.py index 97a6552..4505e17 100644 --- a/perimeterx/px_constants.py +++ b/perimeterx/px_constants.py @@ -30,7 +30,7 @@ EMPTY_GIF_B64 = 'R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' COLLECTOR_HOST = 'collector.perimeterx.net' FIRST_PARTY_FORWARDED_FOR = 'X-FORWARDED-FOR' -MODULE_VERSION = 'Python WSGI Module{} v3.0.2' +MODULE_VERSION = 'Python WSGI Module{} v3.1.0' API_RISK = '/api/v3/risk' PAGE_REQUESTED_ACTIVITY = 'page_requested' BLOCK_ACTIVITY = 'block' diff --git a/setup-gae.py b/setup-gae.py index 7d282fe..23cbc70 100644 --- a/setup-gae.py +++ b/setup-gae.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages -version = 'v3.0.2' +version = 'v3.1.0' setup(name='perimeterx-python-wsgi-gae', version=version, license='MIT', diff --git a/setup.py b/setup.py index ff7307e..d777682 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages -version = 'v3.0.2' +version = 'v3.1.0' setup(name='perimeterx-python-wsgi', version=version, license='MIT',