refactor: clean-up for pylint
This commit is contained in:
parent
d15740d3e9
commit
9a1a960641
2 changed files with 54 additions and 59 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue