Scripting : SSL Monitor for CSV and Website Reporting

If you are looking to monitor SSL certificates which is critical to stopping unscheduled outages due to expiry, this will ultimately give you a SSL report as seen below:


If you notice you also have a button called "Toggle Security Information" as below:


When this button is clicked you will get a Cipher and Protocol report based on risk factor:

This is created by using Python and is divided up into two scripts these are as follows:
  1. ssl_checker.py - Gets the Certificates data and exports that data to a CSV file
  2. generate_dashboard.py - Generates the HTML website based on the CSV data
SSL Checking and Extraction

We first need to query a list of domains that we would like to check this will be driven from a external text file called "sslcheck.txt"

Script : ssl_checker.py

#!/usr/bin/python3
import socket
import ssl
import datetime
import csv
from datetime import datetime
import sys
import time

def print_status(message, status="INFO"):
    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print(f"[{timestamp}] [{status}] {message}")

def get_certificate_info(hostname, verbose=True):
    if verbose:
        print_status(f"Checking certificate for {hostname}")
    
    try:
        # Create SSL context
        if verbose:
            print_status(f"Creating SSL context for {hostname}", "DEBUG")
        context = ssl.create_default_context()
        
        # Connect to the server
        if verbose:
            print_status(f"Attempting connection to {hostname}:443", "DEBUG")
            
        start_time = time.time()
        with socket.create_connection((hostname, 443), timeout=10) as sock:
            with context.wrap_socket(sock, server_hostname=hostname) as ssock:
                cert = ssock.getpeercert()
                
                # Extract required information
                common_name = next(x[0][1] for x in cert['subject'] if x[0][0] == 'commonName')
                issuer = next(x[0][1] for x in cert['issuer'] if x[0][0] == 'commonName')
                expiry_date = datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z')
                
                # Calculate days until expiry
                days_until_expiry = (expiry_date - datetime.now()).days
                
                # Get connection details
                cipher = ssock.cipher()
                protocol = ssock.version()
                
                end_time = time.time()
                duration = round(end_time - start_time, 2)
                
                if verbose:
                    print_status(f"Successfully retrieved certificate for {hostname} ({duration}s)", "SUCCESS")
                    print_status(f"  Common Name: {common_name}", "INFO")
                    print_status(f"  Issuer: {issuer}", "INFO")
                    print_status(f"  Expires: {expiry_date.strftime('%Y-%m-%d')}", "INFO")
                    print_status(f"  Days until expiry: {days_until_expiry}", "INFO")
                    print_status(f"  Protocol: {protocol}", "INFO")
                    print_status(f"  Cipher: {cipher[0]}", "INFO")
                
                return {
                    'hostname': hostname,
                    'common_name': common_name,
                    'issuer': issuer,
                    'expiry_date': expiry_date.strftime('%Y-%m-%d'),
                    'days_until_expiry': days_until_expiry,
                    'status': get_certificate_status(days_until_expiry),
                    'protocol': protocol,
                    'cipher': cipher[0]
                }
    except socket.timeout:
        print_status(f"Connection timeout for {hostname}", "ERROR")
        return None
    except socket.gaierror:
        print_status(f"DNS resolution failed for {hostname}", "ERROR")
        return None
    except ssl.SSLError as e:
        print_status(f"SSL error for {hostname}: {str(e)}", "ERROR")
        return None
    except Exception as e:
        print_status(f"Error checking {hostname}: {str(e)}", "ERROR")
        return None

def get_certificate_status(days):
    if days < 0:
        return 'expired'
    elif days <= 30:
        return 'expiring'
    else:
        return 'valid'

def main():
    print_status("SSL Certificate Checker Starting")
    print_status("Reading domain list from sslcheck.txt")
    
    try:
        # Read domains from file
        with open('sslcheck.txt', 'r') as f:
            domains = [line.strip() for line in f if line.strip()]
        
        print_status(f"Found {len(domains)} domains to check")
        
        # Process certificates and write to CSV
        fieldnames = ['hostname', 'common_name', 'issuer', 'expiry_date', 
                     'days_until_expiry', 'status', 'protocol', 'cipher']
        results = []
        successful = 0
        failed = 0
        
        with open('certificate_status.csv', 'w', newline='') as csvfile:
            writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
            writer.writeheader()
            
            for i, domain in enumerate(domains, 1):
                print_status(f"Processing domain {i} of {len(domains)}: {domain}")
                cert_info = get_certificate_info(domain)
                
                if cert_info:
                    writer.writerow(cert_info)
                    results.append(cert_info)
                    successful += 1
                else:
                    failed += 1
        
        # Print summary
        print_status("Certificate check completed", "SUCCESS")
        print_status(f"Summary:", "INFO")
        print_status(f"  Total domains processed: {len(domains)}", "INFO")
        print_status(f"  Successful checks: {successful}", "INFO")
        print_status(f"  Failed checks: {failed}", "INFO")
        
        # Calculate status statistics
        status_counts = {'valid': 0, 'expiring': 0, 'expired': 0}
        for cert in results:
            status_counts[cert['status']] += 1
        
        print_status("Certificate Status:", "INFO")
        print_status(f"  Valid certificates: {status_counts['valid']}", "INFO")
        print_status(f"  Expiring soon: {status_counts['expiring']}", "INFO")
        print_status(f"  Expired: {status_counts['expired']}", "INFO")
        
        print_status("Results written to certificate_status.csv", "SUCCESS")
        
    except FileNotFoundError:
        print_status("Error: sslcheck.txt not found", "ERROR")
        sys.exit(1)
    except Exception as e:
        print_status(f"Unexpected error: {str(e)}", "ERROR")
        sys.exit(1)

if __name__ == "__main__":
    main()

This will then output a CSV file as you can see below:


This CSV file will contain all the certificate data that the next script will then use, this also grabs the Protocol and Cipher as well:

hostname,common_name,issuer,expiry_date,days_until_expiry,status,protocol,cipher
a6n.co.uk,a6n.co.uk,WR3,2025-04-10,76,valid,TLSv1.3,TLS_AES_256_GCM_SHA384
bythepowerofgreyskull.com,bythepowerofgreyskull.com,R11,2025-04-09,74,valid,TLSv1.3,TLS_AES_256_GCM_SHA384
croucher.cloud,croucher.cloud,R11,2025-03-19,54,valid,TLSv1.3,TLS_AES_256_GCM_SHA384

This data as mentioned before will then feed the next script to produce the report you observed earlier.

Produce the HTML report

Now we need to take that CSV file and produce the html website from that data, that obviously needs to be easy to process and understand and we need to order the "expiry date" as descending (the most critical first)

Finally we need those health card to be clickable that will then filter certificates in that state for some added UI.

Script : generate_dashboard.py

#!/usr/bin/python3
import csv
from datetime import datetime
import os

class SSLDashboardGenerator:
    def __init__(self, csv_file='certificate_status.csv', output_file='ssl_dashboard.html'):
        self.csv_file = csv_file
        self.output_file = output_file
        self.certificates = []
        self.stats = {'valid': 0, 'expiring': 0, 'expired': 0}

    def assess_protocol_security(self, protocol):
        protocol_security = {
            'SSLv2': 'Critical',
            'SSLv3': 'Critical',
            'TLSv1.0': 'High',
            'TLSv1.1': 'High',
            'TLSv1.2': 'Medium',
            'TLSv1.3': 'Low'
        }
        return protocol_security.get(protocol, 'Unknown')

    def assess_cipher_security(self, cipher):
        cipher_security = {
            '3DES': 'Critical',
            'RC4': 'Critical',
            'MD5': 'Critical',
            'SHA1': 'High',
            'TLS_AES_256_GCM_SHA384': 'High',
            'ECDHE-RSA-AES128-GCM-SHA256': 'Low',
            'ECDHE-RSA-AES256-GCM-SHA384': 'Low',
            'AES128': 'Medium',
            'AES256': 'Low',
            'ChaCha20': 'Low'
        }
        return cipher_security.get(cipher, 'Unknown')

    def read_csv(self):
        with open(self.csv_file, 'r') as f:
            reader = csv.DictReader(f)
            self.certificates = list(reader)
            
        for cert in self.certificates:
            status = cert['status']
            self.stats[status] = self.stats.get(status, 0) + 1
            cert['protocol_security'] = self.assess_protocol_security(cert.get('protocol', 'Unknown'))
            cert['cipher_security'] = self.assess_cipher_security(cert.get('cipher', 'Unknown'))

    def generate_html(self):
        html = f'''<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SSL Certificate Dashboard</title>
    <style>
        * {{
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
        }}

        body {{
            background-color: #f3f4f6;
            padding: 1.5rem;
        }}

        .container {{
            max-width: 1200px;
            margin: 0 auto;
        }}

        h1 {{
            font-size: 1.5rem;
            font-weight: bold;
            margin-bottom: 1.5rem;
            color: #111827;
        }}

        .status-cards {{
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
            gap: 1rem;
            margin-bottom: 1.5rem;
        }}

        .card {{
            background-color: white;
            border-radius: 0.5rem;
            padding: 1rem;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
            cursor: pointer;
            transition: transform 0.2s;
            position: relative;
        }}

        .card:hover {{
            transform: translateY(-2px);
        }}

        .card.selected::after {{
            content: '';
            position: absolute;
            bottom: -2px;
            left: 10%;
            width: 80%;
            height: 2px;
            background-color: currentColor;
        }}

        .card.valid {{
            background-color: #f0fdf4;
        }}

        .card.expiring {{
            background-color: #fefce8;
        }}

        .card.expired {{
            background-color: #fef2f2;
        }}

        .card-header {{
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 0.5rem;
        }}

        .card-title {{
            font-size: 1rem;
            font-weight: 600;
        }}

        .card.valid .card-title {{
            color: #15803d;
        }}

        .card.expiring .card-title {{
            color: #854d0e;
        }}

        .card.expired .card-title {{
            color: #991b1b;
        }}

        .card-count {{
            font-size: 1.5rem;
            font-weight: bold;
        }}

        .table-container {{
            background-color: white;
            border-radius: 0.5rem;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
            overflow-x: auto;
        }}

        table {{
            width: 100%;
            border-collapse: collapse;
        }}

        th {{
            background-color: #f9fafb;
            padding: 0.75rem 1rem;
            text-align: left;
            font-size: 0.75rem;
            font-weight: 600;
            color: #6b7280;
            text-transform: uppercase;
            letter-spacing: 0.05em;
        }}

        td {{
            padding: 0.75rem 1rem;
            font-size: 0.875rem;
            color: #111827;
            border-top: 1px solid #e5e7eb;
        }}

        tr.hidden {{
            display: none;
        }}

        .status-badge {{
            display: inline-block;
            padding: 0.25rem 0.75rem;
            border-radius: 9999px;
            font-size: 0.75rem;
            font-weight: 600;
        }}

        .status-badge.valid {{
            background-color: #dcfce7;
            color: #15803d;
        }}

        .status-badge.expiring {{
            background-color: #fef9c3;
            color: #854d0e;
        }}

        .status-badge.expired {{
            background-color: #fee2e2;
            color: #991b1b;
        }}

        .security-toggle {{
            display: inline-block;
            margin: 1rem 0;
            padding: 0.5rem 1rem;
            background-color: #f3f4f6;
            border: 1px solid #e5e7eb;
            border-radius: 0.375rem;
            font-size: 0.875rem;
            color: #374151;
            cursor: pointer;
        }}

        .security-toggle:hover {{
            background-color: #e5e7eb;
        }}

        .security-details {{
            display: none;
            background-color: #f9fafb;
            padding: 0.75rem 1rem;
        }}

        .security-badge {{
            display: inline-block;
            padding: 0.25rem 0.75rem;
            border-radius: 9999px;
            font-size: 0.75rem;
            font-weight: 600;
            margin-left: 0.5rem;
        }}

        .security-badge.Critical {{
            background-color: #fee2e2;
            color: #991b1b;
        }}

        .security-badge.High {{
            background-color: #fef9c3;
            color: #854d0e;
        }}

        .security-badge.Medium {{
            background-color: #dcfce7;
            color: #15803d;
        }}

        .security-badge.Low {{
            background-color: #dcfce7;
            color: #15803d;
        }}

        .security-badge.Unknown {{
            background-color: #f3f4f6;
            color: #6b7280;
        }}

        .footer {{
            margin-top: 1rem;
            text-align: center;
            font-size: 0.875rem;
            color: #6b7280;
        }}

        .clear-filter {{
            display: inline-block;
            margin-bottom: 1rem;
            padding: 0.5rem 1rem;
            background-color: #f3f4f6;
            border: 1px solid #e5e7eb;
            border-radius: 0.375rem;
            font-size: 0.875rem;
            color: #374151;
            cursor: pointer;
            transition: all 0.2s;
        }}

        .clear-filter:hover {{
            background-color: #e5e7eb;
        }}

        .clear-filter.hidden {{
            display: none;
        }}
    </style>
</head>
<body>
    <div class="container">
        <h1>SSL Certificate Dashboard</h1>
        
        <div class="status-cards">
            <div class="card valid" onclick="filterCertificates('valid')" id="valid-card">
                <div class="card-header">
                    <div class="card-title">Valid</div>
                    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                        <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
                        <polyline points="22 4 12 14.01 9 11.01"></polyline>
                    </svg>
                </div>
                <div class="card-count">{self.stats['valid']}</div>
            </div>
            
            <div class="card expiring" onclick="filterCertificates('expiring')" id="expiring-card">
                <div class="card-header">
                    <div class="card-title">Expiring Soon</div>
                    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                        <circle cx="12" cy="12" r="10"></circle>
                        <polyline points="12 6 12 12 16 14"></polyline>
                    </svg>
                </div>
                <div class="card-count">{self.stats['expiring']}</div>
            </div>
            
            <div class="card expired" onclick="filterCertificates('expired')" id="expired-card">
                <div class="card-header">
                    <div class="card-title">Expired</div>
                    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                        <circle cx="12" cy="12" r="10"></circle>
                        <line x1="12" y1="8" x2="12" y2="12"></line>
                        <line x1="12" y1="16" x2="12.01" y2="16"></line>
                    </svg>
                </div>
                <div class="card-count">{self.stats['expired']}</div>
            </div>
        </div>

        <button class="clear-filter hidden" onclick="clearFilter()" id="clear-filter">
            Show All Certificates
        </button>

        <button class="security-toggle" onclick="toggleSecurityDetails()">
            Toggle Security Information
        </button>

        <div class="table-container">
            <table>
                <thead>
                    <tr>
                        <th>Domain</th>
                        <th>Common Name</th>
                        <th>Issuer</th>
                        <th>Expiry Date</th>
                        <th>Days Left</th>
                        <th>Status</th>
                    </tr>
                </thead>
                <tbody>
'''

        sorted_certificates = sorted(self.certificates, 
                                   key=lambda x: int(x['days_until_expiry']))
        
        for cert in sorted_certificates:
            html += f'''
                    <tr class="cert-row" data-status="{cert['status']}">
                        <td>{cert['hostname']}</td>
                        <td>{cert['common_name']}</td>
                        <td>{cert['issuer']}</td>
                        <td>{cert['expiry_date']}</td>
                        <td>{cert['days_until_expiry']}</td>
                        <td>
                            <span class="status-badge {cert['status']}">
                                {cert['status']}
                            </span>
                        </td>
                    </tr>
                    <tr class="security-details">
                        <td colspan="6">
                            <div>
                                <strong>Protocol:</strong> {cert.get('protocol', '')}
                                <span class="security-badge {cert['protocol_security']}">{cert['protocol_security']}</span>
                            </div>
                            <div style="margin-top: 0.5rem;">
                                <strong>Cipher:</strong> {cert.get('cipher', '')}
                                <span class="security-badge {cert['cipher_security']}">{cert['cipher_security']}</span>
                            </div>
                        </td>
                    </tr>'''

        html += f'''
                </tbody>
            </table>
        </div>
        
        <div class="footer">
            Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | Managed and Developed by Mooney
        </div>
    </div>

    <script>
        function filterCertificates(status) {{
            const rows = document.querySelectorAll('.cert-row');
            const cards = document.querySelectorAll('.card');
            const clearFilterBtn = document.getElementById('clear-filter');
            
            cards.forEach(card => card.classList.remove('selected'));
            document.getElementById(`${{status}}-card`).classList.add('selected');
            clearFilterBtn.classList.remove('hidden');
            
            rows.forEach(row => {{
                if (row.getAttribute('data-status') === status) {{
                    row.classList.remove('hidden');
                }} else {{
                    row.classList.add('hidden');
                }}
            }});
        }}

        function clearFilter() {{
            const rows = document.querySelectorAll('.cert-row');
            const cards = document.querySelectorAll('.card');
            const clearFilterBtn = document.getElementById('clear-filter');
            
            cards.forEach(card => card.classList.remove('selected'));
            clearFilterBtn.classList.add('hidden');
            rows.forEach(row => row.classList.remove('hidden'));
        }}

        function toggleSecurityDetails() {{
            const details = document.querySelectorAll('.security-details');
            details.forEach(detail => {{
                detail.style.display = detail.style.display === 'table-row' ? 'none' : 'table-row';
            }});
        }}
    </script>
</body>
</html>'''

        return html

    def save_dashboard(self):
        try:
            self.read_csv()
            html_content = self.generate_html()
            
            with open(self.output_file, 'w') as f:
                f.write(html_content)
                
            print(f"Dashboard generated successfully: {self.output_file}")
            
        except Exception as e:
            print(f"Error generating dashboard: {str(e)}")

def main():
    generator = SSLDashboardGenerator()
    generator.save_dashboard()

if __name__ == "__main__":
    main()
Previous Post Next Post

نموذج الاتصال