commit 4fba7dbabf5e8138a8d68f2ac48c3bb4b82f0b5b Author: Scott Wallace Date: Fri Feb 23 08:09:14 2024 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..27573b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.pyenv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/cert_deets.py b/cert_deets.py new file mode 100644 index 0000000..be6fc9a --- /dev/null +++ b/cert_deets.py @@ -0,0 +1,120 @@ +#!python3 +""" +Return the Akamai property and version for a given site +""" +import argparse +import socket +import ssl +import sys +from typing import Any +from urllib.parse import urlparse + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from tabulate import tabulate + +SAN_GROUPING = 4 + + +def display_error( + site: str, + error: Any = None, +) -> None: + """ + Print a generic error + """ + print(f"ERROR: Could not find a certificate for {site}") + if error: + print(str(error)) + + +if __name__ == "__main__": + + def parseargs() -> argparse.Namespace: + """ + Parse the CLI + + Returns: + argparse.Namespace: parsed arguments + """ + parser = argparse.ArgumentParser() + + parser.add_argument("site", help="site to lookup") + + return parser.parse_args() + + def main() -> int: + """ + Main entrypoint + + Returns: + int: return value + """ + args = parseargs() + + parts = urlparse(args.site, scheme="https") + + if not parts.netloc: + parts = parts._replace(netloc=args.site) + + if not parts.port: + parts = parts._replace(netloc=f"{parts.netloc}:443") + + if not parts.hostname or not parts.port: + display_error(args.site, "Cannot parse hostname") + return 1 + + endpoint = f"{parts.hostname}:{parts.port}" + + try: + pem_data = ssl.get_server_certificate( + (parts.hostname, parts.port), + ).encode("utf-8") + except ( + ConnectionRefusedError, + ssl.CertificateError, + ssl.SSLError, + socket.gaierror, + ) as error: + display_error(endpoint, error) + return 2 + + if not pem_data: + display_error(endpoint, "Cannot fetch PEM data") + return 3 + + cert = x509.load_pem_x509_certificate(pem_data, default_backend()) + + sans = [ + f"DNS:{dns}" + for dns in cert.extensions.get_extension_for_class( + x509.SubjectAlternativeName + ).value.get_values_for_type(x509.DNSName) + ] + sans.extend( + [ + f"IP:{ip}" + for ip in cert.extensions.get_extension_for_class( + x509.SubjectAlternativeName + ).value.get_values_for_type(x509.IPAddress) + ] + ) + + sangroups = [ + sans[group : group + SAN_GROUPING] + for group in range(0, len(sans), SAN_GROUPING) + ] + + table = [ + ["Common name", cert.subject.rfc4514_string()], + ["SANs", tabulate(sangroups, tablefmt="plain")], + ["Valid from", cert.not_valid_before_utc], + ["Valid to", cert.not_valid_after_utc], + ["Issuer", cert.issuer.rfc4514_string()], + ] + + print(tabulate(table, tablefmt="plain")) + + return 0 + + sys.exit(main()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f6b0e25 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "cert-deets" +version = "1.0" +authors = [{ name = "Scott Wallace", email = "scott@wallace.sh" }] +description = "Show TLS certificate details for a given endpoint" +keywords = ["tls", "certificate", "python"] +classifiers = ["Programming Language :: Python :: 3"] +readme = "README.md" +dependencies = ["cryptography", "tabulate"] +requires-python = ">=3.11" + +[project.urls] +Homepage = "https://git.wallace.sh/scott/cert-deets/wiki" +Source = "https://git.wallace.sh/scott/cert-deets" +Issues = "https://git.wallace.sh/scott/cert-deets/issues" + +[project.optional-dependencies] +dev = ["black", "pylint"] +tests = ["pytest", "pytest-cov"] + +[tool.black] +target-version = ["py311"] + +[project.scripts] +certs-deets = "cert_deets:main"