diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..073e3e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +venv/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + diff --git a/LICENSE b/LICENSE index 1d22e3a..f8dc9fb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Jussi-Pekka Erkkila +Copyright (c) 2016-2024 Jussi-Pekka Erkkila Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index af373d6..f9597a5 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,63 @@ -# securityheaders +# secheaders Python script to check HTTP security headers + Same functionality as securityheaders.io but as Python script. Also checks some server/version headers. Written and tested using Python 3.4. With minor modifications could be used as a library for other projects. +## Installation + +The following assumes you have Python installed and command `python` refers to python version >= 3.4. + +### Run without installation + +1. Clone into repository +2. Run `python -m secheaders` + +### Installation + +1. Clone into repository +2. `python -m build` +3. `pip install dist/secheaders-0.1.0-py3-none-any.whl` +4. Run `secheaders --help` + + + ### Usage ``` -$ python securityheaders.py --help -usage: securityheaders.py [-h] [--max-redirects N] URL +$ secheaders --help +usage: secheaders [-h] [--max-redirects N] [--no-check-certificate] URL Check HTTP security headers positional arguments: - URL Target URL + URL Target URL -optional arguments: - -h, --help show this help message and exit - --max-redirects N Max redirects, set 0 to disable (default: 2) -$ +options: + -h, --help show this help message and exit + --max-redirects N Max redirects, set 0 to disable (default: 2) + --no-check-certificate + Do not verify TLS certificate chain (default: False) ``` -### Output + +### Example output ``` -$ python securityheaders.py --max-redirects 5 https://secfault.fi -Header 'x-xss-protection' is missing ... [ WARN ] -Header 'x-content-type-options' is missing ... [ WARN ] +$ secheaders example.com +Header 'x-frame-options' is missing ... [ WARN ] +Header 'strict-transport-security' is missing ... [ WARN ] Header 'content-security-policy' is missing ... [ WARN ] -Header 'x-powered-by' is missing ... [ OK ] -Header 'x-frame-options' contains value 'DENY' ... [ OK ] -Header 'strict-transport-security' contains value 'max-age=63072000' ... [ OK ] -Header 'access-control-allow-origin' is missing ... [ OK ] -Header 'server' contains value 'nginx/1.10.1' ... [ WARN ] +Header 'x-content-type-options' is missing ... [ WARN ] +Header 'x-xss-protection' is missing ... [ OK ] +Header 'referrer-policy' is missing ... [ WARN ] +Header 'permissions-policy' is missing ... [ WARN ] +Header 'server' contains value 'ECAcc (nyd/D187) ... [ WARN ] HTTPS supported ... [ OK ] HTTPS valid certificate ... [ OK ] -HTTP -> HTTPS redirect ... [ OK ] -$ +HTTP -> HTTPS redirect ... [ WARN ] ``` + +## Note + +The project renamed (2024-10-19) from **securityheaders** to **secheaders** to avoid confusion with PyPI package with similar name. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e760f9e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "secheaders" +version = "0.1.0" +requires-python = ">=3.4" +authors = [ + {name = "Jussi-Pekka Erkkilä", email = "jp.erkkila@gmail.com"}, +] +maintainers = [ + {name = "Jussi-Pekka Erkkilä", email = "jp.erkkila@gmail.com"} +] +description = "Scan HTTP security headers" +readme = "README.md" +license = {file = "LICENSE"} +keywords = ["web", "security"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Topic :: Security", +] + +[project.scripts] +secheaders = "secheaders.securityheaders:main" + diff --git a/secheaders/__init__.py b/secheaders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/secheaders/__main__.py b/secheaders/__main__.py new file mode 100644 index 0000000..e06b46b --- /dev/null +++ b/secheaders/__main__.py @@ -0,0 +1,3 @@ +if __name__ == "__main__": + from .securityheaders import main + main() diff --git a/constants.py b/secheaders/constants.py similarity index 100% rename from constants.py rename to secheaders/constants.py diff --git a/secheaders/exceptions.py b/secheaders/exceptions.py new file mode 100644 index 0000000..68196dc --- /dev/null +++ b/secheaders/exceptions.py @@ -0,0 +1,10 @@ +class SecurityHeadersException(Exception): + pass + + +class InvalidTargetURL(SecurityHeadersException): + pass + + +class UnableToConnect(SecurityHeadersException): + pass diff --git a/securityheaders.py b/secheaders/securityheaders.py similarity index 97% rename from securityheaders.py rename to secheaders/securityheaders.py index cfd044e..9029dc8 100644 --- a/securityheaders.py +++ b/secheaders/securityheaders.py @@ -6,20 +6,9 @@ import sys from urllib.parse import ParseResult, urlparse -import utils -from constants import DEFAULT_TIMEOUT, DEFAULT_URL_SCHEME, EVAL_WARN - - -class SecurityHeadersException(Exception): - pass - - -class InvalidTargetURL(SecurityHeadersException): - pass - - -class UnableToConnect(SecurityHeadersException): - pass +from . import utils +from .constants import DEFAULT_TIMEOUT, DEFAULT_URL_SCHEME, EVAL_WARN +from .exceptions import SecurityHeadersException, InvalidTargetURL, UnableToConnect class SecurityHeaders(): @@ -219,7 +208,7 @@ def check_headers(self): return retval -if __name__ == "__main__": +def main(): parser = argparse.ArgumentParser(description='Check HTTP security headers', formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('url', metavar='URL', type=str, help='Target URL') @@ -269,3 +258,7 @@ def check_headers(self): utils.print_ok("HTTP -> HTTPS redirect") else: utils.print_warning("HTTP -> HTTPS redirect") + + +if __name__ == "__main__": + main() diff --git a/utils.py b/secheaders/utils.py similarity index 97% rename from utils.py rename to secheaders/utils.py index 35144c1..dee07ca 100644 --- a/utils.py +++ b/secheaders/utils.py @@ -2,7 +2,7 @@ from typing import Tuple -from constants import EVAL_WARN, EVAL_OK +from .constants import EVAL_WARN, EVAL_OK def eval_x_frame_options(contents: str) -> Tuple[int, list]: @@ -97,7 +97,8 @@ def eval_permissions_policy(contents: str) -> Tuple[int, list]: feat_policy = pp_parsed.get(feature) if feat_policy is None: pp_unsafe = True - notes.append("Privacy-sensitive feature '{}' not defined in permission-policy, always allowed.".format(feature)) + notes.append("Privacy-sensitive feature '{}' not defined in permission-policy, always allowed.".format( + feature)) elif '*' in feat_policy: pp_unsafe = True notes.append("Privacy-sensitive feature '{}' allowed from unsafe origin '*'".format(feature)) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b908cbe --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +import setuptools + +setuptools.setup()