├── .gitignore ├── LICENSE ├── README.md ├── screenshots └── cli.jpg ├── securityheaders ├── __init__.py ├── __main__.py └── core.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # vim swap files 2 | *.swp 3 | 4 | # python bytecode files 5 | *.pyc 6 | __pycache__/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Timo Furrer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # securityheaders 2 | 3 | *securityheaders* is a CLI application to analyze the Security Headers of a given URL using securityheaders.io 4 | 5 | ![Screenshot CLI](https://raw.githubusercontent.com/timofurrer/securityheaders/master/screenshots/cli.jpg) 6 | 7 | ## Installation 8 | 9 | Install in python 3 environment: 10 | 11 | ```bash 12 | pip3 install securityheaders 13 | ``` 14 | 15 | *Note: you might need root privileges or use `--user` switch* 16 | 17 | ## Usage 18 | 19 | ``` 20 | Usage: securityheaders [OPTIONS] URL 21 | 22 | Get Security Headers from a given URL. The data is fetched from 23 | SecurityHeaders.io. 24 | 25 | Options: 26 | --version Show the version and exit. 27 | --json Print the Security Headers analysis as JSON 28 | --help Show this message and exit. 29 | ``` 30 | 31 | 32 | ## Thanks 33 | 34 | Thanks to **[SecurityHeaders.io](https://securityheaders.io)** for their awesome service! 35 | -------------------------------------------------------------------------------- /screenshots/cli.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timofurrer/securityheaders/58a93f31e56be0495ddfe56b31bb69c1a8ad512b/screenshots/cli.jpg -------------------------------------------------------------------------------- /securityheaders/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __VERSION__ = "0.0.1" 4 | __LICENSE__ = "MIT" 5 | __AUTHOR__ = "Timo Furrer" 6 | __AUTHOR_EMAIL__ = "tuxtimo@gmail.com" 7 | __URL__ = "https://github.com/timofurrer/securityheaders" 8 | -------------------------------------------------------------------------------- /securityheaders/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | CLI Application to get security headers 5 | from a given URL. 6 | """ 7 | 8 | 9 | import click 10 | import json 11 | import textwrap 12 | from prettytable import PrettyTable 13 | 14 | from .core import analyze_url 15 | 16 | 17 | def wrap(text): 18 | """Wraps the given text into multiple lines.""" 19 | return "\n".join(textwrap.wrap(text, 50)) 20 | 21 | 22 | @click.command() 23 | @click.version_option() 24 | @click.argument("url", required=True) 25 | @click.option("--json", "to_json", flag_value=True, help="Print the Security Headers analysis as JSON") 26 | def cli(url, to_json): 27 | """Get Security Headers from a given URL. 28 | The data is fetched from SecurityHeaders.io. 29 | """ 30 | if not to_json: 31 | click.echo("==> Analyzing Security Headers of {0}".format(click.style(url, bold=True))) 32 | 33 | # analyze security headers of given URL 34 | data = analyze_url(url) 35 | 36 | if to_json: 37 | click.echo(json.dumps(data, indent=4, sort_keys=True)) 38 | else: 39 | click.echo("➤ Site: {0}".format(click.style(data["site"], bold=True))) 40 | click.echo("➤ IP Address: {0}".format(click.style(data["ip"], bold=True))) 41 | click.echo(click.style("➤ Security Headers:", bold=True)) 42 | 43 | table = PrettyTable(["Header", "Value", "Rating", "Description"]) 44 | table.align["Header"] = "l" 45 | table.align["Value"] = "l" 46 | table.align["Description"] = "l" 47 | 48 | header_styles = { 49 | "info": ("white", False), 50 | "good": ("green", True), 51 | "bad": ("red", True) 52 | } 53 | 54 | for header, info in data["headers"].items(): 55 | fg_color, bold = header_styles[info["rating"]] 56 | header_text = click.style(header, bold=bold, fg=fg_color) 57 | value = info.get("value", "---") 58 | description = info.get("description", "---") 59 | table.add_row([header_text, wrap(value), info["rating"], wrap(description)]) 60 | 61 | click.echo(table) 62 | 63 | 64 | if __name__ == "__main__": 65 | cli() 66 | -------------------------------------------------------------------------------- /securityheaders/core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Retriev and Parse Security Headers from a given URL. 5 | """ 6 | 7 | import os 8 | import requests 9 | from collections import OrderedDict 10 | from bs4 import BeautifulSoup 11 | 12 | __SECURITY_HEADERS_URL__ = os.environ.get("SEUCRITY_HEADERS_URL", "https://securityheaders.io/?q={0}") 13 | 14 | 15 | def analyze_url(url): 16 | """ 17 | Analyze the security relevant headers 18 | of the given URL. 19 | 20 | :param str url: the URL to analyze. 21 | 22 | :returns: the security headers with rating and comments. 23 | :rtype: dict 24 | """ 25 | data = {} 26 | api_url = __SECURITY_HEADERS_URL__.format(url) 27 | response = requests.get(api_url) 28 | 29 | soup = BeautifulSoup(response.text, "html.parser") 30 | data["ip"] = soup.find_all("th", "tableLabel", text="IP Address:")[0].find_next_sibling("td").text.strip() 31 | data["site"] = soup.find_all("th", "tableLabel", text="Site:")[0].find_next_sibling("td").text.strip() 32 | 33 | headers = OrderedDict() 34 | # Parse Raw Headers Report Table 35 | for header, value in get_report_table("Raw Headers", soup): 36 | headers[header] = { 37 | "rating": "info", 38 | "value": value 39 | } 40 | 41 | # Parse ratings from badges 42 | raw_headers = soup.find_all("th", "tableLabel", text="Headers:")[0].find_next_sibling("td").find_all("li") 43 | for raw_header in raw_headers: 44 | rating = "good" if "pill-green" in raw_header["class"] else "bad" 45 | if raw_header.text not in headers: 46 | headers[raw_header.text] = {} 47 | headers[raw_header.text]["rating"] = rating 48 | 49 | 50 | # Parse Missing Headers Report Table 51 | for header, value in get_report_table("Missing Headers", soup): 52 | headers[header]["description"] = value 53 | 54 | # Parse Additional Information Report Table 55 | for header, value in get_report_table("Additional Information", soup): 56 | headers[header]["description"] = value 57 | 58 | data["headers"] = headers 59 | return data 60 | 61 | 62 | def get_report_table(title, soup): 63 | """ 64 | Returns the data of the report table 65 | with the given title. 66 | 67 | :param str title: the title of the report table 68 | 69 | :returns: key/value pairs 70 | :rtype: generator 71 | """ 72 | try: 73 | report_body = soup.find_all("div", "reportTitle", text=title)[0].find_next_sibling("div") 74 | except IndexError: 75 | return [] 76 | else: 77 | report_th = (x.text for x in report_body.select("table tbody tr th")) 78 | report_td = (x.text for x in report_body.select("table tbody tr td")) 79 | return zip(report_th, report_td) 80 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=missing-docstring 3 | 4 | import os 5 | import re 6 | import codecs 7 | from setuptools import setup 8 | 9 | 10 | def read_metafile(path): 11 | """ 12 | Read contents from given metafile 13 | """ 14 | with codecs.open(path, "rb", "utf-8") as f: 15 | return f.read() 16 | 17 | 18 | def get_meta(name): 19 | """ 20 | Get some metdata with the give name from the 21 | Meta file 22 | """ 23 | meta_match = re.search( 24 | r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=name.upper()), 25 | __META_DATA__, re.M 26 | ) 27 | 28 | if not meta_match: 29 | raise RuntimeError("Unable to find __{0}__ string.".format(name)) 30 | return meta_match.group(1) 31 | 32 | 33 | __META_FILE__ = os.path.join("securityheaders", "__init__.py") 34 | __META_DATA__ = read_metafile(__META_FILE__) 35 | 36 | setup( 37 | name="securityheaders", 38 | version=get_meta("version"), 39 | license=get_meta("license"), 40 | description="Fetch Security Headers for a specific URL using securityheaders.io", 41 | author=get_meta("author"), 42 | author_email=get_meta("author_email"), 43 | maintainer=get_meta("author"), 44 | maintainer_email=get_meta("author_email"), 45 | platforms=["Linux", "Windows", "MAC OS X"], 46 | url=get_meta("url"), 47 | download_url=get_meta("url"), 48 | packages=["securityheaders"], 49 | package_data={"": ["*.md"]}, 50 | install_requires=[ 51 | "click", 52 | "requests", 53 | "prettytable", 54 | "beautifulsoup4" 55 | ], 56 | include_package_data=True, 57 | entry_points={"console_scripts": ["securityheaders = securityheaders.__main__:cli"]}, 58 | classifiers=[ 59 | "Development Status :: 4 - Beta", 60 | "Environment :: Console", 61 | "Intended Audience :: Developers", 62 | "Intended Audience :: Education", 63 | "Intended Audience :: Other Audience", 64 | "License :: OSI Approved :: MIT License", 65 | "Natural Language :: English", 66 | "Operating System :: MacOS :: MacOS X", 67 | "Operating System :: OS Independent", 68 | "Operating System :: Microsoft :: Windows", 69 | "Operating System :: Microsoft :: Windows :: Windows 7", 70 | "Operating System :: Microsoft :: Windows :: Windows XP", 71 | "Operating System :: POSIX", 72 | "Operating System :: POSIX :: Linux", 73 | "Programming Language :: Python", 74 | "Programming Language :: Python :: 2.7", 75 | "Programming Language :: Python :: 3", 76 | "Programming Language :: Python :: 3.3", 77 | "Programming Language :: Python :: 3.4", 78 | "Programming Language :: Python :: 3.5", 79 | "Programming Language :: Python :: Implementation", 80 | "Topic :: Education :: Testing", 81 | "Topic :: Software Development", 82 | "Topic :: Software Development :: Testing" 83 | ], 84 | ) 85 | --------------------------------------------------------------------------------