diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/mock_classes.py b/tests/mock_classes.py new file mode 100644 index 0000000..a2c3398 --- /dev/null +++ b/tests/mock_classes.py @@ -0,0 +1,30 @@ +class MockHTTPResponse: + def __init__(self, sock=None, debuglevel=0, method=None, url=None): + pass + + def getheaders(self): + return [ + ('x-xss-protection', '1;'), + ('server', 'nginx') + ] + + def getsomething(self): + pass + + +class MockHTTPSConnection: + def __init__(self, h, context, timeout): + pass + + def request(self, method, url, headers): + pass + + def headers(self): + return "content-type", "accept" + + def getresponse(self): + a = MockHTTPResponse() + return a + + def close(self): + pass diff --git a/tests/test_securityheaders.py b/tests/test_securityheaders.py new file mode 100644 index 0000000..6bf3128 --- /dev/null +++ b/tests/test_securityheaders.py @@ -0,0 +1,41 @@ +from unittest import mock, TestCase +from urllib.parse import ParseResult + +from secheaders.securityheaders import SecurityHeaders + +from .mock_classes import MockHTTPSConnection + + +@mock.patch("http.client.HTTPSConnection", MockHTTPSConnection) +class TestSecurityHeaders(TestCase): + + def test_init(self) -> None: + secheaders = SecurityHeaders("https://www.example.com", 0) + assert secheaders.target_url == ParseResult( + scheme='https', netloc='www.example.com', path='', params='', query='', fragment='') + + def test_fetch_headers(self) -> None: + secheaders = SecurityHeaders("https://www.example.com", 0) + expected_value = { + 'server': 'nginx', + 'x-xss-protection': '1;', + } + secheaders.fetch_headers() + assert secheaders.headers == expected_value + + def test_eval_headers(self) -> None: + secheaders = SecurityHeaders("https://www.example.com", 0) + expected_value = { + 'x-frame-options': {'defined': False, 'warn': True, 'contents': None, 'notes': []}, + 'strict-transport-security': {'defined': False, 'warn': True, 'contents': None, 'notes': []}, + 'content-security-policy': {'defined': False, 'warn': True, 'contents': None, 'notes': []}, + 'x-content-type-options': {'defined': False, 'warn': True, 'contents': None, 'notes': []}, + 'x-xss-protection': {'defined': True, 'warn': True, 'contents': '1;', 'notes': []}, + 'referrer-policy': {'defined': False, 'warn': True, 'contents': None, 'notes': []}, + 'permissions-policy': {'defined': False, 'warn': True, 'contents': None, 'notes': []}, + 'server': {'defined': True, 'warn': False, 'contents': 'nginx', 'notes': []}, + } + + secheaders.fetch_headers() + res = secheaders.check_headers() + assert res == expected_value diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..67bae08 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,74 @@ +from unittest import TestCase +from secheaders import utils +from secheaders.constants import EVAL_OK, EVAL_WARN + + +class TestUtils(TestCase): + + def test_csp_parser(self) -> None: + example_csp = ( + "default-src 'none' *.example.com; script-src 'self' src.example.com 'unsafe-inline'; connect-src 'self';" + "img-src *; style-src 'self'; base-uri 'self';form-action 'self'" + ) + expected_value = { + "default-src": ["'none'", "*.example.com"], + "script-src": ["'self'", "scripts.example.com", "'unsafe-inline'"], + "connect-src": ["'self'"], + "img-src": ["*"], + "style-src": ["'self'"], + "base-uri": ["'self'"], + "form-action": ["'self'"], + } + res = utils.csp_parser(example_csp) + assert res == expected_value + + def test_eval_csp(self) -> None: + unsafe_csp = ( + "default-src 'none' *.example.com; script-src 'self' src.example.com 'unsafe-inline'; connect-src 'self';" + "img-src *; style-src 'self'; base-uri 'self';form-action 'self'" + ) + res = utils.eval_csp(unsafe_csp) + expected_value = ( + EVAL_WARN, + ["Unsafe source 'unsafe-inline' in directive script-src"] + ) + assert res == expected_value + + safe_csp = "default-src 'self'; img-src 'self' cdn.example.com;" + expected_value = (EVAL_OK, []) + res = utils.eval_csp(safe_csp) + assert res == expected_value + + def test_eval_version_info(self) -> None: + nginx_banner_warn = 'nginx 1.17.10 (Ubuntu)' + nginx_banner_ok = 'nginx' + res = utils.eval_version_info(nginx_banner_warn) + assert res == (EVAL_WARN, []) + res = utils.eval_version_info(nginx_banner_ok) + assert res == (EVAL_OK, []) + + def test_permissions_policy_parser(self) -> None: + example_pp = ( + 'geolocation=(src "https://a.example.com" "https://b.example.com"), picture-in-picture=(), camera=*;' + ) + expected_value = { + 'geolocation': ['src', '"https://a.example.com"', '"https://b.example.com"'], + 'picture-in-picture': [], + 'camera': ['*'], + } + res = utils.permissions_policy_parser(example_pp) + assert expected_value == res + + def test_eval_permissions_policy(self) -> None: + unsafe_pp = 'geolocation=(src "https://a.example.com"), picture-in-picture=(), camera=*;' + expected_value = (EVAL_WARN, [ + "Privacy-sensitive feature 'camera' allowed from unsafe origin '*'", + "Privacy-sensitive feature 'microphone' not defined in permission-policy, always allowed.", + "Privacy-sensitive feature 'payment' not defined in permission-policy, always allowed.", + ]) + res = utils.eval_permissions_policy(unsafe_pp) + assert res == expected_value + safe_pp = "geolocation=(src), camera=(), microphone=(), payment=()" + expected_value = EVAL_OK, [] + res = utils.eval_permissions_policy(safe_pp) + assert res == expected_value