refactor: clean-up for pylint

This commit is contained in:
Alex Kelly 2021-09-30 16:14:22 -04:00
parent d15740d3e9
commit 9a1a960641
2 changed files with 54 additions and 59 deletions

View file

@ -45,7 +45,7 @@ limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load, # List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers. # usually to register additional checkers.
load-plugins=pylint_django load-plugins=
# Pickle collected data for later comparisons. # Pickle collected data for later comparisons.
persistent=yes persistent=yes

View file

@ -1,15 +1,14 @@
# heavily modified version of https://gist.githubusercontent.com/gdamjan/55a8b9eec6cf7b771f92021d93b87b2c/raw/d8dc194ec4d0187f985a57138019d04e3a59b51f/ssl-check.py """ CLI to get basic information about certificates and determine validity"""
# to give a CLI for passing the hosts to check and other optional output from collections import namedtuple
import concurrent.futures
from socket import socket
import click import click
from OpenSSL import SSL from OpenSSL import SSL
from OpenSSL import crypto from OpenSSL import crypto
from cryptography import x509 from cryptography import x509
from cryptography.x509.oid import NameOID from cryptography.x509.oid import NameOID
import idna import idna
import sys
from socket import socket
from collections import namedtuple
__version__ = "0.2.0" __version__ = "0.2.0"
@ -19,25 +18,23 @@ HostInfo = namedtuple(
def get_certificate(hostname, port): def get_certificate(hostname, port):
"""retreive certificate details and return HostInfo tuple of values"""
hostname_idna = idna.encode(hostname) hostname_idna = idna.encode(hostname)
sock = socket() sock = socket()
try: sock.connect((hostname, port))
sock.connect((hostname, port)) peername = sock.getpeername()
peername = sock.getpeername() ctx = SSL.Context(SSL.SSLv23_METHOD) # most compatible
ctx = SSL.Context(SSL.SSLv23_METHOD) # most compatible ctx.check_hostname = False
ctx.check_hostname = False ctx.verify_mode = SSL.VERIFY_NONE
ctx.verify_mode = SSL.VERIFY_NONE sock_ssl = SSL.Connection(ctx, sock)
sock_ssl = SSL.Connection(ctx, sock) sock_ssl.set_connect_state()
sock_ssl.set_connect_state() sock_ssl.set_tlsext_host_name(hostname_idna)
sock_ssl.set_tlsext_host_name(hostname_idna) sock_ssl.do_handshake()
sock_ssl.do_handshake() cert = sock_ssl.get_peer_certificate()
cert = sock_ssl.get_peer_certificate() crypto_cert = cert.to_cryptography()
crypto_cert = cert.to_cryptography() sock_ssl.close()
sock_ssl.close() sock.close()
sock.close()
except ConnectionRefusedError:
pass
return HostInfo( return HostInfo(
cert=crypto_cert, cert=crypto_cert,
@ -48,6 +45,7 @@ def get_certificate(hostname, port):
def get_alt_names(cert): def get_alt_names(cert):
"""retreive the SAN values for given cert"""
try: try:
ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
return ext.value.get_values_for_type(x509.DNSName) return ext.value.get_values_for_type(x509.DNSName)
@ -56,10 +54,12 @@ def get_alt_names(cert):
def get_x509_text(cert): def get_x509_text(cert):
"""return the human-readable text version of the certificate"""
return crypto.dump_certificate(crypto.FILETYPE_TEXT, cert) return crypto.dump_certificate(crypto.FILETYPE_TEXT, cert)
def get_common_name(cert): def get_common_name(cert):
"""Retrun the common name from the certificate"""
try: try:
names = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME) names = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
return names[0].value return names[0].value
@ -68,6 +68,7 @@ def get_common_name(cert):
def get_issuer(cert): def get_issuer(cert):
"""Return the name of the CA/Issuer of the certificate"""
try: try:
names = cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME) names = cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)
return names[0].value return names[0].value
@ -75,62 +76,56 @@ def get_issuer(cert):
return None return None
def print_basic_info(hostinfo):
print(
f"""
{hostinfo.hostname} ({hostinfo.peername[0]}:{hostinfo.peername[1]})
\tcommonName: {get_common_name(hostinfo.cert)}
\tSAN: {get_alt_names(hostinfo.cert)}
\tissuer: {get_issuer(hostinfo.cert)}
\tnotBefore: {hostinfo.cert.not_valid_before}
\tnotAfter: {hostinfo.cert.not_valid_after}
"""
)
@click.command() @click.command()
@click.version_option(__version__, prog_name="checkcert") @click.version_option(__version__, prog_name="checkcert")
@click.option("--san", is_flag=True, help="Output Subject Alternate Names") @click.option("--san", is_flag=True, help="Output Subject Alternate Names")
@click.option( @click.option(
"--dump", is_flag=True, help="Dump the full text version of the x509 certificate" "--dump", is_flag=True, help="Dump the full text version of the x509 certificate"
) )
@click.option("--color/--no-color", default=True) @click.option(
"--color/--no-color",
default=True,
help="Enable/disable ANSI color output to show cert validity",
)
@click.argument("hosts", nargs=-1) @click.argument("hosts", nargs=-1)
def main(san, dump, color, hosts): def main(san, dump, color, hosts):
"""Return information about certificates given including their validity"""
# setup the list of tuples # setup the list of tuples
HOSTS = [] all_hosts = []
# handle a domain given with a : in it to specify the port # handle a domain given with a : in it to specify the port
for host in hosts: for host in hosts:
# if a host has a : in it, split on the :, first field will be host # if a host has a : in it, split on the :, first field will be host
# second field will be the port # second field will be the port
if ":" in host: if ":" in host:
host_info = host.split(":") host_info = host.split(":")
HOSTS.append((host_info[0], int(host_info[1]))) all_hosts.append((host_info[0], int(host_info[1])))
else: else:
HOSTS.append((host, 443)) all_hosts.append((host, 443))
for hostinfo in map(lambda x: get_certificate(x[0], x[1]), HOSTS): with concurrent.futures.ThreadPoolExecutor(max_workers=4) as epool:
output_string = "" for hostinfo in epool.map(lambda x: get_certificate(x[0], x[1]), all_hosts):
if dump: output_string = ""
print(get_x509_text(hostinfo.cert).decode()) if dump:
else: print(get_x509_text(hostinfo.cert).decode())
output_string += (
f"{hostinfo.hostname} ({hostinfo.peername[0]}:{hostinfo.peername[1]})\n"
)
output_string += f"\tcommonName: {get_common_name(hostinfo.cert)}\n"
if san:
output_string += f"\tSAN: {get_alt_names(hostinfo.cert)}\n"
output_string += f"\tissuer: {get_issuer(hostinfo.cert)}\n"
output_string += f"\tnotBefore: {hostinfo.cert.not_valid_before}\n"
output_string += f"\tnotAfter: {hostinfo.cert.not_valid_after}\n\n"
if hostinfo.is_valid and color:
click.echo(click.style(output_string, fg="green"))
elif not hostinfo.is_valid and color:
click.echo(click.style(output_string, fg="red"))
else: else:
click.echo(click.style(output_string)) output_string += (
f"{hostinfo.hostname} "
f"({hostinfo.peername[0]}:{hostinfo.peername[1]})\n"
)
output_string += f"\tcommonName: {get_common_name(hostinfo.cert)}\n"
if san:
output_string += f"\tSAN: {get_alt_names(hostinfo.cert)}\n"
output_string += f"\tissuer: {get_issuer(hostinfo.cert)}\n"
output_string += f"\tnotBefore: {hostinfo.cert.not_valid_before}\n"
output_string += f"\tnotAfter: {hostinfo.cert.not_valid_after}\n\n"
if hostinfo.is_valid and color:
click.echo(click.style(output_string, fg="green"))
elif not hostinfo.is_valid and color:
click.echo(click.style(output_string, fg="red"))
else:
click.echo(click.style(output_string))
# print(f"Certificate for {domain}\nexpires after: {x509.get_not_after()}") # print(f"Certificate for {domain}\nexpires after: {x509.get_not_after()}")
if __name__ == "__main__": if __name__ == "__main__":
main() # pragma: no cover main() # pylint: disable=no-value-for-parameter # pragma: no cover