Tests plus faciles à manipuler
This commit is contained in:
parent
426b944b58
commit
979f3563f6
|
@ -3,96 +3,59 @@ require 'mechanize'
|
||||||
|
|
||||||
class SecurityTest < Minitest::Test
|
class SecurityTest < Minitest::Test
|
||||||
include WebserverHelper
|
include WebserverHelper
|
||||||
|
include SSLHelper
|
||||||
|
|
||||||
|
def domain
|
||||||
|
"ssl.evolix.net".freeze
|
||||||
|
end
|
||||||
|
|
||||||
def test_certificate_level
|
def test_certificate_level
|
||||||
level = "intermediate"
|
level = "intermediate"
|
||||||
output = `#{analyze_cmd(domain, level)}`
|
command = analyze_cmd(domain: domain, level: level)
|
||||||
|
output = `#{command}`
|
||||||
|
|
||||||
assert_match %r|has intermediate ssl/tls\nand complies with the '#{level}' level|, output, "Expected to comply with #{level} level :\n#{output.inspect}"
|
assert_match %r|has intermediate ssl/tls\nand complies with the '#{level}' level|, output, "Expected to comply with #{level} level :\n#{output.inspect}"
|
||||||
refute_match %r|consider enabling OCSP Stapling|, output, 'Expected to have OCSP stapling enabled'
|
refute_match %r|consider enabling OCSP Stapling|, output, 'Expected to have OCSP stapling enabled'
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_certificate
|
def test_certificate
|
||||||
output = `#{check_ssl_cert_cmd(domain)}`
|
options = {
|
||||||
|
domain: "ssl.evolix.net",
|
||||||
|
issuer: %Q("Let's Encrypt Authority X3"),
|
||||||
|
cn: "ssl.evolix.net",
|
||||||
|
}
|
||||||
|
command = check_ssl_cert_cmd(options)
|
||||||
|
output = `#{command}`
|
||||||
|
|
||||||
assert_match(/\ASSL_CERT OK/, output, output)
|
assert_match(/\ASSL_CERT OK/, output, output)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_accepts_tls_v1
|
def test_accepts_tls_v1
|
||||||
output = `#{openssl_verify_cmd(domain, "-tls1")}`
|
command = openssl_verify_cmd(domain, "-tls1")
|
||||||
|
output = `#{command}`
|
||||||
|
|
||||||
assert_match(/Verify return code: 0 \(ok\)/, output, "Expected to accept TLSv1")
|
assert_match(/Verify return code: 0 \(ok\)/, output, "Expected to accept TLSv1")
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_refuse_ssl_v3
|
def test_refuse_ssl_v3
|
||||||
output = `#{openssl_verify_cmd(domain, "-ssl3")}`
|
command = openssl_verify_cmd(domain, "-ssl3")
|
||||||
|
output = `#{command}`
|
||||||
|
|
||||||
assert_match(/sslv3 alert handshake failure/, output, "Expected to refuse SSLv3")
|
assert_match(/sslv3 alert handshake failure/, output, "Expected to refuse SSLv3")
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_hsts_header
|
def test_hsts_header
|
||||||
agent = Mechanize.new { |a|
|
agent = Mechanize.new { |a|
|
||||||
a.follow_redirect = false
|
a.follow_redirect = true
|
||||||
}
|
}
|
||||||
page = agent.get("https://#{domain}")
|
url = "https://#{domain}/"
|
||||||
|
page = agent.get(url)
|
||||||
|
context = "for #{url}"
|
||||||
|
|
||||||
assert_has_header("Strict-Transport-Security", page)
|
assert_status_ok page, context
|
||||||
end
|
assert_has_hsts page, context
|
||||||
|
assert_hsts_max_age "315360000", page, context
|
||||||
def check_ssl_cert_cmd(domain)
|
refute_hsts_include_subdomains page, context
|
||||||
# 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 = [
|
|
||||||
"--rootcert", root_certificate,
|
|
||||||
"--openssl", openssl_path(:system),
|
|
||||||
"--issuer", %Q("Gandi Standard SSL CA 2"),
|
|
||||||
"--warning", 60,
|
|
||||||
"--critical", 30,
|
|
||||||
"--cn", %Q("*.example.com"),
|
|
||||||
"--host-cn",
|
|
||||||
"--ocsp",
|
|
||||||
"--host", domain,
|
|
||||||
].join(" ")
|
|
||||||
|
|
||||||
"vendor/check_ssl_cert/check_ssl_cert #{args}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def analyze_cmd(domain, level = "intermediate")
|
|
||||||
# Cipherscan helps audit SSL configuration
|
|
||||||
# cf. https://github.com/jvehent/cipherscan
|
|
||||||
|
|
||||||
args = [
|
|
||||||
"-o", openssl_path(:local),
|
|
||||||
"-l", level,
|
|
||||||
"-t", domain
|
|
||||||
].join(' ')
|
|
||||||
|
|
||||||
"vendor/cipherscan/analyze.py #{args}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def openssl_verify_cmd(domain, options = "")
|
|
||||||
args = [
|
|
||||||
"-CAfile", "#{root_certificate}",
|
|
||||||
"-connect", "#{domain}:443",
|
|
||||||
options,
|
|
||||||
"2>&1",
|
|
||||||
].join(" ")
|
|
||||||
|
|
||||||
"echo QUIT | #{openssl_path} s_client #{args}"
|
|
||||||
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
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,63 @@ require "minitest/reporters"
|
||||||
Minitest::Reporters.use!
|
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
|
module WebserverHelper
|
||||||
|
|
||||||
def message_with_context(message, context = nil)
|
def message_with_context(message, context = nil)
|
||||||
|
@ -91,10 +148,29 @@ module WebserverHelper
|
||||||
assert_has_last_modified page, context
|
assert_has_last_modified page, context
|
||||||
end
|
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)
|
def assert_has_header(header, page, context = nil)
|
||||||
assert page.response.key?(header), message_with_context("Expected to find '#{header}' header".freeze, context)
|
assert page.response.key?(header), message_with_context("Expected to find '#{header}' header".freeze, context)
|
||||||
end
|
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)
|
def assert_has_etag(page, context = nil)
|
||||||
assert_includes page.response.keys, "etag", message_with_context("Expected to find an ETag header", context)
|
assert_includes page.response.keys, "etag", message_with_context("Expected to find an ETag header", context)
|
||||||
end
|
end
|
||||||
|
@ -159,6 +235,25 @@ module WebserverHelper
|
||||||
header(page, "status".freeze)
|
header(page, "status".freeze)
|
||||||
end
|
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)
|
def etag_header(page)
|
||||||
header(page, "etag".freeze)
|
header(page, "etag".freeze)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue