Browse Source

Add collectd support

master
sqozz 3 years ago
parent
commit
0366c1c5e4
  1. 146
      collectd-pyopenssl.py

146
collectd-pyopenssl.py

@ -8,9 +8,7 @@ from pytz import utc as UTC
from datetime import datetime
#collectd stuff
#import collectd
import ipaddress
import collectd
import pdb
@ -18,39 +16,125 @@ DOMAIN = "geekify.de"
address_family = "ipv4"
TIMEOUT_SECONDS = 1
plugin_config = {}
def configure(config):
for c in config.children:
print(c)
machines = {}
for config_child in config.children:
key = config_child.key.lower()
values = config_child.values
# currently only "host" entries are supported
if config_child.key.lower() == "host":
# host can contain several hosts at once, even the ones containing additional config
for machine_name in values:
machine_details = {}
# host entry contains advanced/additional config options
for detail in config_child.children:
key_name = detail.key.lower()
machine_detail_values = []
# TODO: all the difference here is casting value into an int or not. This can maybe be improved or reduced somehow?
if key_name in ("addressfamily",):
for value in detail.values:
machine_detail_values.append(int(value))
elif key_name in ("sninames",):
machine_detail_values = []
for value in detail.values:
machine_detail_values.append(value)
else:
# additional keys are just ignored
pass
machine_details.update({key_name: machine_detail_values})
machines.update({machine_name: machine_details})
plugin_config.update({"machines": machines})
def check_hosts():
for host, host_config in plugin_config.get("machines", {}).items():
for address_family in host_config.get("addressfamily", [4, 6]):
for sni_name in host_config.get("sninames", [host]):
try:
cert_details = get_tls_metrics(host, sni_name, address_family)
for key in cert_details.keys():
val = collectd.Values(host="", plugin="pyopenssl", plugin_instance="host_{}-sni_{}".format(host, sni_name), type_instance="ipv{}-{}".format(address_family, key))
cert_detail = cert_details.get(key)
if key in ("valid", "expired"):
val.type = "gauge"
val.dispatch(values=[1 if cert_detail else 0])
elif key in ("notAfter", "notBefore"):
val.type = "gauge"
val.dispatch(values=[cert_detail.timestamp()])
elif key in ("remaining", "active_since"):
val.type = "gauge"
val.dispatch(values=[int(cert_detail.total_seconds())])
elif key in ("issuer", "digests"):
# currently there is no way to store static strings with collectd
pass
except Exception as e:
print("Failed to check host {} with SNI {} over IPv{}: ".format(host, sni_name, address_family), end="")
print(e)
pass
today = datetime.now(UTC)
port = 443
if address_family == "ipv4":
af = socket.AF_INET
elif address_family == "ipv6":
af = socket.AF_INET6
else:
proto = None
def get_tls_metrics(host, sni_name, address_family):
cert_details = {"valid": True}
today = datetime.now(UTC)
port = 443
if address_family == 4:
af = socket.AF_INET
elif address_family == 6:
af = socket.AF_INET6
else:
proto = None
try:
addrinfo = socket.getaddrinfo(DOMAIN, None, af, proto=socket.IPPROTO_TCP)
addrinfo = socket.getaddrinfo(host, None, af, proto=socket.IPPROTO_TCP)
# addrinfo first contains an array of connections made, we want the first ([0]) and only one we made
# this tuple then contains (family, type, proto, canonname, sockaddr) - we want sockaddr == [4]
# this then can contain either (host, port) or (host, port, flowinfo, scopeid) depending on set address family. Since host is anyways at [0] we take this as final ip address
host = addrinfo[0][4][0]
except socket.gaierror:
host = DOMAIN
context = ssl.create_default_context()
with socket.create_connection((host, port), TIMEOUT_SECONDS) as sock:
with context.wrap_socket(sock, server_hostname=DOMAIN) as sslsock:
der_cert = sslsock.getpeercert(True)
pem_cert = ssl.DER_cert_to_PEM_cert(der_cert)
x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, der_cert)
exp = x509.get_notAfter().decode("utf-8")
expirary_date = date_parser.parse(exp)
delta = expirary_date - today
print(delta.total_seconds())
pdb.set_trace()
context = ssl.create_default_context()
while True:
try:
with socket.create_connection((host, port), TIMEOUT_SECONDS) as sock:
with context.wrap_socket(sock, server_hostname=sni_name) as sslsock:
der_cert = sslsock.getpeercert(True)
pem_cert = ssl.DER_cert_to_PEM_cert(der_cert)
break
except ssl.CertificateError as error:
if context.verify_mode == ssl.CERT_REQUIRED and context.check_hostname:
cert_details["valid"] = False
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
else:
raise error
x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, der_cert)
notAfter = x509.get_notAfter().decode("utf-8")
notAfter = date_parser.parse(notAfter)
notBefore = x509.get_notBefore().decode("utf-8")
notBefore = date_parser.parse(notBefore)
remaining = notAfter - today
active_since = today - notBefore
issuer_components = x509.get_issuer().get_components()
issuer_components_decoded = list(map(lambda bin_component: list(map(lambda keyvalue: keyvalue.decode("utf-8"), bin_component)), issuer_components))
joined_components = list(map(lambda x: "=".join(x), issuer_components_decoded))
issuer = ",".join(joined_components)
digests = {}
for digest_type in ("md5", "sha1", "sha256"):
digests.update({digest_type: x509.digest(digest_type).decode("utf-8")})
cert_details.update({
"expired": x509.has_expired(),
"notAfter": notAfter,
"notBefore": notBefore,
"remaining": remaining,
"active_since": active_since,
"issuer": issuer,
"digests": digests,
})
return cert_details
collectd.register_config(configure)
collectd.register_read(check_hosts)

Loading…
Cancel
Save