Created
December 28, 2025 18:35
-
-
Save jcefoli/feb971de73d44018c52aaba0d791d4c7 to your computer and use it in GitHub Desktop.
Certbot Expiry Checker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import json | |
| import re | |
| import subprocess | |
| from datetime import datetime, timezone | |
| _FIELD_REGEX = re.compile(r"^\s*([^:]+):\s*(.+)$") | |
| def _run_certbot(): | |
| command = ["sudo", "certbot", "certificates"] | |
| try: | |
| result = subprocess.run( | |
| command, | |
| capture_output=True, | |
| text=True, | |
| check=True, | |
| ) | |
| except subprocess.CalledProcessError as exc: | |
| stderr = exc.stderr.strip() or exc.stdout.strip() or "no output captured" | |
| raise RuntimeError( | |
| f"certbot command failed (exit code {exc.returncode}): {stderr}" | |
| ) from exc | |
| return result.stdout | |
| def _field_value(line, key): | |
| match = _FIELD_REGEX.match(line) | |
| if match and match.group(1).strip() == key: | |
| return match.group(2).strip() | |
| return None | |
| def _parse_expiry(raw_value): | |
| timestamp = raw_value.split(" (", 1)[0].strip() | |
| return datetime.fromisoformat(timestamp) | |
| def get_certbot_certs(): | |
| output = _run_certbot() | |
| certs = [] | |
| cert_data = None | |
| for line in output.splitlines(): | |
| name_value = _field_value(line, "Certificate Name") | |
| if name_value: | |
| if cert_data: | |
| certs.append(cert_data) | |
| cert_data = {"name": name_value, "domains": []} | |
| continue | |
| if not cert_data: | |
| continue | |
| domains_value = _field_value(line, "Domains") | |
| if domains_value is not None: | |
| cert_data["domains"] = domains_value.split() | |
| continue | |
| expiry_value = _field_value(line, "Expiry Date") | |
| if expiry_value is not None: | |
| try: | |
| expiry_dt = _parse_expiry(expiry_value) | |
| now = datetime.now(expiry_dt.tzinfo or timezone.utc) | |
| cert_data["expiry"] = expiry_dt.isoformat() | |
| cert_data["days_remaining"] = (expiry_dt - now).days | |
| cert_data["_expiry_dt"] = expiry_dt | |
| except ValueError: | |
| cert_data["expiry"] = expiry_value | |
| continue | |
| cert_path_value = _field_value(line, "Certificate Path") | |
| if cert_path_value is not None: | |
| cert_data["cert_path"] = cert_path_value | |
| continue | |
| key_path_value = _field_value(line, "Private Key Path") | |
| if key_path_value is not None: | |
| cert_data["key_path"] = key_path_value | |
| if cert_data: | |
| certs.append(cert_data) | |
| certs.sort(key=lambda c: c.get("_expiry_dt", datetime.max)) | |
| for cert in certs: | |
| cert.pop("_expiry_dt", None) | |
| return certs | |
| if __name__ == "__main__": | |
| certs_json = json.dumps(get_certbot_certs(), indent=4) | |
| print(certs_json) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment