From f6c1a4666e04cbc8f5d4ad67843250e3bdbed285 Mon Sep 17 00:00:00 2001 From: Myles Braithwaite Date: Sun, 6 Nov 2022 00:13:33 -0500 Subject: [PATCH] Added a Python version --- python_app/README.md | 66 ++++ python_app/config.json | 7 + python_app/get_cert_info.py | 106 ++++++ python_app/monitored_hosts.json | 10 + .../js/tls-dashboard/example_certificates.js | 342 +++++++----------- .../example_certificates.js.orig | 219 +++++++++++ 6 files changed, 537 insertions(+), 213 deletions(-) create mode 100644 python_app/README.md create mode 100644 python_app/config.json create mode 100644 python_app/get_cert_info.py create mode 100644 python_app/monitored_hosts.json create mode 100644 web_service/js/tls-dashboard/example_certificates.js.orig diff --git a/python_app/README.md b/python_app/README.md new file mode 100644 index 0000000..5250392 --- /dev/null +++ b/python_app/README.md @@ -0,0 +1,66 @@ +# tls-dashboard +A dashboard written in JavaScript & HTML to check the remaining time before a TLS certificate expires. A combination of a Node module and an HTML/CSS/JS webpage to display the info. + +**Version:** 1.1.0 + +## Node Setup +### `node_app/config.js` +Contains the configuration variables for the node script. + +* `connection_timeout` - The time in milliseconds that node should leave a connection open without response after the socket has been assigned. Once the timeout expires, node emits a `timeout` event and aborts the connection request. Default is 5000ms. +* `output_file` + * `path` - The path to the directory that you want the output file written to. Can be relative or absolute, requires a trailing `/`, and defaults to the `../web_service/js/tls-dashboard/` directory. If you move the contents of the `./web_service` directory, make sure you update this path. + * `name` - The name of the output file. This typically doesn't need to be changed, but if you do change it, you'll also need to change the filename in `index.html` at line 14. + + +### `node_app/monitored_hosts.js` +Contains an array of all of the hostnames that you want to monitor. + +### `node_app/get_cert_info.js` +This module performs the actual HTTPS connection and evaluation of the peer certificates, and outputs the results into a flat file. There are no configuration changes needed in this file. To get things going, you can either: + +1. Run the script manually whenever you need to update your dashboard by calling `node get_cert_info.js`; or +2. Set up the script to run on a cronjob + +It's entirely up to you how you want to handle it. + +## Python Setup +### `python_app/config.json` +Contains the configuration variables for the python script. + +* `connection_timeout` - The time in milliseconds that node should leave a connection open without response after the socket has been assigned. Once the timeout expires, node emits a `timeout` event and aborts the connection request. Default is 5000ms. +* `output_file` + * `path` - The path to the directory that you want the output file written to. Can be relative or absolute, requires a trailing `/`, and defaults to the `../web_service/js/tls-dashboard/` directory. If you move the contents of the `./web_service` directory, make sure you update this path. + * `name` - The name of the output file. This typically doesn't need to be changed, but if you do change it, you'll also need to change the filename in `index.html` at line 14. + +### `python_app/monitored_hosts.json` +Contains an array of all of the hostnames that you want to monitor. + +### `python_app/get_cert_info.py` +This module performs the actual HTTPS connection and evaluation of the peer certificates, and outputs the results into a flat file. There are no configuration changes needed in this file. To get things going, you can either: +1. Run teh script manually whenever you need to update your dashboard by calling `python3 get_cert_info.py` +2. Set up the script to run on a cronjob + +It's entirely up to you how you want to handle it. + +## Web Service Setup +To get the web service started, you'll need to either move the contents of the `web_service` directory to somewhere in your web site's path, or create a symlink from the web site path back to the directory. If you move the contents, please update the `output_file.path` config value. These are static files with relative links, so other than moving them/pointing the server to them, there's nothing else required for you to do. + +## Example +Take a look at a live example page [here on GitLab][1]. Screenshots below. + +![Example dashboard](https://raw.githubusercontent.com/cmrunton/tls-dashboard/master/screenshot.png) +![Example dashboard](https://raw.githubusercontent.com/cmrunton/tls-dashboard/master/screenshot_2.png) + +## TODO +1. Database integration? +2. Slack integration? + +## Dependencies +The node module has no dependencies external to the node core. The following dependencies are provided for the web service to render properly. + +* jQuery v2.2.2 +* Handlebars v4.0.5 +* Bootstrap v4.0.0-alpha (CSS only) + +[1]:https://craine.gitlab.io/tls-dashboard/ diff --git a/python_app/config.json b/python_app/config.json new file mode 100644 index 0000000..2aaf12d --- /dev/null +++ b/python_app/config.json @@ -0,0 +1,7 @@ +{ + "connection_timeout": 5000, + "output_file": { + "path": "../web_service/js/tls-dashboard/", + "name": "certificates.js" + } +} diff --git a/python_app/get_cert_info.py b/python_app/get_cert_info.py new file mode 100644 index 0000000..ca3158f --- /dev/null +++ b/python_app/get_cert_info.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 + +import re +import os +import ssl +import json +import socket +import datetime + +OUTPUT_TEMPLATE = """var run_date = '{run_date}'; +var cert_info = {cert_info}""" + + +def parse_date(date_string): + """ + Takes a date string and returns the nuumber of days between now and + the future date + """ + return datetime.datetime.strptime(date_string, "%b %d %X %Y %Z") + + +def camelcase_to_underscore(name): + """ + Takes the SSL camelcase stuff and converts it to underscore + Found on StackOverflow + """ + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + + +def get_cert_parameters(hostname, timeout=5000, ssl_port=443): + """ + Creates a connection to the host, and then reads the resulting peer + certificate to extract the desired info + """ + context = ssl.create_default_context() + + sock = socket.socket(socket.AF_INET) + sock.settimeout(timeout) + + connection = context.wrap_socket(sock, server_hostname=hostname) + connection.connect((hostname, ssl_port)) + + certificate = connection.getpeercert() + + connection.close() + + cert_info = { + "server": hostname, + "subject": {}, + "issuer": {}, + "info": {} + } + + for s in certificate.get('subject', None): + subject = s[0] + key = camelcase_to_underscore(subject[0]) + cert_info['subject'][key] = subject[1] + + for i in certificate.get('issuer', None): + issuer = i[0] + key = camelcase_to_underscore(issuer[0]) + cert_info['issuer'][key] = issuer[1] + + cert_info['info']['valid_from'] = parse_date(certificate['notAfter']) + cert_info['info']['valid_to'] = parse_date(certificate['notBefore']) + + time_left = cert_info['info']['valid_from'] - datetime.datetime.now() + + cert_info['info']['days_left'] = time_left.days + + return cert_info + + +def json_default(obj): + if isinstance(obj, datetime.datetime): + return obj.isoformat() + + +def main(): + ROOT_PATH = os.path.dirname(os.path.abspath(__file__)) + + with open(os.path.join(ROOT_PATH, 'config.json'), 'r') as f: + config = json.loads(f.read()) + + with open(os.path.join(ROOT_PATH, 'monitored_hosts.json'), 'r') as f: + monitored_hosts = json.loads(f.read()) + + cert_info = {} + count = 1 + + for host in monitored_hosts['hosts']: + cert_info[count] = get_cert_parameters(host) + count += 1 + + with open(os.path.join(config['output_file']['path'], + config['output_file']['name']), 'w') as f: + run_date = datetime.datetime.now().strftime('%a %b %d %Y') + json_cert_info = json.dumps(cert_info, indent=4, default=json_default) + + f.write(OUTPUT_TEMPLATE.format(cert_info=json_cert_info, + run_date=run_date)) + + +if __name__ == '__main__': + main() diff --git a/python_app/monitored_hosts.json b/python_app/monitored_hosts.json new file mode 100644 index 0000000..698c451 --- /dev/null +++ b/python_app/monitored_hosts.json @@ -0,0 +1,10 @@ +{ + "hosts": [ + "www.google.com", + "www.twitter.com", + "www.github.com", + "www.bitbucket.com", + "news.ycombinator.com", + "barnacl.es" + ] +} diff --git a/web_service/js/tls-dashboard/example_certificates.js b/web_service/js/tls-dashboard/example_certificates.js index e2b21b9..220dff1 100644 --- a/web_service/js/tls-dashboard/example_certificates.js +++ b/web_service/js/tls-dashboard/example_certificates.js @@ -1,219 +1,135 @@ -var run_date = 'Fri Jun 17 2016'; +var run_date = 'Wed May 25 2016'; var cert_info = { - "1": { - "server": { - "hostname":"http-only.runtondev.com" + "1": { + "server": "www.google.com", + "subject": { + "common_name": "www.google.com", + "country_name": "US", + "organization_name": "Google Inc", + "state_or_province_name": "California", + "locality_name": "Mountain View" + }, + "issuer": { + "common_name": "Google Internet Authority G2", + "country_name": "US", + "organization_name": "Google Inc" + }, + "info": { + "days_left": 76, + "valid_from": "2016-08-10T10:46:00", + "valid_to": "2016-05-18T10:59:02" + } }, - "subject": { - "org": "Unknown", - "common_name": "The connection was refused by the remote server", - "sans": "Unknown" + "2": { + "server": "www.twitter.com", + "subject": { + "common_name": "twitter.com", + "business_category": "Private Organization", + "jurisdiction_country_name": "US", + "state_or_province_name": "California", + "organizational_unit_name": "Twitter Security", + "organization_name": "Twitter, Inc.", + "street_address": "1355 Market St", + "serial_number": "4337446", + "jurisdiction_state_or_province_name": "Delaware", + "locality_name": "San Francisco", + "postal_code": "94103", + "country_name": "US" + }, + "issuer": { + "common_name": "DigiCert SHA2 Extended Validation Server CA", + "country_name": "US", + "organization_name": "DigiCert Inc", + "organizational_unit_name": "www.digicert.com" + }, + "info": { + "days_left": 657, + "valid_from": "2018-03-14T12:00:00", + "valid_to": "2016-03-09T00:00:00" + } }, - "issuer": { - "org": "Unknown", - "common_name": "ECONNREFUSED" + "3": { + "server": "www.github.com", + "subject": { + "postal_code": "94107", + "serial_number": "5157550", + "jurisdiction_state_or_province_name": "Delaware", + "business_category": "Private Organization", + "jurisdiction_country_name": "US", + "state_or_province_name": "California", + "locality_name": "San Francisco", + "street_address": "88 Colin P Kelly, Jr Street", + "country_name": "US", + "common_name": "github.com", + "organization_name": "GitHub, Inc." + }, + "issuer": { + "common_name": "DigiCert SHA2 Extended Validation Server CA", + "country_name": "US", + "organization_name": "DigiCert Inc", + "organizational_unit_name": "www.digicert.com" + }, + "info": { + "days_left": 721, + "valid_from": "2018-05-17T12:00:00", + "valid_to": "2016-03-10T00:00:00" + } }, - "info": { - "days_left": "--", - "sort_order": 100000, - "background_class": "info" + "4": { + "server": "www.bitbucket.com", + "subject": { + "common_name": "*.bitbucket.com", + "country_name": "US", + "organization_name": "Atlassian, Inc.", + "state_or_province_name": "CA", + "locality_name": "San Francisco" + }, + "issuer": { + "common_name": "DigiCert SHA2 High Assurance Server CA", + "country_name": "US", + "organization_name": "DigiCert Inc", + "organizational_unit_name": "www.digicert.com" + }, + "info": { + "days_left": 349, + "valid_from": "2017-05-10T12:00:00", + "valid_to": "2015-04-10T00:00:00" + } + }, + "5": { + "server": "news.ycombinator.com", + "subject": { + "common_name": "*.ycombinator.com", + "organizational_unit_name": "PositiveSSL Wildcard" + }, + "issuer": { + "common_name": "COMODO RSA Domain Validation Secure Server CA", + "country_name": "GB", + "organization_name": "COMODO CA Limited", + "state_or_province_name": "Greater Manchester", + "locality_name": "Salford" + }, + "info": { + "days_left": 1183, + "valid_from": "2019-08-21T23:59:59", + "valid_to": "2014-08-22T00:00:00" + } + }, + "6": { + "server": "barnacl.es", + "subject": { + "common_name": "www.barnacl.es" + }, + "issuer": { + "common_name": "StartCom Class 1 DV Server CA", + "country_name": "IL", + "organization_name": "StartCom Ltd.", + "organizational_unit_name": "StartCom Certification Authority" + }, + "info": { + "days_left": 323, + "valid_from": "2017-04-13T20:08:11", + "valid_to": "2016-04-13T20:08:11" + } } - }, - "2": { - "server": { - "hostname":"www.google.com" - }, - "subject": { - "org": "Google Inc", - "common_name": "www.google.com", - "sans": "DNS:www.google.com" - }, - "issuer": { - "org": "Google Inc", - "common_name": "Google Internet Authority G2" - }, - "info": { - "valid_from": "2016-06-08T12:37:29.000Z", - "valid_to": "2016-08-31T12:30:00.000Z", - "days_left": 75, - "sort_order": 75, - "background_class": "success" - } - }, - "3": { - "server": { - "hostname":"expired.badssl.com" - }, - "subject": { - "org": "Unknown", - "common_name": "The certificate has expired", - "sans": "Unknown" - }, - "issuer": { - "org": "Unknown", - "common_name": "CERT_HAS_EXPIRED" - }, - "info": { - "days_left": "0", - "sort_order": 0, - "background_class": "danger" - } - }, - "4": { - "server": { - "hostname":"incomplete-chain.badssl.com" - }, - "subject": { - "org": "Unknown", - "common_name": "The server provided a self-signed certificate or the provided certificate chain was incomplete", - "sans": "Unknown" - }, - "issuer": { - "org": "Unknown", - "common_name": "UNABLE_TO_VERIFY_LEAF_SIGNATURE" - }, - "info": { - "days_left": "--", - "sort_order": 100000, - "background_class": "info" - } - }, - "5": { - "server": { - "hostname":"wrong.host.badssl.com" - }, - "subject": { - "org": "Unknown", - "common_name": "There was mismatch between the requested hostname and the certificate presented by the server", - "sans": "Unknown" - }, - "issuer": { - "org": "Unknown", - "common_name": "HOSTNAME_MISMATCH" - }, - "info": { - "days_left": "--", - "sort_order": 100000, - "background_class": "info" - } - }, - "6": { - "server": { - "hostname":"self-signed.badssl.com" - }, - "subject": { - "org": "Unknown", - "common_name": "The server provided a self-signed certificate or the provided certificate chain was incomplete", - "sans": "Unknown" - }, - "issuer": { - "org": "Unknown", - "common_name": "UNABLE_TO_VERIFY_LEAF_SIGNATURE" - }, - "info": { - "days_left": "--", - "sort_order": 100000, - "background_class": "info" - } - }, - "7": { - "server": { - "hostname":"sha256.badssl.com" - }, - "subject": { - "common_name": "*.badssl.com", - "sans": "DNS:*.badssl.com, DNS:badssl.com" - }, - "issuer": { - "org": "COMODO CA Limited", - "common_name": "COMODO RSA Domain Validation Secure Server CA" - }, - "info": { - "valid_from": "2015-04-09T00:00:00.000Z", - "valid_to": "2016-07-07T23:59:59.000Z", - "days_left": 20, - "sort_order": 20, - "background_class": "danger" - } - }, - "8": { - "server": { - "hostname":"www.twitter.com" - }, - "subject": { - "org": "Twitter, Inc.", - "common_name": "twitter.com", - "sans": "DNS:twitter.com, DNS:www.twitter.com" - }, - "issuer": { - "org": "DigiCert Inc", - "common_name": "DigiCert SHA2 Extended Validation Server CA" - }, - "info": { - "valid_from": "2016-03-09T00:00:00.000Z", - "valid_to": "2018-03-14T12:00:00.000Z", - "days_left": 635, - "sort_order": 635, - "background_class": "success" - } - }, - "9": { - "server": { - "hostname":"nonexistent.runtondev.com" - }, - "subject": { - "org": "Unknown", - "common_name": "The connection was reset by the server or timed out", - "sans": "Unknown" - }, - "issuer": { - "org": "Unknown", - "common_name": "ECONNRESET" - }, - "info": { - "days_left": "--", - "sort_order": 100000, - "background_class": "info" - } - }, - "10": { - "server": { - "hostname":"warning.runtondev.com" - }, - "subject": { - "common_name": "warning.runtondev.com", - "sans": "" - }, - "issuer": { - "org": "Madeup CA", - "common_name": "CA that doesn't exist" - }, - "info": { - "valid_from": "2015-04-09T00:00:00.000Z", - "valid_to": "2016-07-07T23:59:59.000Z", - "days_left": 45, - "sort_order": 45, - "background_class": "warning" - } - }, - "11": { - "server": { - "hostname":"danger.runtondev.com" - }, - "subject": { - "common_name": "danger.runtondev.com", - "sans": "" - }, - "issuer": { - "org": "Madeup CA", - "common_name": "CA that doesn't exist" - }, - "info": { - "valid_from": "2015-04-09T00:00:00.000Z", - "valid_to": "2016-07-07T23:59:59.000Z", - "days_left": 1, - "sort_order": 1, - "background_class": "danger" - } - }, } diff --git a/web_service/js/tls-dashboard/example_certificates.js.orig b/web_service/js/tls-dashboard/example_certificates.js.orig new file mode 100644 index 0000000..e2b21b9 --- /dev/null +++ b/web_service/js/tls-dashboard/example_certificates.js.orig @@ -0,0 +1,219 @@ +var run_date = 'Fri Jun 17 2016'; +var cert_info = { + "1": { + "server": { + "hostname":"http-only.runtondev.com" + }, + "subject": { + "org": "Unknown", + "common_name": "The connection was refused by the remote server", + "sans": "Unknown" + }, + "issuer": { + "org": "Unknown", + "common_name": "ECONNREFUSED" + }, + "info": { + "days_left": "--", + "sort_order": 100000, + "background_class": "info" + } + }, + "2": { + "server": { + "hostname":"www.google.com" + }, + "subject": { + "org": "Google Inc", + "common_name": "www.google.com", + "sans": "DNS:www.google.com" + }, + "issuer": { + "org": "Google Inc", + "common_name": "Google Internet Authority G2" + }, + "info": { + "valid_from": "2016-06-08T12:37:29.000Z", + "valid_to": "2016-08-31T12:30:00.000Z", + "days_left": 75, + "sort_order": 75, + "background_class": "success" + } + }, + "3": { + "server": { + "hostname":"expired.badssl.com" + }, + "subject": { + "org": "Unknown", + "common_name": "The certificate has expired", + "sans": "Unknown" + }, + "issuer": { + "org": "Unknown", + "common_name": "CERT_HAS_EXPIRED" + }, + "info": { + "days_left": "0", + "sort_order": 0, + "background_class": "danger" + } + }, + "4": { + "server": { + "hostname":"incomplete-chain.badssl.com" + }, + "subject": { + "org": "Unknown", + "common_name": "The server provided a self-signed certificate or the provided certificate chain was incomplete", + "sans": "Unknown" + }, + "issuer": { + "org": "Unknown", + "common_name": "UNABLE_TO_VERIFY_LEAF_SIGNATURE" + }, + "info": { + "days_left": "--", + "sort_order": 100000, + "background_class": "info" + } + }, + "5": { + "server": { + "hostname":"wrong.host.badssl.com" + }, + "subject": { + "org": "Unknown", + "common_name": "There was mismatch between the requested hostname and the certificate presented by the server", + "sans": "Unknown" + }, + "issuer": { + "org": "Unknown", + "common_name": "HOSTNAME_MISMATCH" + }, + "info": { + "days_left": "--", + "sort_order": 100000, + "background_class": "info" + } + }, + "6": { + "server": { + "hostname":"self-signed.badssl.com" + }, + "subject": { + "org": "Unknown", + "common_name": "The server provided a self-signed certificate or the provided certificate chain was incomplete", + "sans": "Unknown" + }, + "issuer": { + "org": "Unknown", + "common_name": "UNABLE_TO_VERIFY_LEAF_SIGNATURE" + }, + "info": { + "days_left": "--", + "sort_order": 100000, + "background_class": "info" + } + }, + "7": { + "server": { + "hostname":"sha256.badssl.com" + }, + "subject": { + "common_name": "*.badssl.com", + "sans": "DNS:*.badssl.com, DNS:badssl.com" + }, + "issuer": { + "org": "COMODO CA Limited", + "common_name": "COMODO RSA Domain Validation Secure Server CA" + }, + "info": { + "valid_from": "2015-04-09T00:00:00.000Z", + "valid_to": "2016-07-07T23:59:59.000Z", + "days_left": 20, + "sort_order": 20, + "background_class": "danger" + } + }, + "8": { + "server": { + "hostname":"www.twitter.com" + }, + "subject": { + "org": "Twitter, Inc.", + "common_name": "twitter.com", + "sans": "DNS:twitter.com, DNS:www.twitter.com" + }, + "issuer": { + "org": "DigiCert Inc", + "common_name": "DigiCert SHA2 Extended Validation Server CA" + }, + "info": { + "valid_from": "2016-03-09T00:00:00.000Z", + "valid_to": "2018-03-14T12:00:00.000Z", + "days_left": 635, + "sort_order": 635, + "background_class": "success" + } + }, + "9": { + "server": { + "hostname":"nonexistent.runtondev.com" + }, + "subject": { + "org": "Unknown", + "common_name": "The connection was reset by the server or timed out", + "sans": "Unknown" + }, + "issuer": { + "org": "Unknown", + "common_name": "ECONNRESET" + }, + "info": { + "days_left": "--", + "sort_order": 100000, + "background_class": "info" + } + }, + "10": { + "server": { + "hostname":"warning.runtondev.com" + }, + "subject": { + "common_name": "warning.runtondev.com", + "sans": "" + }, + "issuer": { + "org": "Madeup CA", + "common_name": "CA that doesn't exist" + }, + "info": { + "valid_from": "2015-04-09T00:00:00.000Z", + "valid_to": "2016-07-07T23:59:59.000Z", + "days_left": 45, + "sort_order": 45, + "background_class": "warning" + } + }, + "11": { + "server": { + "hostname":"danger.runtondev.com" + }, + "subject": { + "common_name": "danger.runtondev.com", + "sans": "" + }, + "issuer": { + "org": "Madeup CA", + "common_name": "CA that doesn't exist" + }, + "info": { + "valid_from": "2015-04-09T00:00:00.000Z", + "valid_to": "2016-07-07T23:59:59.000Z", + "days_left": 1, + "sort_order": 1, + "background_class": "danger" + } + }, +}