diff --git a/datadog_checks_base/datadog_checks/base/data/agent_requirements.in b/datadog_checks_base/datadog_checks/base/data/agent_requirements.in index df90b03d1d641b..46bb202911197d 100644 --- a/datadog_checks_base/datadog_checks/base/data/agent_requirements.in +++ b/datadog_checks_base/datadog_checks/base/data/agent_requirements.in @@ -14,7 +14,7 @@ flup-py3==1.0.3; python_version > '3.0' gearman==2.0.2; sys_platform != 'win32' and python_version < '3.0' httplib2==0.10.3 in-toto==0.3.0 -ipaddress==1.0.22 +ipaddress==1.0.22; python_version < '3.0' jaydebeapi==1.1.1 jpype1==0.6.3 kafka-python==1.4.4; sys_platform != 'win32' @@ -56,6 +56,7 @@ scandir==1.8 securesystemslib[crypto,pynacl]==0.11.3 selectors34==1.2.0; sys_platform == 'win32' and python_version < '3.4' serpent==1.27; sys_platform == 'win32' +service_identity[idna]==18.1.0 simplejson==3.6.5 six==1.12.0 supervisor==3.3.3; python_version < '3.0' diff --git a/ssh_check/requirements.in b/ssh_check/requirements.in index db86e170108365..921cf57f6071b2 100644 --- a/ssh_check/requirements.in +++ b/ssh_check/requirements.in @@ -2,7 +2,7 @@ asn1crypto==0.24.0 cffi==1.11.5 cryptography==2.3.1 enum34==1.1.6 -ipaddress==1.0.22 +ipaddress==1.0.22; python_version < '3.0' paramiko==2.1.5 pyasn1==0.4.2 pycparser==2.18 diff --git a/ssl/CHANGELOG.md b/ssl/CHANGELOG.md deleted file mode 100644 index 34ee6fdea8ecb4..00000000000000 --- a/ssl/CHANGELOG.md +++ /dev/null @@ -1,2 +0,0 @@ -# CHANGELOG - Ssl - diff --git a/ssl/datadog_checks/ssl/config.py b/ssl/datadog_checks/ssl/config.py deleted file mode 100644 index a61687c4269899..00000000000000 --- a/ssl/datadog_checks/ssl/config.py +++ /dev/null @@ -1,54 +0,0 @@ -# (C) Datadog, Inc. 2019 -# All rights reserved -# Licensed under a 3-clause BSD style license (see LICENSE) - -# from datadog_checks.config import is_affirmative - -# compatability layer for agents under 6.6.0 -try: - from datadog_checks.errors import ConfigurationError -except ImportError: - ConfigurationError = Exception - - -class SslConfig: - """ - A config object. Parse the instance and return it as an object that can be passed around - No need to parse the instance more than once in the check run - """ - - def __init__(self, instance): - self.name = instance.get('name') - self.host = instance.get('host') - self.port = instance.get('port') - self.host_and_port = "{}({})".format(self.host, self.port) - self.timeout = instance.get('timeout') - self.days_warning = instance.get('days_warning', '14') - self.days_critical = instance.get('days_critical', '7') - self.check_hostname = instance.get('check_hostname', 'true') - self.ssl_hostname = instance.get('ssl_hostname') - self.custom_tags = instance.get('tags', []) - self.local_cert_path = instance.get('local_cert_path') - # We would tag by name parameter, IP/URL/hostname+port, - # TLS version. If possible to protocols, tag by protocol. - # Tags would be added to all the metrics and service checks. - self.tags = [ - "name:{}".format(self.name), "ssl_version:unknown" - ] + self.custom_tags - if not self.local_cert_path: - self.tags = self.tags + ["host:{}".format(self.host), - "port:{}".format(self.port)] - - self.cert_remote = True - - # check if remote/local - if self.local_cert_path: - print("getting local") - self.cert_remote = False - - # self.get_config - - def check_properly_configured(self): - if not self.local_cert_path and not self.host: - msg = "Check must be configured with either a local certificate path or remote host." - raise ConfigurationError(msg) diff --git a/ssl/datadog_checks/ssl/ssl.py b/ssl/datadog_checks/ssl/ssl.py deleted file mode 100644 index e6f1b0263415db..00000000000000 --- a/ssl/datadog_checks/ssl/ssl.py +++ /dev/null @@ -1,109 +0,0 @@ -# (C) Datadog, Inc. 2019 -# All rights reserved -# Licensed under a 3-clause BSD style license (see LICENSE) -# to prevent conflicts with built-in SSL libraries: -from __future__ import absolute_import -# import pdb -from datadog_checks.base import AgentCheck -from datadog_checks.errors import CheckException -from .config import SslConfig -from cryptography import x509 -from cryptography.hazmat.backends import default_backend - -from datetime import datetime -import ssl -import socket - -DEFAULT_EXPIRE_DAYS_WARNING = 14 -DEFAULT_EXPIRE_DAYS_CRITICAL = 7 -DEFAULT_EXPIRE_WARNING = DEFAULT_EXPIRE_DAYS_WARNING * 24 * 3600 -DEFAULT_EXPIRE_CRITICAL = DEFAULT_EXPIRE_DAYS_CRITICAL * 24 * 3600 - - -class SslCheck(AgentCheck): - SERVICE_CHECK_CAN_CONNECT = 'ssl_cert.can_connect' - SERVICE_CHECK_EXPIRATION = 'ssl_cert.expiration' - SERVICE_CHECK_IS_VALID = 'ssl_cert.is_valid' - - def check(self, instance): - config = SslConfig(instance) - config.check_properly_configured() - - if config.cert_remote: - self.check_remote_cert(config.host, config.port) - else: - self.check_local_cert(config.local_cert_path) - - url = config.host_and_port - ssl_version = "unknown" - tags = ['url:%s' % url] - # pdb.set_trace() - - if not url: - raise CheckException("Configuration error, url field missing, please fix ssl.yaml") - - try: - context = ssl.create_default_context() - sock = socket.create_connection((url, 443)) - ssl_sock = context.wrap_socket(sock, server_hostname=url) - print(ssl_sock.getpeercert()) - ssl_version = ssl_sock.version() - print(ssl_version) - tags.append('ssl_version:%s' % ssl_version) - except Exception as e: - # Something went horribly wrong. Ideally we'd be more specific... - print("Exception: " + str(e)) - self.service_check('ssl_cert.can_connect', self.CRITICAL, tags=tags) - - def check_expiration(self, exp_date): - # add variables for custom configured thresholds - seconds_warning = \ - DEFAULT_EXPIRE_WARNING - seconds_critical = \ - DEFAULT_EXPIRE_CRITICAL - time_left = exp_date - datetime.utcnow() - days_left = time_left.days - seconds_left = time_left.total_seconds() - print("Exp_date: {}".format(exp_date)) - if seconds_left < seconds_critical: - print('critical', days_left, seconds_left, - "This cert TTL is critical: only {} days before it expires".format(days_left)) - elif seconds_left < seconds_warning: - print('warning', days_left, seconds_left, - "This cert is almost expired, only {} days left".format(days_left)) - else: - print('up', days_left, seconds_left, "Days left: {}".format(days_left)) - - def can_connect(self, status, message=''): - print("can_connect: {}, {}".format(status, message)) - - def is_valid(self, status, message=''): - print("is_valid: {}, {}".format(status, message)) - - def is_expiring(self, status, message=''): - print("is_expiring: {}, {}".format(status, message)) - - def check_protocol_version(self, hostname, port): - context = ssl.create_default_context() - sock = socket.create_connection((hostname, port)) - ssock = context.wrap_socket(sock, server_hostname=hostname) - print(ssock.version()) - - def check_remote_cert(self, host, port): - try: - context = ssl.create_default_context() - sock = socket.create_connection((host, port)) - ssock = context.wrap_socket(sock, server_hostname=host) - return x509.load_der_x509_certificate(ssock.getpeercert(binary_form=True), default_backend()) - except Exception as e: - self.can_connect('critical', e) - - def check_local_cert(self, local_cert_path): - try: - local_cert_file = open(local_cert_path, 'rb') - local_cert_data = x509.load_pem_x509_certificate(local_cert_file.read(), default_backend()) - self.can_connect('up') - return local_cert_data - except Exception as e: - self.can_connect('critical', e) - # self.service_check('my_check.all_good', self.CRITICAL, e) diff --git a/ssl/manifest.json b/ssl/manifest.json deleted file mode 100644 index fc287946aef3f5..00000000000000 --- a/ssl/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "display_name": "SSL", - "maintainer": "help@datadoghq.com", - "manifest_version": "1.0.0", - "name": "ssl", - "metric_prefix": "ssl_cert.", - "metric_to_check": "", - "creates_events": false, - "short_description": "Monitor SSL certificates for expiration, validity, etc.", - "guid": "4e27a211-a034-42dd-9939-9ef967b1da50", - "support": "core", - "supported_os": ["linux", "mac_os", "windows"], - "public_title": "Datadog-SSL Integration", - "categories": [""], - "type": "check", - "is_public": false -} diff --git a/ssl/metadata.csv b/ssl/metadata.csv deleted file mode 100644 index 3882d5dac67289..00000000000000 --- a/ssl/metadata.csv +++ /dev/null @@ -1,3 +0,0 @@ -metric_name,metric_type,interval,unit_name,per_unit_name,description,orientation,integration,short_name -ssl_cert.days_left,gauge,,day,,Days until SSL certificate expiration,1,network,days till expiration -ssl_cert.seconds_left,gauge,,second,,Seconds until SSL certificate expiration,1,network,seconds till expiration \ No newline at end of file diff --git a/ssl/mock.cert b/ssl/mock.cert deleted file mode 100644 index 6b23b6dbe97321..00000000000000 --- a/ssl/mock.cert +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBzCCAe+gAwIBAgIJAMxMOYQndt2IMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV -BAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xOTAzMDcyMTMxNDhaFw0yOTAzMDQyMTMx -NDhaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBAM3FJhZaotf94d/NNAlYSB6cMiKH15pymbWCXWRFpzRJ -tNStadhiY5sHPtMPL5uNKbS0M/OfzPOrxBmtB9dNnx8oUd6CO53G9IFzr73Fwp+Y -EfXbcVtgOjeIYoyKdNM3AVEBhpColhClwehDBMZl1x2oavkOrqb1A76DGwsnyDaE -4ETwRjZ0JUL17YeNExFHKyl1L0EjtAyt/XsUVqbMEi6cQoKE6eQNUwoKkq6CfiR1 -h2BgyYxd76m/fimIIprjIyTEbnGjZcxiYOBjAoYZxDmBqzR1OVUaT3k71P7I74Vz -0jqCjtiA+StJJJhupWBs9MMmIrozLIFbXIwl1JPOxscCAwEAAaNQME4wHQYDVR0O -BBYEFIsjUnjk/G7tKB871u5Gp8odD5dRMB8GA1UdIwQYMBaAFIsjUnjk/G7tKB87 -1u5Gp8odD5dRMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAIAyzAfZ -WJk0njDruk9xwlO9S4O42N0TA2jDVZ7ORliWLBzOV+H6YdIJ5KjwfR+iPKQyqR0t -1j0r/CndA+8diQFAXg5E3F+Mn2YMr/O81kkY6D05Bgm8svrGLK5RAqbqQpBEn2Ee -fwz9+ZgrkkvctqbSlET50DnEpRp4Jzf8ux2rv0Fzl/EbYeJCs+FK+aG1jMqsg3a4 -tmELU9tCFb+aWQNBNtCCp9wv11/iiBMz0JiTVmiPefc0zx5EizCVhGRfFy7bL7vu -Ck7ekgJKAmRSki0UK88ytefMMvJ95hqNV8ACua56N5Jg6DcRP4otYfEzAt+bYG/p -c/YVcp5m0lYrRvc= ------END CERTIFICATE----- diff --git a/ssl/mock.key b/ssl/mock.key deleted file mode 100644 index 4f36790a71c66a..00000000000000 --- a/ssl/mock.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAzcUmFlqi1/3h3800CVhIHpwyIofXmnKZtYJdZEWnNEm01K1p -2GJjmwc+0w8vm40ptLQz85/M86vEGa0H102fHyhR3oI7ncb0gXOvvcXCn5gR9dtx -W2A6N4hijIp00zcBUQGGkKiWEKXB6EMExmXXHahq+Q6upvUDvoMbCyfINoTgRPBG -NnQlQvXth40TEUcrKXUvQSO0DK39exRWpswSLpxCgoTp5A1TCgqSroJ+JHWHYGDJ -jF3vqb9+KYgimuMjJMRucaNlzGJg4GMChhnEOYGrNHU5VRpPeTvU/sjvhXPSOoKO -2ID5K0kkmG6lYGz0wyYiujMsgVtcjCXUk87GxwIDAQABAoIBAHKQOUxo2hF3gLKi -BT7OsBiazi77gsay13F/S4/wV8982fkvM6cN4HcH6aqI4fCw/29khSg26F0iXOQO -ujbdOKKYSDyiNZ76jlckmcwGtv00JYvEybAeO0t/255z1+dg0JLRTqJrumE6aGNw -LNBpEwOoKSbRTdwWvibrx7rGQ2pWQRI8elMvEWWfZKk2hWjDoj6cyxwrT6e040/3 -FeawUAcwr7ov1eHeivNEHrnJ4wQ50g5gBAMTdXeNn+PVdvZlXeGxCZsXiyp9Xzf0 -oZCgpiZCMq/9rpyMV7CO8sCr1rhyj6cj2JsXyVMZSwCHbNcL7t/ethWEDjXMrJkR -PXNtiQECgYEA56JoJB3JDuyuG80ciAKGlTLDt1bKiXBHT/xIAHl3GjUqFD47d+Rz -SUhAzrQmOQJF7dYya/6wEJfCuWVRL2LJxTlOpt1WCMNDP7+zRXuRPk/Wdr8gf994 -YraU1q4VW6HKyVIeZbxZcP+leUwyN47Sy3OQMED3Tewq2hNAPJK+5bECgYEA42pA -pnLbNoWI6Wj5utPd3Q/mxylVxZ2MQyedUPkIeaI30K5fOPXcSqDY9lbpST5/JS9Z -9c6ff7AFsmcBKOgWFNkAZIpcBmpxoCoIaPsGw34uXphaLb28f/MoEI887WIp9c/z -dQ8EiHyre/gVRSL7gIHWVyM7DrpFF4IPloMW+fcCgYEAmy6A2oFh66/AzTtS/APF -KjuCR1fMBNvFyt5XVooJsvMfJ8SxjpbsbZIMaO7kFJljQ/2WdieAyP0DJbWs+DQU -vR6xhLUQKHq4mQLTFZ+9JKPUKGXPXhvWyTtM565k/Kid9NYhK0NOIJgEwgi8M+Bp -dIgNd2ZuyDOKWeM/y9T8M2ECgYBZz6p8kbuVCqOJWpXVfLqQBSmk2eQvyQqNsuVk -BxWbw767QUIF4sL/DgrhLm2vKWlZLBwhAVroOIHndNp2Z0HhNdn72tCBECSTlkX/ -/7MqHXj+jrh+fAInncXi1E7BiRT9KQUC481sgZ/Ps6fix+//Tkdx3k4CgPmsUQcK -7/Zw/QKBgF8B45y5NRJ48dC9QNArPnbKfkYplGoWuVRif502apwK8/yZIPduLe2M -Biq1qDxwvhzFDIFlkcUeJWJdNcs7thATrYNbkyQy65ByhJpBHPm0ERnjSmKv2zTT -5r3/6ZAN2+yxqgqxlj1EydbhUUAVDN+RQGo2VqANlvDF/kDf2Tm+ ------END RSA PRIVATE KEY----- diff --git a/ssl/my-ssl.py b/ssl/my-ssl.py deleted file mode 100644 index a906956040ba8b..00000000000000 --- a/ssl/my-ssl.py +++ /dev/null @@ -1,126 +0,0 @@ -import socket -import ssl -# import requests -from datetime import datetime -# from datadog_checks.checks import AgentCheck -from cryptography import x509 -from cryptography.hazmat.backends import default_backend - -DEFAULT_EXPIRE_DAYS_WARNING = 14 -DEFAULT_EXPIRE_DAYS_CRITICAL = 7 -DEFAULT_EXPIRE_WARNING = DEFAULT_EXPIRE_DAYS_WARNING * 24 * 3600 -DEFAULT_EXPIRE_CRITICAL = DEFAULT_EXPIRE_DAYS_CRITICAL * 24 * 3600 - - -def main(): - # get and check config - hostname = 'google.com' - # hostname = '' - # ip = '1.1.1.1' - port = '443' - # local_cert_path = 'mock.cert' - - -### - -# for hash event storing to check for changes: -# hash_mutable function in datadogchecksdev -# pass dictionary (certificate name, expirtation, etc), it will give you hash - -# config.check_properly_configured() - - # config must include local or remote endpoint for checking SSL cert - # check if path/URL is valid - - # try to connect/read local file - # if can't can_connect critical, otherwise up - # if len(hostname) > 1: - # print("getting remote") - # cert_data = check_remote_cert(hostname, port) - # elif len(local_cert_path) > 1: - # print("getting local") - # cert_data = check_local_cert(local_cert_path) - # else: - # print("Hostname or local path to certificate are required config options.") - -# self.check_function = check_properly_configured - - # TLS/SSL protocol only applies to remote certs, maybe not useful? - # https://www.sslsupportdesk.com/clearing-confusion-tls-ssl-certificates-are-the-same-thing/ - check_protocol_version(hostname, port) - - # read cert, check is_valid - # some exceptions we can use: https://cryptography.io/en/latest/x509/reference/#cryptography.x509.InvalidVersion - # will probably need to make some invalid certs: - # https://cryptography.io/en/latest/x509/reference/#x-509-certificate-builder - - # remote_cert = x509.load_der_x509_certificate(peer_cert, default_backend()) - # print(cert_data.version) - # print(peer_cert['notAfter']) - # print(remote_cert.not_valid_after) - - # calculate expiration, send metrics, tags - # check_expiration(cert_data.not_valid_after) - - -def check_expiration(exp_date): - # add variables for custom configured thresholds - seconds_warning = \ - DEFAULT_EXPIRE_WARNING - seconds_critical = \ - DEFAULT_EXPIRE_CRITICAL - time_left = exp_date - datetime.utcnow() - days_left = time_left.days - seconds_left = time_left.total_seconds() - print("Exp_date: {}".format(exp_date)) - if seconds_left < seconds_critical: - print('critical', days_left, seconds_left, - "This cert TTL is critical: only {} days before it expires".format(days_left)) - elif seconds_left < seconds_warning: - print('warning', days_left, seconds_left, - "This cert is almost expired, only {} days left".format(days_left)) - else: - print('up', days_left, seconds_left, "Days left: {}".format(days_left)) - - -def can_connect(status, message=''): - print("can_connect: {}, {}".format(status, message)) - - -def is_valid(status, message=''): - print("is_valid: {}, {}".format(status, message)) - - -def is_expiring(status, message=''): - print("is_expiring: {}, {}".format(status, message)) - - -def check_protocol_version(hostname, port): - context = ssl.create_default_context() - sock = socket.create_connection((hostname, port)) - ssock = context.wrap_socket(sock, server_hostname=hostname) - print(ssock.version()) - - -def check_remote_cert(hostname, port): - try: - context = ssl.create_default_context() - sock = socket.create_connection((hostname, port)) - ssock = context.wrap_socket(sock, server_hostname=hostname) - return x509.load_der_x509_certificate(ssock.getpeercert(binary_form=True), default_backend()) - except Exception as e: - can_connect('critical', e) - - -def check_local_cert(local_cert_path): - try: - local_cert_file = open(local_cert_path, 'rb') - local_cert_data = x509.load_pem_x509_certificate(local_cert_file.read(), default_backend()) - can_connect('up') - return local_cert_data - except Exception as e: - can_connect('critical', e) - # self.service_check('my_check.all_good', self.CRITICAL, e) - - -main() diff --git a/ssl/requirements.in b/ssl/requirements.in deleted file mode 100644 index 364047de22069c..00000000000000 --- a/ssl/requirements.in +++ /dev/null @@ -1 +0,0 @@ -cryptography==2.3.1 \ No newline at end of file diff --git a/ssl/tcp-ssl.py b/ssl/tcp-ssl.py deleted file mode 100644 index 5aa549aebc4d2f..00000000000000 --- a/ssl/tcp-ssl.py +++ /dev/null @@ -1,261 +0,0 @@ -# (C) Datadog, Inc. 2010-2017 -# All rights reserved -# Licensed under Simplified BSD License (see LICENSE) - -# stdlib -import socket -import time -import ssl -from datetime import datetime - -# project -# dn -from datadog_checks.checks import NetworkCheck, Status -from datadog_checks.base import is_affirmative -from datadog_checks.base.utils.ca_cert import get_ca_certs_path - - -DEFAULT_EXPIRE_DAYS_WARNING = 14 -DEFAULT_EXPIRE_DAYS_CRITICAL = 7 -DEFAULT_EXPIRE_WARNING = DEFAULT_EXPIRE_DAYS_WARNING * 24 * 3600 -DEFAULT_EXPIRE_CRITICAL = DEFAULT_EXPIRE_DAYS_CRITICAL * 24 * 3600 - - -class BadConfException(Exception): - # dn - pass - - -class TCPCheck(NetworkCheck): - # dn - SOURCE_TYPE_NAME = 'system' - SERVICE_CHECK_CAN_CONNECT = 'tcp.can_connect' - SERVICE_CHECK_SSL_CERT = 'tcp.ssl_cert' - - def __init__(self, name, init_config, agentConfig, instances=None): - # dn - NetworkCheck.__init__(self, name, init_config, agentConfig, instances) -# dn - ca_certs are for.,.. - self.ca_certs = init_config.get('ca_certs') - if not self.ca_certs: - self.ca_certs = get_ca_certs_path() - - def _load_conf(self, instance): - # Fetches the conf - - port = instance.get('port', None) - timeout = float(instance.get('timeout', 10)) - response_time = instance.get('collect_response_time', False) - custom_tags = instance.get('tags', []) - socket_type = None - try: - port = int(port) - except Exception: - raise BadConfException("{} is not a correct port.".format(str(port))) - try: - url = instance.get('host', None) - split = url.split(":") - except Exception: # Would be raised if url is not a string - raise BadConfException("A valid url must be specified") - - # IPv6 address format: 2001:db8:85a3:8d3:1319:8a2e:370:7348 - if len(split) == 8: # It may then be a IP V6 address, we check that - for block in split: - if len(block) != 4: - raise BadConfException("{} is not a correct IPv6 address.".format(url)) - - addr = url - # It's a correct IP V6 address - socket_type = socket.AF_INET6 - - if socket_type is None: - try: - addr = socket.gethostbyname(url) - socket_type = socket.AF_INET - except Exception: - msg = "URL: {} is not a correct IPv4, IPv6 or hostname".format(url) - raise BadConfException(msg) - - check_certificate_expiration = is_affirmative(instance.get('check_certificate_expiration', False)) - ssl_server_name = instance.get('ssl_server_name') or url - ca_certs = self.ca_certs or instance.get('ca_certs') - client_key = instance.get('client_key') - client_cert = instance.get('client_cert') - check_hostname = is_affirmative(instance.get('check_hostname', True)) - try: - days_warning = int(instance.get('days_warning', DEFAULT_EXPIRE_DAYS_WARNING)) - except Exception: - raise BadConfException("{} should be an integer".format(instance['days_warning'])) - try: - days_critical = int(instance.get('days_critical', DEFAULT_EXPIRE_DAYS_CRITICAL)) - except Exception: - raise BadConfException("{} should be an integer".format(instance['days_critical'])) - try: - seconds_warning = int(instance.get('seconds_warning', 0)) - except Exception: - raise BadConfException("{} should be an integer".format(instance['seconds_warning'])) - try: - seconds_critical = int(instance.get('seconds_critical', 0)) - except Exception: - raise BadConfException("{} should be an integer".format(instance['seconds_critical'])) - - return url, addr, port, custom_tags, socket_type, timeout, response_time, \ - check_certificate_expiration, ssl_server_name, ca_certs, \ - client_key, client_cert, check_hostname, \ - days_warning, days_critical, seconds_warning, seconds_critical - - def check_cert_expiration(self, url, addr, port, timeout, ca_certs, - check_hostname, ssl_server_name, - days_warning, days_critical, seconds_warning, seconds_critical, - client_key=None, client_cert=None): - # thresholds expressed in seconds take precedence over those expressed in days - seconds_warning = seconds_warning or days_warning * 24 * 3600 - seconds_critical = seconds_critical or days_critical * 24 * 3600 - server_name = ssl_server_name or url - - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(float(timeout)) - sock.connect((url, port)) - context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) - context.verify_mode = ssl.CERT_REQUIRED - context.check_hostname = check_hostname - context.load_verify_locations(ca_certs) - - if client_cert and client_key: - context.load_cert_chain(client_cert, keyfile=client_key) - - ssl_sock = context.wrap_socket(sock, server_hostname=server_name) - cert = ssl_sock.getpeercert() - - except ssl.CertificateError as e: - self.log.debug("The hostname on the SSL certificate does not match the given host: {}".format(e)) - return Status.CRITICAL, 0, 0, str(e) - except ssl.SSLError as e: - self.log.debug("error: {}. Cert might be expired.".format(e)) - return Status.DOWN, 0, 0, str(e) - except Exception as e: - self.log.debug("Site is down, unable to connect to get cert expiration: {}".format(e)) - return Status.DOWN, 0, 0, str(e) - - exp_date = datetime.strptime(cert['notAfter'], "%b %d %H:%M:%S %Y %Z") - time_left = exp_date - datetime.utcnow() - days_left = time_left.days - seconds_left = time_left.total_seconds() - - self.log.debug("Exp_date: {}".format(exp_date)) - self.log.debug("seconds_left: {}".format(seconds_left)) - - if seconds_left < seconds_critical: - return (Status.CRITICAL, days_left, seconds_left, - "This cert TTL is critical: only {} days before it expires".format(days_left)) - - elif seconds_left < seconds_warning: - return (Status.WARNING, days_left, seconds_left, - "This cert is almost expired, only {} days left".format(days_left)) - - else: - return Status.UP, days_left, seconds_left, "Days left: {}".format(days_left) - - def _check(self, instance): - url, addr, port, custom_tags, socket_type, timeout, response_time, \ - check_certificate_expiration, ssl_server_name, ca_certs, \ - client_key, client_cert, check_hostname, \ - days_warning, days_critical, seconds_warning, seconds_critical = self._load_conf(instance) - status_checks = [] - - start = time.time() - try: - self.log.debug("Connecting to {} {}".format(addr, port)) - sock = socket.socket(socket_type) - try: - sock.settimeout(timeout) - sock.connect((addr, port)) - finally: - sock.close() - - except socket.timeout as e: - # The connection timed out because it took more time than the specified value in the yaml config file - length = int((time.time() - start) * 1000) - self.log.info("{}:{} is DOWN ({}). Connection failed after {} ms".format(addr, port, str(e), length)) - status_checks.append(( - self.SERVICE_CHECK_CAN_CONNECT, - Status.DOWN, - "{}. Connection failed after {} ms".format(str(e), length))) - - except socket.error as e: - length = int((time.time() - start) * 1000) - if "timed out" in str(e): - - # The connection timed out becase it took more time than the system tcp stack allows - self.log.warning('The connection timed out because it took more time ' - 'than the system tcp stack allows. You might want to ' - 'change this setting to allow longer timeouts') - self.log.info("System tcp timeout. Assuming that the checked system is down") - status_checks.append(( - self.SERVICE_CHECK_CAN_CONNECT, - Status.DOWN, - """Socket error: {}. - The connection timed out after {} ms because it took more time than the system tcp stack allows. - You might want to change this setting to allow longer timeouts""".format(str(e), length))) - else: - self.log.info("{}:{} is DOWN ({}). Connection failed after {} ms".format(addr, port, str(e), length)) - status_checks.append(( - self.SERVICE_CHECK_CAN_CONNECT, - Status.DOWN, - "{}. Connection failed after {} ms".format(str(e), length))) - - except Exception as e: - length = int((time.time() - start) * 1000) - self.log.info("{}:{} is DOWN ({}). Connection failed after {} ms".format(addr, port, str(e), length)) - status_checks.append(( - self.SERVICE_CHECK_CAN_CONNECT, - Status.DOWN, - "{}. Connection failed after {} ms".format(str(e), length))) - else: - self.log.debug("{}:{} is UP".format(addr, port)) - status_checks.append((self.SERVICE_CHECK_CAN_CONNECT, Status.UP, "")) - - if response_time: - self.gauge('network.tcp.response_time', time.time() - start, - tags=['url:{}:{}'.format(instance.get('host', None), port), - 'instance:{}'.format(instance.get('name'))] + custom_tags) - - if check_certificate_expiration: - ssl_check_status, days_left, seconds_left, message = self.check_cert_expiration( - url, addr, port, timeout, ca_certs, - check_hostname, ssl_server_name, - days_warning, days_critical, seconds_warning, seconds_critical, - client_key, client_cert) - tags_list = custom_tags + [ - 'target_host:{}'.format(url), - 'port:{}'.format(port), - 'instance:{}'.format(self.normalize(instance['name'])) - ] - self.gauge('tcp.ssl.days_left', days_left, tags=tags_list) - self.gauge('tcp.ssl.seconds_left', seconds_left, tags=tags_list) - status_checks.append((self.SERVICE_CHECK_SSL_CERT, ssl_check_status, message)) - - return status_checks - - def report_as_service_check(self, sc_name, status, instance, msg=None): - instance_name = self.normalize(instance['name']) - host = instance.get('host', None) - port = instance.get('port', None) - custom_tags = instance.get('tags', []) - - if status == Status.UP: - msg = None - - tags = custom_tags + ['target_host:{}'.format(host), - 'port:{}'.format(port), - 'instance:{}'.format(instance_name)] - - self.service_check(sc_name, - NetworkCheck.STATUS_TO_SERVICE_CHECK[status], - tags=tags, - message=msg - ) - # Report as a metric as well - if sc_name == self.SERVICE_CHECK_CAN_CONNECT: - self.gauge("network.tcp.can_connect", 1 if status == Status.UP else 0, tags=tags) diff --git a/ssl/tests/common.py b/ssl/tests/common.py deleted file mode 100644 index b37ed380d51af4..00000000000000 --- a/ssl/tests/common.py +++ /dev/null @@ -1,9 +0,0 @@ -# (C) Datadog, Inc. 2018 -# All rights reserved -# Licensed under a 3-clause BSD style license (see LICENSE) -import os - -HERE = os.path.dirname(os.path.abspath(__file__)) -ROOT = os.path.dirname(os.path.dirname(HERE)) - -CHECK_NAME = 'ssl' diff --git a/ssl/tests/conftest.py b/ssl/tests/conftest.py deleted file mode 100644 index 788845c5b5ddf6..00000000000000 --- a/ssl/tests/conftest.py +++ /dev/null @@ -1,25 +0,0 @@ -# (C) Datadog, Inc. 2019 -# All rights reserved -# Licensed under a 3-clause BSD style license (see LICENSE) -import pytest - - -@pytest.fixture(scope='session') -def dd_environment(): - yield - - -@pytest.fixture -def instance_local_cert(): - return { - 'local_cert_path': "mock.cert", - # 'ssl_version': "tls_1.2" - } - - -@pytest.fixture -def instance_remote_cert(): - return { - 'host': "http://www.google.com", - # 'ssl_version': "tls_1.2" - } diff --git a/ssl/tests/test_ssl.py b/ssl/tests/test_ssl.py deleted file mode 100644 index 7e106f782d2ffc..00000000000000 --- a/ssl/tests/test_ssl.py +++ /dev/null @@ -1,17 +0,0 @@ -# (C) Datadog, Inc. 2019 -# All rights reserved -# Licensed under a 3-clause BSD style license (see LICENSE) -from datadog_checks.ssl import SslCheck -from . import common - - -def test_local_cert(aggregator, instance_local_cert): - ssl_check = SslCheck(common.CHECK_NAME, {}, {}) - ssl_check.check(instance_local_cert) - aggregator.assert_all_metrics_covered() - - -def test_remote_cert(aggregator, instance_remote_cert): - ssl_check = SslCheck(common.CHECK_NAME, {}, {}) - ssl_check.check(instance_remote_cert) - aggregator.assert_all_metrics_covered() diff --git a/tls/CHANGELOG.md b/tls/CHANGELOG.md new file mode 100644 index 00000000000000..ce4d38a34c2ce1 --- /dev/null +++ b/tls/CHANGELOG.md @@ -0,0 +1,2 @@ +# CHANGELOG - TLS + diff --git a/ssl/MANIFEST.in b/tls/MANIFEST.in similarity index 100% rename from ssl/MANIFEST.in rename to tls/MANIFEST.in diff --git a/ssl/README.md b/tls/README.md similarity index 98% rename from ssl/README.md rename to tls/README.md index 06a874e38e8d7b..9ff1375a948542 100644 --- a/ssl/README.md +++ b/tls/README.md @@ -1,8 +1,8 @@ -# SSL Check +# TLS Check ## Overview -This check monitors various aspects of SSL certificates. +This check monitors various aspects of TLS certificates. ## Setup diff --git a/ssl/datadog_checks/__init__.py b/tls/datadog_checks/__init__.py similarity index 100% rename from ssl/datadog_checks/__init__.py rename to tls/datadog_checks/__init__.py diff --git a/ssl/datadog_checks/ssl/__about__.py b/tls/datadog_checks/tls/__about__.py similarity index 100% rename from ssl/datadog_checks/ssl/__about__.py rename to tls/datadog_checks/tls/__about__.py diff --git a/ssl/datadog_checks/ssl/__init__.py b/tls/datadog_checks/tls/__init__.py similarity index 65% rename from ssl/datadog_checks/ssl/__init__.py rename to tls/datadog_checks/tls/__init__.py index 4f7eb3e6023a0f..946c5ffd2adc40 100644 --- a/ssl/datadog_checks/ssl/__init__.py +++ b/tls/datadog_checks/tls/__init__.py @@ -2,9 +2,6 @@ # All rights reserved # Licensed under a 3-clause BSD style license (see LICENSE) from .__about__ import __version__ -from .ssl import SslCheck +from .tls import TLSCheck -__all__ = [ - '__version__', - 'SslCheck' -] +__all__ = ['__version__', 'TLSCheck'] diff --git a/ssl/datadog_checks/ssl/data/conf.yaml.example b/tls/datadog_checks/tls/data/conf.yaml.example similarity index 96% rename from ssl/datadog_checks/ssl/data/conf.yaml.example rename to tls/datadog_checks/tls/data/conf.yaml.example index 2e1bb831dc183d..baa1a1920d01c6 100644 --- a/ssl/datadog_checks/ssl/data/conf.yaml.example +++ b/tls/datadog_checks/tls/data/conf.yaml.example @@ -23,4 +23,4 @@ instances: # days_warning: 14 # days_critical: 7 # check_hostname: true - # ssl_hostname: \ No newline at end of file + # ssl_hostname: diff --git a/tls/datadog_checks/tls/tls.py b/tls/datadog_checks/tls/tls.py new file mode 100644 index 00000000000000..28c3471295267c --- /dev/null +++ b/tls/datadog_checks/tls/tls.py @@ -0,0 +1,272 @@ +# (C) Datadog, Inc. 2019 +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +import socket +import ssl +from datetime import datetime +from os.path import expanduser, isdir + +import service_identity +from cryptography.hazmat.backends import default_backend +from cryptography.x509 import load_der_x509_certificate, load_pem_x509_certificate +from six import text_type +from six.moves.urllib.parse import urlparse + +from datadog_checks.base import AgentCheck, is_affirmative + +from .utils import closing, days_to_seconds, is_ip_address, seconds_to_days + + +class TLSCheck(AgentCheck): + SERVICE_CHECK_CAN_CONNECT = 'tls.can_connect' + SERVICE_CHECK_VALIDATION = 'tls.cert_valid' + SERVICE_CHECK_EXPIRATION = 'tls.cert_expiration' + + DEFAULT_EXPIRE_DAYS_WARNING = 14 + DEFAULT_EXPIRE_DAYS_CRITICAL = 7 + DEFAULT_EXPIRE_SECONDS_WARNING = days_to_seconds(DEFAULT_EXPIRE_DAYS_WARNING) + DEFAULT_EXPIRE_SECONDS_CRITICAL = days_to_seconds(DEFAULT_EXPIRE_DAYS_CRITICAL) + + def __init__(self, name, init_config, instances): + super(TLSCheck, self).__init__(name, init_config, instances) + + host = self.instance.get('host', '') + parsed_uri = urlparse(host) + + # Handle IP addresses, see https://bugs.python.org/issue754016 + if not parsed_uri.hostname: + parsed_uri = urlparse('//{}'.format(host)) + + self._host = parsed_uri.hostname + + # TODO: Support (implement) UDP + # https://chris-wood.github.io/2016/05/06/OpenSSL-DTLS.html + transport = self.instance.get('transport', 'tcp').lower() + if transport == 'udp': + # SOCK_DGRAM + self._sock_type = socket.SOCK_STREAM + # Default to 4433 (no standard port, but it's what OpenSSL uses) + self._port = int(self.instance.get('port', parsed_uri.port or 443)) + else: + self._sock_type = socket.SOCK_STREAM + self._port = int(self.instance.get('port', parsed_uri.port or 443)) + + self._name = self.instance.get('name') + self._local_cert_path = self.instance.get('local_cert_path') + self._timeout = float(self.instance.get('timeout', 10)) + self._server_hostname = self.instance.get('server_hostname', self._host) + + self._validate_hostname = is_affirmative(self.instance.get('validate_hostname', True)) + if is_ip_address(self._server_hostname): + self._hostname_validation = (service_identity.cryptography.verify_certificate_ip_address, 'IP address') + else: + self._hostname_validation = (service_identity.cryptography.verify_certificate_hostname, 'hostname') + + self._cert = self.instance.get('cert') + if self._cert: + self._cert = expanduser(self._cert) + + self._private_key = self.instance.get('private_key') + if self._private_key: + self._private_key = expanduser(self._private_key) + + self._cafile = None + self._capath = None + ca_cert = self.instance.get('ca_cert') + if ca_cert: + ca_cert = expanduser(ca_cert) + if isdir(ca_cert): + self._capath = ca_cert + else: + self._cafile = ca_cert + + # Thresholds expressed in seconds take precedence over those expressed in days + self._seconds_warning = ( + int(self.instance.get('seconds_warning', 0)) + or days_to_seconds(float(self.instance.get('days_warning', 0))) + or self.DEFAULT_EXPIRE_SECONDS_WARNING + ) + self._seconds_critical = ( + int(self.instance.get('seconds_critical', 0)) + or days_to_seconds(float(self.instance.get('days_critical', 0))) + or self.DEFAULT_EXPIRE_SECONDS_CRITICAL + ) + + self._tags = self.instance.get('tags', []) + if self._name: + self._tags.append('name:{}'.format(self._name)) + + # Decide the method of collection for this instance + if self._local_cert_path: + self.check = self.check_local + if self._host: + self._tags.append('host:{}'.format(self._host)) + else: + self.check = self.check_remote + self._tags.append('host:{}'.format(self._host)) + self._tags.append('port:{}'.format(self._port)) + + def check_remote(self, instance): + context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=self._cafile, capath=self._capath) + if self._cert: + context.load_cert_chain(self._cert, keyfile=self._private_key) + + # Run our own validation later on if need be + context.check_hostname = False + + try: + sock = self.create_connection() + except Exception as e: + self.service_check(self.SERVICE_CHECK_CAN_CONNECT, self.CRITICAL, tags=self._tags, message=str(e)) + return + else: + self.service_check(self.SERVICE_CHECK_CAN_CONNECT, self.OK, tags=self._tags) + + with closing(sock): + try: + with closing(context.wrap_socket(sock, server_hostname=self._server_hostname)) as secure_sock: + der_cert = secure_sock.getpeercert(binary_form=True) + # protocol_version = secure_sock.version() + except Exception as e: + message = str(e) + self.service_check(self.SERVICE_CHECK_VALIDATION, self.CRITICAL, tags=self._tags, message=str(message)) + + # There's no sane way to tell it to not validate expiration + # This only works on Python 3.7+ + if 'expired' in message: + self.service_check( + self.SERVICE_CHECK_EXPIRATION, self.CRITICAL, tags=self._tags, message='Certificate has expired' + ) + + return + + try: + cert = load_der_x509_certificate(der_cert, default_backend()) + except Exception as e: + self.service_check( + self.SERVICE_CHECK_VALIDATION, + self.CRITICAL, + tags=self._tags, + message='Unable to parse the certificate: {}'.format(e), + ) + return + + self.validate_certificate(cert) + self.check_age(cert) + + def check_local(self, instance): + try: + with open(self._local_cert_path, 'rb') as f: + cert = f.read() + except Exception as e: + self.service_check( + self.SERVICE_CHECK_VALIDATION, + self.CRITICAL, + tags=self._tags, + message='Unable to open the certificate: {}'.format(e), + ) + return + + if self._local_cert_path.endswith(('.cer', '.crt', '.der')): + loader = load_der_x509_certificate + else: + loader = load_pem_x509_certificate + + try: + cert = loader(cert, default_backend()) + except Exception as e: + self.service_check( + self.SERVICE_CHECK_VALIDATION, + self.CRITICAL, + tags=self._tags, + message='Unable to parse the certificate: {}'.format(e), + ) + return + + self.validate_certificate(cert) + self.check_age(cert) + + def validate_certificate(self, cert): + if self._validate_hostname and self._server_hostname: + validator, host_type = self._hostname_validation + + try: + validator(cert, text_type(self._server_hostname)) + except service_identity.VerificationError: + self.service_check( + self.SERVICE_CHECK_VALIDATION, + self.CRITICAL, + tags=self._tags, + message='The {} on the certificate does not match the given host'.format(host_type), + ) + return + except service_identity.CertificateError as e: + self.service_check( + self.SERVICE_CHECK_VALIDATION, + self.CRITICAL, + tags=self._tags, + message='The certificate contains invalid/unexpected data: {}'.format(e), + ) + return + + self.service_check(self.SERVICE_CHECK_VALIDATION, self.OK, tags=self._tags) + + def check_age(self, cert): + delta = cert.not_valid_after - datetime.utcnow() + seconds_left = delta.total_seconds() + days_left = seconds_to_days(seconds_left) + + self.gauge('tls.days_left', days_left, tags=self._tags) + self.gauge('tls.seconds_left', seconds_left, tags=self._tags) + + if seconds_left <= 0: + self.service_check( + self.SERVICE_CHECK_EXPIRATION, self.CRITICAL, tags=self._tags, message='Certificate has expired' + ) + elif seconds_left < self._seconds_critical: + self.service_check( + self.SERVICE_CHECK_EXPIRATION, + self.CRITICAL, + tags=self._tags, + message='Certificate will expire in only {} days'.format(days_left), + ) + elif seconds_left < self._seconds_warning: + self.service_check( + self.SERVICE_CHECK_EXPIRATION, + self.WARNING, + tags=self._tags, + message='Certificate will expire in {} days'.format(days_left), + ) + else: + self.service_check(self.SERVICE_CHECK_EXPIRATION, self.OK, tags=self._tags) + + def create_connection(self): + """See: https://github.com/python/cpython/blob/40ee9a3640d702bce127e9877c82a99ce817f0d1/Lib/socket.py#L691""" + err = None + try: + for res in socket.getaddrinfo(self._host, self._port, 0, self._sock_type): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket.socket(af, socktype, proto) + sock.settimeout(self._timeout) + sock.connect(sa) + # Break explicitly a reference cycle + err = None + return sock + + except socket.error as _: + err = _ + if sock is not None: + sock.close() + + if err is not None: + raise err + else: + raise socket.error('No valid addresses found, try checking your IPv6 connectivity') + except socket.gaierror as e: + err_code, message = e.args + if err_code == socket.EAI_NODATA or err_code == socket.EAI_NONAME: + raise socket.error('Unable to resolve host, check your DNS: {}'.format(message)) + + raise diff --git a/tls/datadog_checks/tls/utils.py b/tls/datadog_checks/tls/utils.py new file mode 100644 index 00000000000000..685b846fd90185 --- /dev/null +++ b/tls/datadog_checks/tls/utils.py @@ -0,0 +1,38 @@ +# (C) Datadog, Inc. 2019 +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +from __future__ import division + +from ipaddress import ip_address + +from six import PY2, text_type + + +def is_ip_address(hostname): + try: + ip_address(text_type(hostname)) + except ValueError: + return False + + return True + + +def days_to_seconds(days): + return int(days * 24 * 60 * 60) + + +def seconds_to_days(seconds): + return seconds / 60 / 60 / 24 + + +if PY2: + from contextlib import closing as _closing + + def closing(sock): + return _closing(sock) + + +else: + + def closing(sock): + return sock diff --git a/ssl/logos/README.md b/tls/logos/README.md similarity index 100% rename from ssl/logos/README.md rename to tls/logos/README.md diff --git a/tls/manifest.json b/tls/manifest.json new file mode 100644 index 00000000000000..8de2a46ce0cc4a --- /dev/null +++ b/tls/manifest.json @@ -0,0 +1,25 @@ +{ + "display_name": "TLS", + "maintainer": "help@datadoghq.com", + "manifest_version": "1.0.0", + "name": "tls", + "metric_prefix": "tls.", + "metric_to_check": "tls.seconds_left", + "creates_events": false, + "short_description": "Monitor TLS for protocol version, certificate expiration & validity, etc.", + "guid": "4e27a211-a034-42dd-9939-9ef967b1da50", + "support": "core", + "supported_os": [ + "linux", + "mac_os", + "windows" + ], + "public_title": "Datadog-TLS Integration", + "categories": [ + "network", + "web" + ], + "type": "check", + "is_public": false, + "integration_id": "tls" +} diff --git a/tls/metadata.csv b/tls/metadata.csv new file mode 100644 index 00000000000000..9e21104e83e9b0 --- /dev/null +++ b/tls/metadata.csv @@ -0,0 +1,3 @@ +metric_name,metric_type,interval,unit_name,per_unit_name,description,orientation,integration,short_name +tls.days_left,gauge,,day,,Days until X.509 certificate expiration,1,tls,Days until expiration +tls.seconds_left,gauge,,second,,Seconds until X.509 certificate expiration,1,tls,Seconds until expiration diff --git a/ssl/requirements-dev.txt b/tls/requirements-dev.txt similarity index 100% rename from ssl/requirements-dev.txt rename to tls/requirements-dev.txt diff --git a/tls/requirements.in b/tls/requirements.in new file mode 100644 index 00000000000000..a21ea81b4271ac --- /dev/null +++ b/tls/requirements.in @@ -0,0 +1,3 @@ +cryptography==2.3.1 +ipaddress==1.0.22; python_version < '3.0' +service_identity[idna]==18.1.0 diff --git a/ssl/setup.py b/tls/setup.py similarity index 85% rename from ssl/setup.py rename to tls/setup.py index 0cfd5667749f05..a7d443acfb11a9 100644 --- a/ssl/setup.py +++ b/tls/setup.py @@ -10,7 +10,7 @@ # Get version info ABOUT = {} -with open(path.join(HERE, 'datadog_checks', 'ssl', '__about__.py')) as f: +with open(path.join(HERE, 'datadog_checks', 'tls', '__about__.py')) as f: exec(f.read(), ABOUT) # Get the long description from the README file @@ -22,23 +22,19 @@ setup( - name='datadog-ssl', + name='datadog-tls', version=ABOUT['__version__'], - description='The Ssl check', + description='The TLS check', long_description=long_description, long_description_content_type='text/markdown', - keywords='datadog agent ssl check', - + keywords='datadog agent tls check', # The project's main homepage. url='https://github.com/DataDog/integrations-core', - # Author details author='Datadog', author_email='packages@datadoghq.com', - # License license='BSD-3-Clause', - # See https://pypi.org/classifiers classifiers=[ 'Development Status :: 5 - Production/Stable', @@ -47,16 +43,12 @@ 'Topic :: System :: Monitoring', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', ], - # The package we're going to ship - packages=['datadog_checks.ssl'], - + packages=['datadog_checks.tls'], # Run-time dependencies install_requires=[CHECKS_BASE_REQ], - # Extra files to ship with the wheel package include_package_data=True, ) diff --git a/ssl/tests/__init__.py b/tls/tests/__init__.py similarity index 100% rename from ssl/tests/__init__.py rename to tls/tests/__init__.py diff --git a/tls/tests/common.py b/tls/tests/common.py new file mode 100644 index 00000000000000..1ac5a91e72aded --- /dev/null +++ b/tls/tests/common.py @@ -0,0 +1,6 @@ +# (C) Datadog, Inc. 2019 +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +from datadog_checks.dev import get_here + +HERE = get_here() diff --git a/tls/tests/conftest.py b/tls/tests/conftest.py new file mode 100644 index 00000000000000..a5cf31572d4d86 --- /dev/null +++ b/tls/tests/conftest.py @@ -0,0 +1,100 @@ +# (C) Datadog, Inc. 2019 +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +import pytest + +from datadog_checks.tls import TLSCheck +from datadog_checks.tls.utils import days_to_seconds + +from .utils import download_cert + + +@pytest.fixture(scope='session', autouse=True) +def dd_environment(): + yield {'host': 'https://www.google.com'} + + +@pytest.fixture(scope='session') +def local_all_ok(): + instance = {'host': 'https://www.google.com'} + + with download_cert('ok.pem', instance['host']) as cert: + instance['local_cert_path'] = cert + + yield TLSCheck('tls', {}, [instance]) + + +@pytest.fixture(scope='session') +def local_all_ok_der(): + instance = {'host': 'https://www.google.com'} + + with download_cert('ok.crt', instance['host'], raw=True) as cert: + instance['local_cert_path'] = cert + + yield TLSCheck('tls', {}, [instance]) + + +@pytest.fixture +def remote_all_ok(): + instance = {'host': 'https://www.google.com'} + return TLSCheck('tls', {}, [instance]) + + +@pytest.fixture +def remote_all_ok_ip(): + instance = {'host': '1.1.1.1'} + return TLSCheck('tls', {}, [instance]) + + +@pytest.fixture +def remote_no_resolve(): + instance = {'host': 'https://this.does.not.exist.foo'} + return TLSCheck('tls', {}, [instance]) + + +@pytest.fixture +def remote_no_connect(): + instance = {'host': 'localhost', 'port': 56789} + return TLSCheck('tls', {}, [instance]) + + +@pytest.fixture +def remote_no_connect_port_in_host(): + instance = {'host': 'localhost:56789'} + return TLSCheck('tls', {}, [instance]) + + +@pytest.fixture +def remote_hostname_mismatch(): + instance = {'host': 'https://wrong.host.badssl.com'} + return TLSCheck('tls', {}, [instance]) + + +@pytest.fixture +def remote_cert_expired(): + instance = {'host': 'https://expired.badssl.com'} + return TLSCheck('tls', {}, [instance]) + + +@pytest.fixture +def remote_cert_critical_days(): + instance = {'host': 'https://sha256.badssl.com', 'days_critical': 1000} + return TLSCheck('tls', {}, [instance]) + + +@pytest.fixture +def remote_cert_critical_seconds(): + instance = {'host': 'https://sha256.badssl.com', 'days_critical': -1, 'seconds_critical': days_to_seconds(1000)} + return TLSCheck('tls', {}, [instance]) + + +@pytest.fixture +def remote_cert_warning_days(): + instance = {'host': 'https://sha256.badssl.com', 'days_warning': 1000} + return TLSCheck('tls', {}, [instance]) + + +@pytest.fixture +def remote_cert_warning_seconds(): + instance = {'host': 'https://sha256.badssl.com', 'days_warning': -1, 'seconds_warning': days_to_seconds(1000)} + return TLSCheck('tls', {}, [instance]) diff --git a/tls/tests/test_tls.py b/tls/tests/test_tls.py new file mode 100644 index 00000000000000..31616c455ba6df --- /dev/null +++ b/tls/tests/test_tls.py @@ -0,0 +1,284 @@ +# (C) Datadog, Inc. 2019 +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +from six import PY2 + + +class TestLocal: + def test_all_ok(self, aggregator, local_all_ok): + local_all_ok.check(None) + + aggregator.assert_service_check(local_all_ok.SERVICE_CHECK_CAN_CONNECT, count=0) + aggregator.assert_service_check( + local_all_ok.SERVICE_CHECK_VALIDATION, status=local_all_ok.OK, tags=local_all_ok._tags, count=1 + ) + aggregator.assert_service_check( + local_all_ok.SERVICE_CHECK_EXPIRATION, status=local_all_ok.OK, tags=local_all_ok._tags, count=1 + ) + + aggregator.assert_metric('tls.days_left', count=1) + aggregator.assert_metric('tls.seconds_left', count=1) + aggregator.assert_all_metrics_covered() + + def test_all_ok_der(self, aggregator, local_all_ok_der): + local_all_ok_der.check(None) + + aggregator.assert_service_check(local_all_ok_der.SERVICE_CHECK_CAN_CONNECT, count=0) + aggregator.assert_service_check( + local_all_ok_der.SERVICE_CHECK_VALIDATION, status=local_all_ok_der.OK, tags=local_all_ok_der._tags, count=1 + ) + aggregator.assert_service_check( + local_all_ok_der.SERVICE_CHECK_EXPIRATION, status=local_all_ok_der.OK, tags=local_all_ok_der._tags, count=1 + ) + + aggregator.assert_metric('tls.days_left', count=1) + aggregator.assert_metric('tls.seconds_left', count=1) + aggregator.assert_all_metrics_covered() + + +class TestRemote: + def test_all_ok(self, aggregator, remote_all_ok): + remote_all_ok.check(None) + + aggregator.assert_service_check( + remote_all_ok.SERVICE_CHECK_CAN_CONNECT, status=remote_all_ok.OK, tags=remote_all_ok._tags, count=1 + ) + aggregator.assert_service_check( + remote_all_ok.SERVICE_CHECK_VALIDATION, status=remote_all_ok.OK, tags=remote_all_ok._tags, count=1 + ) + aggregator.assert_service_check( + remote_all_ok.SERVICE_CHECK_EXPIRATION, status=remote_all_ok.OK, tags=remote_all_ok._tags, count=1 + ) + + aggregator.assert_metric('tls.days_left', count=1) + aggregator.assert_metric('tls.seconds_left', count=1) + aggregator.assert_all_metrics_covered() + + def test_all_ok_ip(self, aggregator, remote_all_ok_ip): + remote_all_ok_ip.check(None) + + aggregator.assert_service_check( + remote_all_ok_ip.SERVICE_CHECK_CAN_CONNECT, status=remote_all_ok_ip.OK, tags=remote_all_ok_ip._tags, count=1 + ) + aggregator.assert_service_check( + remote_all_ok_ip.SERVICE_CHECK_VALIDATION, status=remote_all_ok_ip.OK, tags=remote_all_ok_ip._tags, count=1 + ) + aggregator.assert_service_check( + remote_all_ok_ip.SERVICE_CHECK_EXPIRATION, status=remote_all_ok_ip.OK, tags=remote_all_ok_ip._tags, count=1 + ) + + aggregator.assert_metric('tls.days_left', count=1) + aggregator.assert_metric('tls.seconds_left', count=1) + aggregator.assert_all_metrics_covered() + + def test_no_resolve(self, aggregator, remote_no_resolve): + remote_no_resolve.check(None) + + aggregator.assert_service_check( + remote_no_resolve.SERVICE_CHECK_CAN_CONNECT, + status=remote_no_resolve.CRITICAL, + tags=remote_no_resolve._tags, + count=1, + ) + aggregator.assert_service_check(remote_no_resolve.SERVICE_CHECK_VALIDATION, count=0) + aggregator.assert_service_check(remote_no_resolve.SERVICE_CHECK_EXPIRATION, count=0) + + message = 'Unable to resolve host, check your DNS' + assert message in aggregator.service_checks(remote_no_resolve.SERVICE_CHECK_CAN_CONNECT)[0].message + + aggregator.assert_all_metrics_covered() + + def test_no_connect(self, aggregator, remote_no_connect): + remote_no_connect.check(None) + + aggregator.assert_service_check( + remote_no_connect.SERVICE_CHECK_CAN_CONNECT, + status=remote_no_connect.CRITICAL, + tags=remote_no_connect._tags, + count=1, + ) + aggregator.assert_service_check(remote_no_connect.SERVICE_CHECK_VALIDATION, count=0) + aggregator.assert_service_check(remote_no_connect.SERVICE_CHECK_EXPIRATION, count=0) + + message = 'Unable to resolve host, check your DNS' + assert message not in aggregator.service_checks(remote_no_connect.SERVICE_CHECK_CAN_CONNECT)[0].message + + aggregator.assert_all_metrics_covered() + + def test_no_connect_port_in_host(self, aggregator, remote_no_connect_port_in_host): + remote_no_connect_port_in_host.check(None) + + aggregator.assert_service_check( + remote_no_connect_port_in_host.SERVICE_CHECK_CAN_CONNECT, + status=remote_no_connect_port_in_host.CRITICAL, + tags=remote_no_connect_port_in_host._tags, + count=1, + ) + aggregator.assert_service_check(remote_no_connect_port_in_host.SERVICE_CHECK_VALIDATION, count=0) + aggregator.assert_service_check(remote_no_connect_port_in_host.SERVICE_CHECK_EXPIRATION, count=0) + + message = 'Unable to resolve host, check your DNS' + assert ( + message + not in aggregator.service_checks(remote_no_connect_port_in_host.SERVICE_CHECK_CAN_CONNECT)[0].message + ) + + aggregator.assert_all_metrics_covered() + + def test_hostname_mismatch(self, aggregator, remote_hostname_mismatch): + remote_hostname_mismatch.check(None) + + aggregator.assert_service_check( + remote_hostname_mismatch.SERVICE_CHECK_CAN_CONNECT, + status=remote_hostname_mismatch.OK, + tags=remote_hostname_mismatch._tags, + count=1, + ) + aggregator.assert_service_check( + remote_hostname_mismatch.SERVICE_CHECK_VALIDATION, + status=remote_hostname_mismatch.CRITICAL, + tags=remote_hostname_mismatch._tags, + count=1, + ) + aggregator.assert_service_check( + remote_hostname_mismatch.SERVICE_CHECK_EXPIRATION, + status=remote_hostname_mismatch.OK, + tags=remote_hostname_mismatch._tags, + count=1, + ) + + aggregator.assert_metric('tls.days_left', count=1) + aggregator.assert_metric('tls.seconds_left', count=1) + aggregator.assert_all_metrics_covered() + + def test_cert_expired(self, aggregator, remote_cert_expired): + remote_cert_expired.check(None) + + aggregator.assert_service_check( + remote_cert_expired.SERVICE_CHECK_CAN_CONNECT, + status=remote_cert_expired.OK, + tags=remote_cert_expired._tags, + count=1, + ) + aggregator.assert_service_check( + remote_cert_expired.SERVICE_CHECK_VALIDATION, + status=remote_cert_expired.CRITICAL, + tags=remote_cert_expired._tags, + count=1, + ) + if PY2: + aggregator.assert_service_check(remote_cert_expired.SERVICE_CHECK_EXPIRATION, count=0) + else: + aggregator.assert_service_check( + remote_cert_expired.SERVICE_CHECK_EXPIRATION, + status=remote_cert_expired.CRITICAL, + tags=remote_cert_expired._tags, + message='Certificate has expired', + count=1, + ) + + aggregator.assert_all_metrics_covered() + + def test_cert_critical_days(self, aggregator, remote_cert_critical_days): + remote_cert_critical_days.check(None) + + aggregator.assert_service_check( + remote_cert_critical_days.SERVICE_CHECK_CAN_CONNECT, + status=remote_cert_critical_days.OK, + tags=remote_cert_critical_days._tags, + count=1, + ) + aggregator.assert_service_check( + remote_cert_critical_days.SERVICE_CHECK_VALIDATION, + status=remote_cert_critical_days.OK, + tags=remote_cert_critical_days._tags, + count=1, + ) + aggregator.assert_service_check( + remote_cert_critical_days.SERVICE_CHECK_EXPIRATION, + status=remote_cert_critical_days.CRITICAL, + tags=remote_cert_critical_days._tags, + count=1, + ) + + aggregator.assert_metric('tls.days_left', count=1) + aggregator.assert_metric('tls.seconds_left', count=1) + aggregator.assert_all_metrics_covered() + + def test_cert_critical_seconds(self, aggregator, remote_cert_critical_seconds): + remote_cert_critical_seconds.check(None) + + aggregator.assert_service_check( + remote_cert_critical_seconds.SERVICE_CHECK_CAN_CONNECT, + status=remote_cert_critical_seconds.OK, + tags=remote_cert_critical_seconds._tags, + count=1, + ) + aggregator.assert_service_check( + remote_cert_critical_seconds.SERVICE_CHECK_VALIDATION, + status=remote_cert_critical_seconds.OK, + tags=remote_cert_critical_seconds._tags, + count=1, + ) + aggregator.assert_service_check( + remote_cert_critical_seconds.SERVICE_CHECK_EXPIRATION, + status=remote_cert_critical_seconds.CRITICAL, + tags=remote_cert_critical_seconds._tags, + count=1, + ) + + aggregator.assert_metric('tls.days_left', count=1) + aggregator.assert_metric('tls.seconds_left', count=1) + aggregator.assert_all_metrics_covered() + + def test_cert_warning_days(self, aggregator, remote_cert_warning_days): + remote_cert_warning_days.check(None) + + aggregator.assert_service_check( + remote_cert_warning_days.SERVICE_CHECK_CAN_CONNECT, + status=remote_cert_warning_days.OK, + tags=remote_cert_warning_days._tags, + count=1, + ) + aggregator.assert_service_check( + remote_cert_warning_days.SERVICE_CHECK_VALIDATION, + status=remote_cert_warning_days.OK, + tags=remote_cert_warning_days._tags, + count=1, + ) + aggregator.assert_service_check( + remote_cert_warning_days.SERVICE_CHECK_EXPIRATION, + status=remote_cert_warning_days.WARNING, + tags=remote_cert_warning_days._tags, + count=1, + ) + + aggregator.assert_metric('tls.days_left', count=1) + aggregator.assert_metric('tls.seconds_left', count=1) + aggregator.assert_all_metrics_covered() + + def test_cert_warning_seconds(self, aggregator, remote_cert_warning_seconds): + remote_cert_warning_seconds.check(None) + + aggregator.assert_service_check( + remote_cert_warning_seconds.SERVICE_CHECK_CAN_CONNECT, + status=remote_cert_warning_seconds.OK, + tags=remote_cert_warning_seconds._tags, + count=1, + ) + aggregator.assert_service_check( + remote_cert_warning_seconds.SERVICE_CHECK_VALIDATION, + status=remote_cert_warning_seconds.OK, + tags=remote_cert_warning_seconds._tags, + count=1, + ) + aggregator.assert_service_check( + remote_cert_warning_seconds.SERVICE_CHECK_EXPIRATION, + status=remote_cert_warning_seconds.WARNING, + tags=remote_cert_warning_seconds._tags, + count=1, + ) + + aggregator.assert_metric('tls.days_left', count=1) + aggregator.assert_metric('tls.seconds_left', count=1) + aggregator.assert_all_metrics_covered() diff --git a/tls/tests/utils.py b/tls/tests/utils.py new file mode 100644 index 00000000000000..6986ce2c889aea --- /dev/null +++ b/tls/tests/utils.py @@ -0,0 +1,46 @@ +# (C) Datadog, Inc. 2019 +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +import os +import socket +import ssl +import time +from contextlib import contextmanager + +from six.moves.urllib.parse import urlparse + +from datadog_checks.dev import TempDir +from datadog_checks.tls.utils import closing + + +@contextmanager +def download_cert(name, host, raw=False): + host = urlparse(host).hostname + context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + for _ in range(20): + try: + with closing(socket.create_connection((host, 443))) as sock: + with closing(context.wrap_socket(sock, server_hostname=host)) as secure_sock: + cert = secure_sock.getpeercert(binary_form=True) + except Exception: # no cov + time.sleep(3) + else: + break + else: # no cov + raise Exception('Unable to connect to {}'.format(host)) + + with TempDir() as d: + path = os.path.join(d, name) + + if raw: + with open(path, 'wb') as f: + f.write(cert) + else: + cert = ssl.DER_cert_to_PEM_cert(cert) + with open(path, 'w') as f: + f.write(cert) + + yield path diff --git a/ssl/tox.ini b/tls/tox.ini similarity index 96% rename from ssl/tox.ini rename to tls/tox.ini index c02eca249f713b..7097b8697dd989 100644 --- a/ssl/tox.ini +++ b/tls/tox.ini @@ -4,7 +4,6 @@ skip_missing_interpreters = true basepython = py37 envlist = py{27,37} - flake8 [testenv] dd_check_style = true