Download plugins/cert_check.py from elyanlabs/trashclaw: direct link, hf CLI and curl.
- Browser
- Download file 6.81 kB
-
https://huggingface.co/elyanlabs/trashclaw/resolve/main/plugins/cert_check.py
- Command line
-
hf download hf://elyanlabs/trashclaw/plugins/cert_check.py
-
curl -L -o cert_check.py https://huggingface.co/elyanlabs/trashclaw/resolve/main/plugins/cert_check.py
6.81 kB
| # SPDX-License-Identifier: MIT | |
| """ | |
| Cert Check Plugin for TrashClaw | |
| Check TLS certificate expiry, issuer, and validity for a domain. | |
| Pure stdlib (ssl + socket), no openssl CLI needed. | |
| Plugin contract: | |
| TOOL_DEF = dict with name, description, parameters (OpenAI function schema) | |
| run(**kwargs) -> str (called with the parsed arguments) | |
| """ | |
| import ssl | |
| import socket | |
| from datetime import datetime, timezone | |
| TOOL_DEF = { | |
| "name": "cert_check", | |
| "description": "Check TLS certificate expiry, issuer, subject, and validity for a domain. Shows days until expiration.", | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "domain": { | |
| "type": "string", | |
| "description": "Domain name to check (e.g. 'example.com')" | |
| }, | |
| "port": { | |
| "type": "integer", | |
| "description": "TLS port (default: 443)" | |
| } | |
| }, | |
| "required": ["domain"] | |
| } | |
| } | |
| def _parse_cert_time(time_str: str) -> datetime: | |
| """Parse certificate time string to datetime.""" | |
| # OpenSSL format: 'Mon DD HH:MM:SS YYYY GMT' | |
| return datetime.strptime(time_str, "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc) | |
| def _format_dn(dn_tuple) -> str: | |
| """Format a certificate distinguished name tuple into readable string.""" | |
| parts = [] | |
| if not dn_tuple: | |
| return "(empty)" | |
| for rdn in dn_tuple: | |
| for attr_type, attr_value in rdn: | |
| parts.append(f"{attr_type}={attr_value}") | |
| return ", ".join(parts) | |
| def _get_san(cert: dict) -> list: | |
| """Extract Subject Alternative Names from certificate.""" | |
| san = [] | |
| for entry_type, value in cert.get("subjectAltName", ()): | |
| san.append(f"{entry_type}:{value}") | |
| return san | |
| def run(domain: str = "", port: int = 443, **kwargs) -> str: | |
| if not domain: | |
| return "Error: 'domain' is required." | |
| # Strip protocol prefix if provided | |
| domain = domain.replace("https://", "").replace("http://", "") | |
| # Strip path | |
| domain = domain.split("/")[0] | |
| # Strip port from domain if included | |
| if ":" in domain: | |
| parts = domain.split(":") | |
| domain = parts[0] | |
| try: | |
| port = int(parts[1]) | |
| except ValueError: | |
| pass | |
| port = int(port) | |
| try: | |
| # Create SSL context that still retrieves the certificate | |
| ctx = ssl.create_default_context() | |
| conn = ctx.wrap_socket(socket.socket(socket.AF_INET), server_hostname=domain) | |
| conn.settimeout(10.0) | |
| conn.connect((domain, port)) | |
| cert = conn.getpeercert() | |
| cipher = conn.cipher() | |
| protocol = conn.version() | |
| conn.close() | |
| except ssl.SSLCertVerificationError as e: | |
| # Still try to get cert info even if verification fails | |
| try: | |
| ctx = ssl.create_default_context() | |
| ctx.check_hostname = False | |
| ctx.verify_mode = ssl.CERT_NONE | |
| conn = ctx.wrap_socket(socket.socket(socket.AF_INET), server_hostname=domain) | |
| conn.settimeout(10.0) | |
| conn.connect((domain, port)) | |
| # With CERT_NONE, getpeercert() returns empty dict; use binary | |
| cert_bin = conn.getpeercert(True) | |
| cipher = conn.cipher() | |
| protocol = conn.version() | |
| conn.close() | |
| return (f"Cert Check: {domain}:{port}\n" | |
| f"{'=' * 40}\n\n" | |
| f"WARNING: Certificate verification FAILED\n" | |
| f"Reason: {e}\n\n" | |
| f"Protocol: {protocol}\n" | |
| f"Cipher: {cipher[0] if cipher else 'unknown'}\n\n" | |
| f"The certificate exists but is not trusted.\n" | |
| f"This could mean: self-signed, expired, or wrong hostname.") | |
| except Exception as e2: | |
| return f"Error: could not connect to {domain}:{port}\n SSL error: {e}\n Fallback error: {e2}" | |
| except socket.gaierror: | |
| return f"Error: cannot resolve hostname '{domain}'." | |
| except socket.timeout: | |
| return f"Error: connection to {domain}:{port} timed out." | |
| except ConnectionRefusedError: | |
| return f"Error: connection refused to {domain}:{port}." | |
| except Exception as e: | |
| return f"Error connecting to {domain}:{port}: {e}" | |
| if not cert: | |
| return f"Error: no certificate data returned from {domain}:{port}." | |
| # Parse certificate fields | |
| subject = _format_dn(cert.get("subject", ())) | |
| issuer = _format_dn(cert.get("issuer", ())) | |
| not_before_str = cert.get("notBefore", "") | |
| not_after_str = cert.get("notAfter", "") | |
| serial = cert.get("serialNumber", "unknown") | |
| san = _get_san(cert) | |
| # Calculate expiry | |
| now = datetime.now(timezone.utc) | |
| try: | |
| not_before = _parse_cert_time(not_before_str) | |
| not_after = _parse_cert_time(not_after_str) | |
| days_remaining = (not_after - now).days | |
| total_days = (not_after - not_before).days | |
| except Exception: | |
| not_before = not_before_str | |
| not_after = not_after_str | |
| days_remaining = None | |
| total_days = None | |
| # Status determination | |
| if days_remaining is not None: | |
| if days_remaining < 0: | |
| status = "EXPIRED" | |
| status_detail = f"Expired {abs(days_remaining)} days ago!" | |
| elif days_remaining <= 7: | |
| status = "CRITICAL" | |
| status_detail = f"Expires in {days_remaining} days!" | |
| elif days_remaining <= 30: | |
| status = "WARNING" | |
| status_detail = f"Expires in {days_remaining} days" | |
| else: | |
| status = "OK" | |
| status_detail = f"{days_remaining} days remaining" | |
| else: | |
| status = "UNKNOWN" | |
| status_detail = "Could not parse dates" | |
| # Format output | |
| lines = [ | |
| f"Cert Check: {domain}:{port}", | |
| "=" * 40, | |
| "", | |
| f"Status: {status} - {status_detail}", | |
| "", | |
| f"Subject: {subject}", | |
| f"Issuer: {issuer}", | |
| f"Serial: {serial}", | |
| "", | |
| f"Valid From: {not_before_str}", | |
| f"Valid Until: {not_after_str}", | |
| ] | |
| if days_remaining is not None: | |
| lines.append(f"Remaining: {days_remaining} days") | |
| if total_days is not None: | |
| lines.append(f"Validity: {total_days} days total") | |
| if san: | |
| lines.append("") | |
| lines.append(f"SANs ({len(san)}):") | |
| for s in san[:20]: # Cap at 20 | |
| lines.append(f" {s}") | |
| if len(san) > 20: | |
| lines.append(f" ... and {len(san) - 20} more") | |
| if cipher: | |
| lines.append("") | |
| lines.append(f"Protocol: {protocol}") | |
| lines.append(f"Cipher: {cipher[0]}") | |
| if len(cipher) > 2: | |
| lines.append(f"Bits: {cipher[2]}") | |
| return "\n".join(lines) | |