|
|
|
@ -3,6 +3,63 @@ require "minitest/reporters"
|
|
|
|
|
Minitest::Reporters.use!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
module SSLHelper
|
|
|
|
|
def check_ssl_cert_cmd(domain:, rootcert: nil, openssl: openssl_path(:system), issuer: nil, warning: 45, critical: 15, cn: nil, ocsp: true, host_cn: true)
|
|
|
|
|
# check_ssl_cert is a Nagios plugin, usable outside of Nagios
|
|
|
|
|
# cf. https://trac.id.ethz.ch/projects/nagios_plugins/wiki/check_ssl_cert
|
|
|
|
|
|
|
|
|
|
args = []
|
|
|
|
|
args.push("--host", domain)
|
|
|
|
|
args.push("--rootcert", rootcert) if rootcert
|
|
|
|
|
args.push("--openssl", openssl) if openssl
|
|
|
|
|
args.push("--issuer", issuer) if issuer
|
|
|
|
|
args.push("--warning", warning) if warning
|
|
|
|
|
args.push("--critical", critical) if critical
|
|
|
|
|
args.push("--cn", cn) if cn
|
|
|
|
|
args.push("--ocsp") if ocsp
|
|
|
|
|
args.push("--host-cn") if host_cn
|
|
|
|
|
|
|
|
|
|
"vendor/check_ssl_cert/check_ssl_cert #{args.join(" ")}"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def analyze_cmd(domain:, level: "intermediate", openssl: openssl_path(:system))
|
|
|
|
|
# Cipherscan helps audit SSL configuration
|
|
|
|
|
# cf. https://github.com/jvehent/cipherscan
|
|
|
|
|
|
|
|
|
|
args = [
|
|
|
|
|
"-o", openssl,
|
|
|
|
|
"-l", level,
|
|
|
|
|
"-t", domain
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
"vendor/cipherscan/analyze.py #{args.join(' ')}"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def openssl_verify_cmd(domain, options = "")
|
|
|
|
|
args = [
|
|
|
|
|
"-CAfile", "#{root_certificate}",
|
|
|
|
|
"-connect", "#{domain}:443",
|
|
|
|
|
options,
|
|
|
|
|
"2>&1",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
"echo QUIT | #{openssl_path} s_client #{args.join(' ')}"
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def openssl_path(variant = :system)
|
|
|
|
|
case variant
|
|
|
|
|
when :local
|
|
|
|
|
"vendor/cipherscan/openssl-darwin64"
|
|
|
|
|
else
|
|
|
|
|
`which openssl`.chop
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def root_certificate
|
|
|
|
|
"test/certs/AddTrust_External_CA_Root.pem"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
module WebserverHelper
|
|
|
|
|
|
|
|
|
|
def message_with_context(message, context = nil)
|
|
|
|
@ -91,10 +148,29 @@ module WebserverHelper
|
|
|
|
|
assert_has_last_modified page, context
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# def assert_hsts(page, context = nil)
|
|
|
|
|
# assert_status_ok page, context
|
|
|
|
|
# assert_hsts_max_age "315360000", page, context
|
|
|
|
|
# assert_public page, context
|
|
|
|
|
# refute_hsts_include_subdomains page, context
|
|
|
|
|
# end
|
|
|
|
|
|
|
|
|
|
def assert_has_header(header, page, context = nil)
|
|
|
|
|
assert page.response.key?(header), message_with_context("Expected to find '#{header}' header".freeze, context)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def assert_has_hsts(page, context = nil)
|
|
|
|
|
assert_includes page.response.keys, "strict-transport-security", message_with_context("Expected to find an Strict-Transport-Security header", context)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def assert_hsts_max_age(expected, page, context = nil)
|
|
|
|
|
assert_equal expected, hsts_max_age(page), message_with_context("Expected Strict-Transport-Security 'max-age' directive to be #{expected}", context)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def assert_hsts_include_subdomains(page, context = nil)
|
|
|
|
|
refute hsts_include_subdomains?(page), message_with_context("Expected not to find Strict-Transport-Security 'includeSubdomains' directive", context)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def assert_has_etag(page, context = nil)
|
|
|
|
|
assert_includes page.response.keys, "etag", message_with_context("Expected to find an ETag header", context)
|
|
|
|
|
end
|
|
|
|
@ -159,6 +235,25 @@ module WebserverHelper
|
|
|
|
|
header(page, "status".freeze)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def hsts_header(page)
|
|
|
|
|
header(page, "strict-transport-security".freeze)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def hsts_directives(page)
|
|
|
|
|
hsts_header(page).downcase.split(',').map(&:strip)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def hsts_include_subdomains?(page)
|
|
|
|
|
cache_control_directives(page).include?("includeSubdomains".freeze)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def hsts_max_age(page)
|
|
|
|
|
pattern = /\Amax-age\s?=\s?(\d+)\Z/
|
|
|
|
|
if found = hsts_directives(page).detect("") { |v| pattern =~ v }
|
|
|
|
|
found[pattern, 1]
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def etag_header(page)
|
|
|
|
|
header(page, "etag".freeze)
|
|
|
|
|
end
|
|
|
|
|