forked from mxx1111/Homelab
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcerts.py
More file actions
81 lines (72 loc) · 2.91 KB
/
Copy pathcerts.py
File metadata and controls
81 lines (72 loc) · 2.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
"""TLS 证书到期检查"""
import re
import subprocess
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
_SUBJECT = re.compile(r"subject=.*?CN\s*=\s*([^,\n/]+)")
_ISSUER = re.compile(r"issuer=.*?CN\s*=\s*([^,\n/]+)")
_NOT_AFTER = re.compile(r"notAfter=(.+)")
def _check(target, warn_days, crit_days):
host = target.get("host")
port = target.get("port", 443)
label = f"{host}:{port}"
try:
raw = subprocess.run(
["openssl", "s_client", "-connect", f"{host}:{port}",
"-servername", host, "-verify_return_error"],
input="", capture_output=True, text=True, timeout=15,
)
chain_ok = "Verify return code: 0 (ok)" in raw.stdout
info = subprocess.run(
["openssl", "x509", "-noout", "-subject", "-issuer", "-dates"],
input=raw.stdout, capture_output=True, text=True, timeout=10,
).stdout
if not info.strip():
return {"target": label, "ok": False, "error": "无法读取证书"}
not_after = _NOT_AFTER.search(info)
expires_at = days_left = None
if not_after:
dt = datetime.strptime(not_after.group(1).strip(), "%b %d %H:%M:%S %Y %Z")
dt = dt.replace(tzinfo=timezone.utc)
expires_at = dt.isoformat()
days_left = (dt - datetime.now(timezone.utc)).days
level = "ok"
if days_left is not None:
if days_left <= crit_days:
level = "crit"
elif days_left <= warn_days:
level = "warn"
subject = _SUBJECT.search(info)
issuer = _ISSUER.search(info)
return {
"target": label,
"ok": True,
"subject": subject.group(1).strip() if subject else None,
"issuer": issuer.group(1).strip() if issuer else None,
"expires_at": expires_at,
"days_left": days_left,
"chain_valid": chain_ok,
"level": level,
}
except subprocess.TimeoutExpired:
return {"target": label, "ok": False, "error": "连接超时"}
except Exception as exc: # noqa: BLE001
return {"target": label, "ok": False, "error": str(exc)[:120]}
def collect(cfg):
ccfg = cfg.get("certs") or {}
targets = ccfg.get("targets") or []
if not targets:
return {"ok": True, "items": [], "level": "ok"}
warn = ccfg.get("warn_days", 30)
crit = ccfg.get("crit_days", 7)
with ThreadPoolExecutor(max_workers=min(6, len(targets))) as pool:
items = list(pool.map(lambda t: _check(t, warn, crit), targets))
items.sort(key=lambda c: (c.get("days_left") is None, c.get("days_left", 9999)))
level = "ok"
for c in items:
if c.get("level") == "crit" or not c.get("ok"):
level = "crit"
break
if c.get("level") == "warn":
level = "warn"
return {"ok": True, "items": items, "level": level}