Initial commit: Basic CLI and checks
Certo supports polling one or several hostnames. Output can be human-readable or JSON.
This commit is contained in:
1
certo/__init__.py
Normal file
1
certo/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__version__ = "0.1.0"
|
||||
36
certo/__main__.py
Normal file
36
certo/__main__.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
Usage: certo [-v] [-j] <hostnames>... [-d DAYS|--days-to-expiration=DAYS] [-t SECONDS|--timeout=SECONDS]
|
||||
|
||||
Options:
|
||||
-v Increase verbosity [default: False]
|
||||
-j Output in JSON format [default: False]
|
||||
-d DAYS --days-to-expiration=DAYS Warn about impending expiration if within DAYS of the cert's notAfter [default: 5]
|
||||
-t SECONDS --timeout=SECONDS Timeout for SSL Handshake [default: 5]
|
||||
"""
|
||||
import logging
|
||||
from pprint import pprint
|
||||
|
||||
from docopt import docopt
|
||||
|
||||
from certo.checks.hostname import check_host_certificate_expiration
|
||||
from certo.output import default_output, json_output
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = docopt(__doc__)
|
||||
|
||||
output_as_json = args.get("-j")
|
||||
if args.get("-v"):
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
hostnames = args.get("<hostnames>")
|
||||
days_to_expiration = int(args.get("--days-to-expiration"))
|
||||
|
||||
results = []
|
||||
|
||||
for hs in hostnames:
|
||||
logging.info(f"Getting CERT from {hs}")
|
||||
results.append(check_host_certificate_expiration(hs, days_to_expiration))
|
||||
|
||||
if output_as_json:
|
||||
print(json_output(results))
|
||||
else:
|
||||
print(default_output(results))
|
||||
BIN
certo/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
certo/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
certo/__pycache__/__main__.cpython-310.pyc
Normal file
BIN
certo/__pycache__/__main__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
certo/__pycache__/output.cpython-310.pyc
Normal file
BIN
certo/__pycache__/output.cpython-310.pyc
Normal file
Binary file not shown.
0
certo/checks/__init__.py
Normal file
0
certo/checks/__init__.py
Normal file
BIN
certo/checks/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
certo/checks/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
certo/checks/__pycache__/hostname.cpython-310.pyc
Normal file
BIN
certo/checks/__pycache__/hostname.cpython-310.pyc
Normal file
Binary file not shown.
44
certo/checks/hostname.py
Normal file
44
certo/checks/hostname.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import datetime
|
||||
from collections import namedtuple
|
||||
|
||||
from dateutil.parser import parse as dtparse
|
||||
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
CertCheckResult = namedtuple(
|
||||
"CertCheckResult", ["hostname", "check_successful", "expiration_date", "debug"]
|
||||
)
|
||||
|
||||
|
||||
def get_cert(hostname, timeout):
|
||||
ctx = ssl.create_default_context()
|
||||
with ctx.wrap_socket(socket.socket(), server_hostname=hostname) as s:
|
||||
s.settimeout(timeout)
|
||||
|
||||
s.connect((hostname, 443))
|
||||
return s.getpeercert()
|
||||
|
||||
|
||||
def check_host_certificate_expiration(hostname, days_to_expiration, timeout=5):
|
||||
try:
|
||||
cert = get_cert(hostname, timeout)
|
||||
except ssl.SSLCertVerificationError as e:
|
||||
return CertCheckResult(hostname, False, None, e.strerror)
|
||||
|
||||
expdate = dtparse(cert.get("notAfter"))
|
||||
curdate = datetime.datetime.now(tz=expdate.tzinfo)
|
||||
|
||||
if expdate - curdate < datetime.timedelta(days=days_to_expiration):
|
||||
return CertCheckResult(
|
||||
hostname,
|
||||
False,
|
||||
expdate,
|
||||
f"Certificate expires in {(expdate - curdate).days} days - expected more than {days_to_expiration}",
|
||||
)
|
||||
return CertCheckResult(
|
||||
hostname,
|
||||
True,
|
||||
expdate,
|
||||
f"Certificate expires in {(expdate - curdate).days} days",
|
||||
)
|
||||
34
certo/output.py
Normal file
34
certo/output.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import json
|
||||
from typing import List
|
||||
|
||||
from certo.checks.hostname import CertCheckResult
|
||||
|
||||
|
||||
def json_output(cert_check_results: List[CertCheckResult]):
|
||||
def make_check_serialisable(check):
|
||||
"""
|
||||
Converts a CertCheckResult for json serialisation
|
||||
:param check: CertCheckResult as output by checks
|
||||
:return: check as dict() with appropriate type conversions for json.dumps
|
||||
"""
|
||||
ret = check._asdict()
|
||||
if check.expiration_date:
|
||||
ret["expiration_date"] = check.expiration_date.strftime("%c %Z")
|
||||
return ret
|
||||
|
||||
return json.dumps(
|
||||
list(map(lambda check: make_check_serialisable(check), cert_check_results)),
|
||||
indent=4,
|
||||
)
|
||||
|
||||
|
||||
def default_output(cert_check_results: List[CertCheckResult]):
|
||||
res = list()
|
||||
for check in cert_check_results:
|
||||
result = f"[{'PASS' if check.check_successful else 'FAIL'}] Check host {check.hostname}"
|
||||
if check.debug:
|
||||
result += f" - {check.debug}"
|
||||
if check.expiration_date:
|
||||
result += f" - Certificate expires on {check.expiration_date} {check.expiration_date.tzname()}"
|
||||
res.append(result)
|
||||
return "\n".join(res)
|
||||
Reference in New Issue
Block a user