Changeset - d0238a1adb40
[Not reviewed]
0 1 3
x - 11 months ago 2024-05-09 16:57:52
xbr@c3l.lu
style: refactor mail checking and add TLSDetails class
4 files changed with 147 insertions and 79 deletions:
0 comments (0 inline, 0 general)
backend/check_domains.py
Show inline comments
 
#!/usr/bin/env python3
 
import json
 
import ssl
 
import smtplib
 
import imaplib
 
import socket
 
import os
 
from rich.console import Console
 
from cryptography import x509
 
import datetime
 
import math
 

	
 
def get_expiry_timestamps(expiry_timestamp: int, now_timestamp: int = datetime.datetime.now().timestamp()) -> (bool, int):
 
    seconds_left = expiry_timestamp - now_timestamp
 
    days_left = math.floor(seconds_left / 86400)
 
    return (seconds_left >= 0,days_left)
 

	
 
def get_validity_days(cert) -> (bool, int):
 
    # Get expiry date
 
    notAfter = cert['notAfter']
 
    notAfter_date = datetime.datetime.strptime(notAfter, '%b %d %H:%M:%S %Y %Z')
 

	
 
    # datetime to UNIX time
 
    notAfter_timestamp = notAfter_date.timestamp()
 
    expiry = get_expiry_timestamps(notAfter_timestamp)
 
    return (expiry[0], abs(expiry[1]))
 
import web
 
from mail import *
 
import tls_utils
 

	
 
if __name__ == "__main__":
 
    console = Console()
 

	
 
    # Parse the input file
 
with open('input.json') as raw_data:
 
    path = os.path.split(__file__)[0] + "/"
 
    with open(path + 'input.json') as raw_data:
 
        input = json.load(raw_data)
 

	
 
console.log("[white]Checking web domains...")
 

	
 
    context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
 

	
 
def web_noconn_expiry_days(web_domain: str) -> int | None:
 
    try:
 
        pem_cert = ssl.get_server_certificate((web_domain, 443), timeout=5)
 
        cert = x509.load_pem_x509_certificate(pem_cert.encode())
 
    except Exception as e:
 
        console = Console()
 
        console.log("Could not grab server cert for", "[orange bold underline]"+web_domain, ":", e, style="orange")
 
        return None
 
    
 
    not_after = cert.not_valid_after.timestamp()
 
    return get_expiry_timestamps(not_after)
 
    console.log("[white]Checking web domains...")
 

	
 
    for web_domain in input["domains"]["web"]:
 
        # Initiate TLS connection
 
@@ -55,7 +31,7 @@ for web_domain in input["domains"]["web"]:
 
            except ssl.SSLCertVerificationError as e:
 
                saved = e
 
                if e.verify_code == 10:
 
                expiry = web_noconn_expiry_days(web_domain)[1]
 
                    expiry = web.web_noconn_expiry_days(web_domain)[1]
 
                    if(expiry != None):
 
                        # TODO: add the TLS expiry stuff here
 
                        # possibly a list of domains that have expired
 
@@ -77,58 +53,18 @@ for web_domain in input["domains"]["web"]:
 
                print(e)
 
                continue
 

	
 
    validity = get_validity_days(cert)[1]
 
        validity = tls_utils.get_validity_days(cert)[1]
 
        # Print expiry date
 
        console.log("[green bold underline]" + web_domain, "expires in", validity, "days", style="green")
 
        # TODO: remove known expired certs
 
        # If the cert was expired before, we know that it is now valid
 
        # -> remove it from the list of expirjuded certs
 

	
 
def __mail_connection(host, port, verification: bool, initializer, starttls_args, closer) -> (bool, int):
 
    connection = initializer(host, port)
 
    if verification:
 
        connection.starttls(**starttls_args)
 
    else:
 
        connection.starttls()
 
    cert = connection.sock.getpeercert()
 
    closer(connection)
 
    return get_validity_days(cert)
 

	
 
def __smtp_closing(connection):
 
    connection.quit()
 
    
 
def __smtp_connection(domain, port, verification: bool) -> (bool, int):
 
    initializer = smtplib.SMTP
 
    starttls_args = {"context": context}
 
    return __mail_connection(domain, port, verification, initializer, starttls_args, __smtp_closing)
 

	
 
def __imap_closing(connection):
 
    connection.logout()
 

	
 
def __imap_connection(domain, port, verification: bool) -> (bool, int):
 
    initializer = imaplib.IMAP4
 
    starttls_args = {"ssl_context": context}
 
    return __mail_connection(domain, port, verification, initializer, starttls_args, __imap_closing)
 

	
 
def __mail_connect(domain, port, protocol_func):
 
    try:
 
        expiry = protocol_func(domain, port, True)[1]
 
        console.log("[green bold underline]" + domain, "expires in", expiry, "days", style="green")
 
    except ssl.SSLCertVerificationError as e:
 
        if (e.verify_code == 10):
 
            expiry = protocol_func(domain, port, False)[1]
 
            console.log("[red bold underline]" + web_domain, "expired", abs(expiry), "days ago.", style="red")
 
        else:
 
            console.log("[red bold underline]" + domain, "failed verification:", e.verify_message + ".", style="red")
 

	
 
def smtp_connect(domain, port):
 
    __mail_connect(domain, port, __smtp_connection)
 

	
 
def imap_connect(domain, port):
 
    __mail_connect(domain, port, __imap_connection)
 

	
 
    mail = MailVerificator(context)
 
    for smtp_entry in input["domains"]["smtp"]:
 
    smtp_connect(smtp_entry["host"], smtp_entry["port"])
 
        result = mail.connect(smtp_entry["host"], smtp_entry["port"], "smtp")
 
        result.print(console)
 

	
 
    for imap_entry in input["domains"]["imap"]:
 
    imap_connect(imap_entry["host"], imap_entry["port"])
 
        result = mail.connect(imap_entry["host"], imap_entry["port"], "imap")
 
        result.print(console)
backend/mail.py
Show inline comments
 
new file 100644
 
#!/usr/bin/env python3
 
import ssl
 
import smtplib
 
import imaplib
 
from rich.console import Console
 
from cryptography import x509
 
import tls_utils
 
from tls_utils import TLSDetails
 
from abc import ABC, abstractmethod
 

	
 
class MailHandler(ABC):
 
    def __init__(self, host: str, port: int, context: ssl.SSLContext):
 
        self.host = host
 
        self.port = port
 
        self.context = context
 
    
 
    def connect(self, verification: bool) -> tuple[bool, int]:
 
        connection = self.protocol_init(self.host, self.port)
 
        if verification:
 
            connection.starttls(**self.protocol_starttls_args())
 
        else:
 
            connection.starttls()
 
        cert = connection.sock.getpeercert()
 
        self.protocol_close(connection)
 
        return tls_utils.get_validity_days(cert)
 

	
 
    @abstractmethod
 
    def protocol_init(self, host, port):
 
        raise NotImplementedError()
 
    @abstractmethod
 
    def protocol_close(self, connection):
 
        raise NotImplementedError()
 
    @abstractmethod
 
    def protocol_starttls_args(self):
 
        raise NotImplementedError()
 
    
 
    @staticmethod
 
    def create_handler(protocol: str):
 
        if protocol == "smtp":
 
            return SMTPHandler
 
        elif protocol == "imap":
 
            return IMAPHandler
 
        else:
 
            raise ValueError("Invalid protocol")
 

	
 
class IMAPHandler(MailHandler):
 
    def protocol_init(self, host, port):
 
        return imaplib.IMAP4(host, port)
 
    def protocol_close(self, connection):
 
        connection.logout()
 
    def protocol_starttls_args(self):
 
        return {"ssl_context": self.context}
 

	
 
class SMTPHandler(MailHandler):
 
    def protocol_init(self, host, port):
 
        return smtplib.SMTP(host, port)
 
    def protocol_close(self, connection):
 
        connection.quit()
 
    def protocol_starttls_args(self):
 
        return {"context": self.context}
 

	
 
class MailVerificator:
 
    def __init__(self, context: ssl.SSLContext):
 
        self.context = context
 

	
 
    def connect(self, domain: str, port: int, protocol: str) -> TLSDetails:
 
        mail = MailHandler.create_handler(protocol)(domain, port, self.context)
 
        try:
 
            expiry = mail.connect(True)[1]
 
            return TLSDetails(domain_name=domain, expires_in_days=expiry)
 
        except ssl.SSLCertVerificationError as e:
 
            if (e.verify_code == 10):
 
                expiry = mail.connect(False)[1]
 
                return TLSDetails(domain_name=domain, expires_in_days=expiry)
 
            else:
 
                error = "failed verification:", e.verify_message + "."
 
                return TLSDetails(domain_name=domain, error_message=error)
 
\ No newline at end of file
backend/tls_utils.py
Show inline comments
 
new file 100644
 
#!/usr/bin/env python3
 
from rich.console import Console
 
import datetime
 
import math
 

	
 
class TLSDetails:
 
    domain_name = None
 
    expires_in_days = None
 
    error_message = None
 

	
 
    def __init__(self, domain_name : str = None, expires_in_days : str = None, error_message : str = None):
 
        self.domain_name = domain_name
 
        self.expires_in_days = expires_in_days
 
        self.error_message = error_message
 
    
 
    def print(self, console: Console):
 
        if self.error_message != None:
 
            console.log("[red bold underline]" + self.domain_name, self.error_message, style="red")
 
        elif self.expires_in_days < 0:
 
            console.log("[red bold underline]" + self.domain_name, "expired", abs(self.expires_in_days), "days ago.", style="red")
 
        else:
 
            console.log("[green bold underline]" + self.domain_name, "expires in", self.expires_in_days, "days", style="green")
 

	
 
def get_expiry_timestamps(expiry_timestamp: int, now_timestamp: int = datetime.datetime.now().timestamp()) -> tuple[bool, int]:
 
    seconds_left = expiry_timestamp - now_timestamp
 
    days_left = math.floor(seconds_left / 86400)
 
    return (seconds_left >= 0, days_left)
 

	
 
def get_validity_days(cert) -> tuple[bool, int]:
 
    # Get expiry date
 
    notAfter = cert['notAfter']
 
    notAfter_date = datetime.datetime.strptime(notAfter, '%b %d %H:%M:%S %Y %Z')
 

	
 
    # datetime to UNIX time
 
    notAfter_timestamp = notAfter_date.timestamp()
 
    expiry = get_expiry_timestamps(notAfter_timestamp)
 
    return (expiry[0], abs(expiry[1]))
 
\ No newline at end of file
backend/web.py
Show inline comments
 
new file 100644
 
#!/usr/bin/env python3
 
import ssl
 
from rich.console import Console
 
from cryptography import x509
 

	
 
import tls_utils as tls_utils
 

	
 
def web_noconn_expiry_days(web_domain: str) -> int | None:
 
    try:
 
        pem_cert = ssl.get_server_certificate((web_domain, 443), timeout=5)
 
        cert = x509.load_pem_x509_certificate(pem_cert.encode())
 
    except Exception as e:
 
        console = Console()
 
        console.log("Could not grab server cert for", "[orange bold underline]"+web_domain, ":", e, style="orange")
 
        return None
 
    
 
    not_after = cert.not_valid_after.timestamp()
 
    return tls_utils.get_expiry_timestamps(not_after)
 
\ No newline at end of file
0 comments (0 inline, 0 general)