sensu-plugins-ssl-boutetnico 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1 -0
- data/LICENSE +22 -0
- data/README.md +78 -0
- data/bin/check-java-keystore-cert.rb +88 -0
- data/bin/check-ssl-anchor.rb +120 -0
- data/bin/check-ssl-cert.rb +130 -0
- data/bin/check-ssl-crl.rb +76 -0
- data/bin/check-ssl-host.rb +198 -0
- data/bin/check-ssl-hsts-preloadable.rb +79 -0
- data/bin/check-ssl-hsts-status.rb +101 -0
- data/bin/check-ssl-qualys.rb +193 -0
- data/bin/check-ssl-root-issuer.rb +126 -0
- data/lib/sensu-plugins-ssl.rb +1 -0
- data/lib/sensu-plugins-ssl/version.rb +9 -0
- metadata +229 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# check-ssl-crl
|
4
|
+
#
|
5
|
+
# DESCRIPTION:
|
6
|
+
# Check in minutes when a certificate revocation list will expire.
|
7
|
+
#
|
8
|
+
# OUTPUT:
|
9
|
+
# plain text
|
10
|
+
#
|
11
|
+
# PLATFORMS:
|
12
|
+
# Linux
|
13
|
+
#
|
14
|
+
# DEPENDENCIES:
|
15
|
+
# gem: sensu-plugin
|
16
|
+
#
|
17
|
+
# USAGE:
|
18
|
+
# ./check-ssl-crl -c 300 -w 600 -u /path/to/crl
|
19
|
+
# ./check-ssl-crl -c 300 -w 600 -u http://www.website.com/file.crl
|
20
|
+
#
|
21
|
+
# LICENSE:
|
22
|
+
# Stephen Hoekstra <shoekstra@schubergphilis.com>
|
23
|
+
#
|
24
|
+
# Released under the same terms as Sensu (the MIT license); see LICENSE
|
25
|
+
# for details.
|
26
|
+
#
|
27
|
+
|
28
|
+
require 'open-uri'
|
29
|
+
require 'openssl'
|
30
|
+
require 'sensu-plugin/check/cli'
|
31
|
+
require 'time'
|
32
|
+
|
33
|
+
#
|
34
|
+
# Check SSL Cert
|
35
|
+
#
|
36
|
+
class CheckSSLCRL < Sensu::Plugin::Check::CLI
|
37
|
+
option :critical,
|
38
|
+
description: 'Numbers of minutes left',
|
39
|
+
short: '-c',
|
40
|
+
long: '--critical MINUTES',
|
41
|
+
proc: proc { |v| v.to_i },
|
42
|
+
required: true
|
43
|
+
|
44
|
+
option :url,
|
45
|
+
description: 'URL (or path) to CRL file',
|
46
|
+
short: '-u',
|
47
|
+
long: '--url URL',
|
48
|
+
required: true
|
49
|
+
|
50
|
+
option :warning,
|
51
|
+
description: 'Numbers of minutes left',
|
52
|
+
short: '-w',
|
53
|
+
long: '--warning MINUTES',
|
54
|
+
proc: proc { |v| v.to_i },
|
55
|
+
required: true
|
56
|
+
|
57
|
+
def seconds_to_minutes(seconds)
|
58
|
+
(seconds / 60).to_i
|
59
|
+
end
|
60
|
+
|
61
|
+
def validate_opts
|
62
|
+
unknown 'warning cannot be less than critical' if config[:warning] < config[:critical]
|
63
|
+
end
|
64
|
+
|
65
|
+
def run
|
66
|
+
validate_opts
|
67
|
+
|
68
|
+
next_update = OpenSSL::X509::CRL.new(open(config[:url]).read).next_update
|
69
|
+
minutes_until = seconds_to_minutes(Time.parse(next_update.to_s) - Time.now)
|
70
|
+
|
71
|
+
critical "#{config[:url]} - Expired #{minutes_until.abs} minutes ago" if minutes_until.negative?
|
72
|
+
critical "#{config[:url]} - #{minutes_until} minutes left, next update at #{next_update}" if minutes_until < config[:critical].to_i
|
73
|
+
warning "#{config[:url]} - #{minutes_until} minutes left, next update at #{next_update}" if minutes_until < config[:warning].to_i
|
74
|
+
ok "#{config[:url]} - #{minutes_until} minutes left, next update at #{next_update}"
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# check-ssl-host.rb
|
4
|
+
#
|
5
|
+
# DESCRIPTION:
|
6
|
+
# SSL certificate checker
|
7
|
+
# Connects to a HTTPS (or other SSL) server and performs several checks on
|
8
|
+
# the certificate:
|
9
|
+
# - Is the hostname valid for the host we're requesting
|
10
|
+
# - If any certificate chain is presented, is it valid (i.e. is each
|
11
|
+
# certificate signed by the next)
|
12
|
+
# - Is the certificate about to expire
|
13
|
+
# Currently no checks are performed to make sure the certificate is signed
|
14
|
+
# by a trusted authority.
|
15
|
+
#
|
16
|
+
# DEPENDENCIES:
|
17
|
+
# gem: sensu-plugin
|
18
|
+
#
|
19
|
+
# USAGE:
|
20
|
+
# # Basic usage
|
21
|
+
# check-ssl-host.rb -h <hostname>
|
22
|
+
# # Specify specific days before cert expiry to alert on
|
23
|
+
# check-ssl-host.rb -h <hostmame> -c <critical_days> -w <warning_days>
|
24
|
+
# # Use -p to specify an alternate port
|
25
|
+
# check-ssl-host.rb -h <hostname> -p 8443
|
26
|
+
# # Use --skip-hostname-verification and/or --skip-chain-verification to
|
27
|
+
# # disable some of the checks made.
|
28
|
+
# check-ssl-host.rb -h <hostname> --skip-chain-verification
|
29
|
+
#
|
30
|
+
# LICENSE:
|
31
|
+
# Copyright 2014 Chef Software, Inc.
|
32
|
+
# Released under the same terms as Sensu (the MIT license); see LICENSE for
|
33
|
+
# details.
|
34
|
+
#
|
35
|
+
|
36
|
+
require 'sensu-plugin/check/cli'
|
37
|
+
require 'date'
|
38
|
+
require 'openssl'
|
39
|
+
require 'socket'
|
40
|
+
|
41
|
+
#
|
42
|
+
# Check SSL Host
|
43
|
+
#
|
44
|
+
class CheckSSLHost < Sensu::Plugin::Check::CLI
|
45
|
+
STARTTLS_PROTOS = %w[smtp imap].freeze
|
46
|
+
|
47
|
+
check_name 'check_ssl_host'
|
48
|
+
|
49
|
+
option :critical,
|
50
|
+
description: 'Return critical this many days before cert expiry',
|
51
|
+
short: '-c',
|
52
|
+
long: '--critical DAYS',
|
53
|
+
proc: proc(&:to_i),
|
54
|
+
default: 7
|
55
|
+
|
56
|
+
option :warning,
|
57
|
+
description: 'Return warning this many days before cert expiry',
|
58
|
+
short: '-w',
|
59
|
+
long: '--warning DAYS',
|
60
|
+
required: true,
|
61
|
+
proc: proc(&:to_i),
|
62
|
+
default: 14
|
63
|
+
|
64
|
+
option :host,
|
65
|
+
description: 'Hostname of the server certificate to check, by default used as the server address if none ' \
|
66
|
+
'is given',
|
67
|
+
short: '-h',
|
68
|
+
long: '--host HOST',
|
69
|
+
required: true
|
70
|
+
|
71
|
+
option :port,
|
72
|
+
description: 'Port on server to check',
|
73
|
+
short: '-p',
|
74
|
+
long: '--port PORT',
|
75
|
+
default: 443
|
76
|
+
|
77
|
+
option :address,
|
78
|
+
description: 'Address of server to check. This is used instead of the host argument for the TCP connection, ' \
|
79
|
+
'however the server hostname is still used for the TLS/SSL context.',
|
80
|
+
short: '-a',
|
81
|
+
long: '--address ADDRESS'
|
82
|
+
|
83
|
+
option :client_cert,
|
84
|
+
description: 'Path to the client certificate in DER/PEM format',
|
85
|
+
long: '--client-cert CERT'
|
86
|
+
|
87
|
+
option :client_key,
|
88
|
+
description: 'Path to the client RSA key in DER/PEM format',
|
89
|
+
long: '--client-key KEY'
|
90
|
+
|
91
|
+
option :skip_hostname_verification,
|
92
|
+
description: 'Disables hostname verification',
|
93
|
+
long: '--skip-hostname-verification',
|
94
|
+
boolean: true
|
95
|
+
|
96
|
+
option :skip_chain_verification,
|
97
|
+
description: 'Disables certificate chain verification',
|
98
|
+
long: '--skip-chain-verification',
|
99
|
+
boolean: true
|
100
|
+
|
101
|
+
option :starttls,
|
102
|
+
description: 'use STARTTLS negotiation for the given protocol '\
|
103
|
+
"(#{STARTTLS_PROTOS.join(', ')})",
|
104
|
+
long: '--starttls PROTO'
|
105
|
+
|
106
|
+
def get_cert_chain(host, port, address, client_cert, client_key)
|
107
|
+
tcp_client = TCPSocket.new(address || host, port)
|
108
|
+
handle_starttls(config[:starttls], tcp_client) if config[:starttls]
|
109
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
110
|
+
ssl_context.cert = OpenSSL::X509::Certificate.new File.read(client_cert) if client_cert
|
111
|
+
ssl_context.key = OpenSSL::PKey::RSA.new File.read(client_key) if client_key
|
112
|
+
ssl_client = OpenSSL::SSL::SSLSocket.new(tcp_client, ssl_context)
|
113
|
+
|
114
|
+
# If the OpenSSL version in use supports Server Name Indication (SNI, RFC 3546), then we set the hostname we
|
115
|
+
# received to request that certificate.
|
116
|
+
ssl_client.hostname = host if ssl_client.respond_to? :hostname=
|
117
|
+
|
118
|
+
ssl_client.connect
|
119
|
+
certs = ssl_client.peer_cert_chain
|
120
|
+
ssl_client.close
|
121
|
+
certs
|
122
|
+
end
|
123
|
+
|
124
|
+
def handle_starttls(proto, socket)
|
125
|
+
if STARTTLS_PROTOS.include?(proto) # rubocop:disable Style/GuardClause
|
126
|
+
send("starttls_#{proto}", socket)
|
127
|
+
else
|
128
|
+
raise ArgumentError, "STARTTLS supported only for #{STARTTLS_PROTOS.join(', ')}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def starttls_smtp(socket)
|
133
|
+
status = socket.readline
|
134
|
+
unless /^220 / =~ status
|
135
|
+
critical "#{config[:host]} - did not receive initial SMTP 220"
|
136
|
+
# no fall-through
|
137
|
+
end
|
138
|
+
socket.puts 'STARTTLS'
|
139
|
+
|
140
|
+
status = socket.readline
|
141
|
+
return if /^220 / =~ status
|
142
|
+
|
143
|
+
critical "#{config[:host]} - did not receive SMTP 220 in response to STARTTLS"
|
144
|
+
end
|
145
|
+
|
146
|
+
def starttls_imap(socket)
|
147
|
+
status = socket.readline
|
148
|
+
unless /^* OK / =~ status
|
149
|
+
critical "#{config[:host]} - did not receive initial * OK"
|
150
|
+
end
|
151
|
+
socket.puts 'a001 STARTTLS'
|
152
|
+
|
153
|
+
status = socket.readline
|
154
|
+
return if /^a001 OK Begin TLS negotiation now/ =~ status
|
155
|
+
|
156
|
+
critical "#{config[:host]} - did not receive OK Begin TLS negotiation now"
|
157
|
+
end
|
158
|
+
|
159
|
+
def verify_expiry(cert)
|
160
|
+
# Expiry check
|
161
|
+
days = (cert.not_after.to_date - Date.today).to_i
|
162
|
+
message = "#{config[:host]} - #{days} days until expiry"
|
163
|
+
critical "#{config[:host]} - Expired #{days} days ago" if days.negative?
|
164
|
+
critical message if days < config[:critical]
|
165
|
+
warning message if days < config[:warning]
|
166
|
+
ok message
|
167
|
+
end
|
168
|
+
|
169
|
+
def verify_certificate_chain(certs)
|
170
|
+
# Validates that a chain of certs are each signed by the next
|
171
|
+
# NOTE: doesn't validate that the top of the chain is signed by a trusted
|
172
|
+
# CA.
|
173
|
+
valid = true
|
174
|
+
parent = nil
|
175
|
+
certs.reverse_each do |c|
|
176
|
+
if parent
|
177
|
+
valid &= c.verify(parent.public_key)
|
178
|
+
end
|
179
|
+
parent = c
|
180
|
+
end
|
181
|
+
critical "#{config[:host]} - Invalid certificate chain" unless valid
|
182
|
+
end
|
183
|
+
|
184
|
+
def verify_hostname(cert)
|
185
|
+
unless OpenSSL::SSL.verify_certificate_identity(cert, config[:host]) # rubocop:disable Style/GuardClause
|
186
|
+
critical "#{config[:host]} hostname mismatch (#{cert.subject})"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def run
|
191
|
+
chain = get_cert_chain(config[:host], config[:port], config[:address], config[:client_cert], config[:client_key])
|
192
|
+
verify_hostname(chain[0]) unless config[:skip_hostname_verification]
|
193
|
+
verify_certificate_chain(chain) unless config[:skip_chain_verification]
|
194
|
+
verify_expiry(chain[0])
|
195
|
+
rescue Errno::ECONNRESET => e
|
196
|
+
critical "#{e.class} - #{e.message}"
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# check-ssl-hsts-preloadable.rb
|
4
|
+
#
|
5
|
+
# DESCRIPTION:
|
6
|
+
# Checks a domain against the chromium HSTS API returning errors/warnings if the domain is preloadable
|
7
|
+
#
|
8
|
+
# OUTPUT:
|
9
|
+
# plain text
|
10
|
+
#
|
11
|
+
# PLATFORMS:
|
12
|
+
# Linux
|
13
|
+
#
|
14
|
+
# DEPENDENCIES:
|
15
|
+
# gem: sensu-plugin
|
16
|
+
#
|
17
|
+
# USAGE:
|
18
|
+
# # Basic usage
|
19
|
+
# check-ssl-hsts-preloadable.rb -d <domain_name>
|
20
|
+
#
|
21
|
+
# LICENSE:
|
22
|
+
# Copyright 2017 Rowan Wookey <admin@rwky.net>
|
23
|
+
# Released under the same terms as Sensu (the MIT license); see LICENSE for
|
24
|
+
# details.
|
25
|
+
#
|
26
|
+
# Inspired by https://github.com/sensu-plugins/sensu-plugins-ssl/blob/master/bin/check-ssl-qualys.rb Copyright 2015 William Cooke <will@bruisyard.eu>
|
27
|
+
#
|
28
|
+
|
29
|
+
require 'sensu-plugin/check/cli'
|
30
|
+
require 'json'
|
31
|
+
require 'net/http'
|
32
|
+
|
33
|
+
class CheckSSLHSTSPreloadable < Sensu::Plugin::Check::CLI
|
34
|
+
option :domain,
|
35
|
+
description: 'The domain to run the test against',
|
36
|
+
short: '-d DOMAIN',
|
37
|
+
long: '--domain DOMAIN',
|
38
|
+
required: true
|
39
|
+
|
40
|
+
option :api_url,
|
41
|
+
description: 'The URL of the API to run against',
|
42
|
+
long: '--api-url URL',
|
43
|
+
default: 'https://hstspreload.org/api/v2/preloadable'
|
44
|
+
|
45
|
+
def fetch(uri, limit = 10)
|
46
|
+
if limit.zero?
|
47
|
+
return nil
|
48
|
+
end
|
49
|
+
|
50
|
+
response = Net::HTTP.get_response(uri)
|
51
|
+
|
52
|
+
case response
|
53
|
+
when Net::HTTPSuccess then response
|
54
|
+
when Net::HTTPRedirection then
|
55
|
+
location = URI(response['location'])
|
56
|
+
fetch(location, limit - 1)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def run
|
61
|
+
uri = URI(config[:api_url])
|
62
|
+
uri.query = URI.encode_www_form(domain: config[:domain])
|
63
|
+
response = fetch(uri)
|
64
|
+
if response.nil?
|
65
|
+
return warning 'Bad response recieved from API'
|
66
|
+
end
|
67
|
+
|
68
|
+
body = JSON.parse(response.body)
|
69
|
+
if !body['errors'].empty?
|
70
|
+
critical body['errors'].map { |u| u['summary'] }.join(', ')
|
71
|
+
elsif !body['warnings'].empty?
|
72
|
+
warning body['warnings'].map { |u| u['summary'] }.join(', ')
|
73
|
+
else
|
74
|
+
ok
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# vim: set tabstop=2 shiftwidth=2 expandtab:
|
@@ -0,0 +1,101 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# check-ssl-hsts-preload.rb
|
4
|
+
#
|
5
|
+
# DESCRIPTION:
|
6
|
+
# Checks a domain against the chromium HSTS API reporting on the preload status of the domain
|
7
|
+
#
|
8
|
+
# OUTPUT:
|
9
|
+
# plain text
|
10
|
+
#
|
11
|
+
# PLATFORMS:
|
12
|
+
# Linux
|
13
|
+
#
|
14
|
+
# DEPENDENCIES:
|
15
|
+
# gem: sensu-plugin
|
16
|
+
#
|
17
|
+
# USAGE:
|
18
|
+
# # Basic usage
|
19
|
+
# check-ssl-hsts-preload.rb -d <domain_name>
|
20
|
+
# # Specify the CRITICAL and WARNING alerts to either unknown (not in the database), pending or preloaded
|
21
|
+
# check-ssl-hsts-preload.rb -d <domain_name> -c <critical_alert> -w <warning_alert>
|
22
|
+
#
|
23
|
+
# LICENSE:
|
24
|
+
# Copyright 2017 Rowan Wookey <admin@rwky.net>
|
25
|
+
# Released under the same terms as Sensu (the MIT license); see LICENSE for
|
26
|
+
# details.
|
27
|
+
#
|
28
|
+
# Inspired by https://github.com/sensu-plugins/sensu-plugins-ssl/blob/master/bin/check-ssl-qualys.rb Copyright 2015 William Cooke <will@bruisyard.eu>
|
29
|
+
#
|
30
|
+
|
31
|
+
require 'sensu-plugin/check/cli'
|
32
|
+
require 'json'
|
33
|
+
require 'net/http'
|
34
|
+
|
35
|
+
class CheckSSLHSTSStatus < Sensu::Plugin::Check::CLI
|
36
|
+
STATUSES = %w[unknown pending preloaded].freeze
|
37
|
+
|
38
|
+
option :domain,
|
39
|
+
description: 'The domain to run the test against',
|
40
|
+
short: '-d DOMAIN',
|
41
|
+
long: '--domain DOMAIN',
|
42
|
+
required: true
|
43
|
+
|
44
|
+
option :warn,
|
45
|
+
short: '-w STATUS',
|
46
|
+
long: '--warn STATUS',
|
47
|
+
description: 'WARNING if this status or worse',
|
48
|
+
in: STATUSES,
|
49
|
+
default: 'pending'
|
50
|
+
|
51
|
+
option :critical,
|
52
|
+
short: '-c STATUS',
|
53
|
+
long: '--critical STATUS',
|
54
|
+
description: 'CRITICAL if this status or worse',
|
55
|
+
in: STATUSES,
|
56
|
+
default: 'unknown'
|
57
|
+
|
58
|
+
option :api_url,
|
59
|
+
description: 'The URL of the API to run against',
|
60
|
+
long: '--api-url URL',
|
61
|
+
default: 'https://hstspreload.org/api/v2/status'
|
62
|
+
|
63
|
+
def fetch(uri, limit = 10)
|
64
|
+
if limit.zero?
|
65
|
+
return nil
|
66
|
+
end
|
67
|
+
|
68
|
+
response = Net::HTTP.get_response(uri)
|
69
|
+
|
70
|
+
case response
|
71
|
+
when Net::HTTPSuccess then response
|
72
|
+
when Net::HTTPRedirection then
|
73
|
+
location = URI(response['location'])
|
74
|
+
fetch(location, limit - 1)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def run
|
79
|
+
uri = URI(config[:api_url])
|
80
|
+
uri.query = URI.encode_www_form(domain: config[:domain])
|
81
|
+
response = fetch(uri)
|
82
|
+
if response.nil?
|
83
|
+
return warning 'Bad response recieved from API'
|
84
|
+
end
|
85
|
+
|
86
|
+
body = JSON.parse(response.body)
|
87
|
+
unless STATUSES.include? body['status']
|
88
|
+
warning 'Invalid status returned ' + body['status']
|
89
|
+
end
|
90
|
+
|
91
|
+
if STATUSES.index(body['status']) <= STATUSES.index(config[:critical])
|
92
|
+
critical body['status']
|
93
|
+
elsif STATUSES.index(body['status']) <= STATUSES.index(config[:warn])
|
94
|
+
warning body['status']
|
95
|
+
else
|
96
|
+
ok
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# vim: set tabstop=2 shiftwidth=2 expandtab:
|