Tests plus faciles à manipuler
This commit is contained in:
parent
426b944b58
commit
979f3563f6
2 changed files with 121 additions and 63 deletions
|
@ -3,96 +3,59 @@ require 'mechanize'
|
|||
|
||||
class SecurityTest < Minitest::Test
|
||||
include WebserverHelper
|
||||
include SSLHelper
|
||||
|
||||
def domain
|
||||
"ssl.evolix.net".freeze
|
||||
end
|
||||
|
||||
def test_certificate_level
|
||||
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}"
|
||||
refute_match %r|consider enabling OCSP Stapling|, output, 'Expected to have OCSP stapling enabled'
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
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")
|
||||
end
|
||||
|
||||
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")
|
||||
end
|
||||
|
||||
def test_hsts_header
|
||||
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)
|
||||
end
|
||||
|
||||
def check_ssl_cert_cmd(domain)
|
||||
# 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"
|
||||
assert_status_ok page, context
|
||||
assert_has_hsts page, context
|
||||
assert_hsts_max_age "315360000", page, context
|
||||
refute_hsts_include_subdomains page, context
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue