Ensemble de scripts Ruby pour vérifier le comportement d'un frontal web (headers, redirections, certificat SSL/TLS, ...)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

304 lines
8.4 KiB

require "minitest/autorun"
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)
if context.nil? || context.empty?
message
else
message + " #{context}"
end
end
def webserver_env
webserver_env = ENV['WEBSERVER_ENV']
webserver_env = "production" if webserver_env.nil? || webserver_env.empty?
if %w(production staging development).include?(webserver_env)
webserver_env
else
fail ArgumentError, "Environnement #{webserver_env} invalide"
end
end
def domain
case webserver_env
when "production"
"www.example.com"
when "staging"
"www-staging.example.com"
when "development"
"local.example.com"
else
fail ArgumentError, "Domaine indéterminé"
end
end
def base_url
"https://#{domain}"
end
def internal_url?(url)
URI(url).host[/example.com\Z/]
end
def agent
@agent ||= Mechanize.new
end
def page_and_doc(url)
page = agent.get(url)
doc = Nokogiri::HTML(page.body)
if block_given?
yield(page, doc)
else
[page, doc]
end
end
def on_page(path = "/", &block)
uri = URI.join(base_url, path)
page, doc = page_and_doc(uri)
yield(page, doc)
end
def on_home_page(&block)
on_page("/", &block)
end
def on_pages(pages = [], &block)
pages.each do |page|
on_page(page, &block)
end
end
# Custom assertions
def assert_scheme(scheme, url, context = nil)
uri = URI.parse(url)
assert_equal scheme, uri.scheme, message_with_context("Expected scheme to be '#{scheme}' for '#{url}'", context)
end
def assert_cachable_asset(page, context = nil)
assert_status_ok page, context
assert_max_age "315360000", page, context
assert_public page, context
assert_has_etag page, context
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
def refute_has_etag(page, context = nil)
refute_includes page.response.keys, "etag", message_with_context("Expected not to find an ETag header", context)
end
def assert_has_last_modified(page, context = nil)
assert_includes page.response.keys, "last-modified", message_with_context("Expected to find a Last-Modified header", context)
end
def assert_max_age(expected, page, context = nil)
assert_equal expected, cache_max_age(page), message_with_context("Expected Cache-Control 'max-age' directive to be #{expected}", context)
end
def assert_must_revalidate(page, context = nil)
assert cache_must_revalidate?(page), message_with_context("Expected Cache-Control 'must-revalidate' directive to be found", context)
end
def refute_must_revalidate(page, context = nil)
refute cache_must_revalidate?(page), message_with_context("Expected Cache-Control 'must-revalidate' directive not to be found", context)
end
def assert_private(page, context = nil)
assert cache_private?(page), message_with_context("Expected Cache-Control directive to be private", context)
end
def assert_public(page, context = nil)
assert cache_public?(page), message_with_context("Expected Cache-Control directive to be public", context)
end
def assert_status_ok(page, context = nil)
assert_code("200", page, context)
end
def assert_status_not_modified(page, context = nil)
assert_code("304", page, context)
end
def assert_code(expected_code, page_or_code, context = nil)
actual_code = page_or_code.respond_to?(:code) ? page_or_code.code : page_or_code
assert_equal expected_code, actual_code, message_with_context("Expected HTTP status code to be #{expected_code}", context)
end
def assert_has_x_cache(page, context = nil)
assert_includes page.response.keys, "x-cache", message_with_context("Expected to find an X-Cache header", context)
end
def assert_x_cache_hit(page, context = nil)
assert_equal "HIT", x_cache_header(page), message_with_context("Expected X-Cache header to be be HIT", context)
end
def assert_x_cache_miss(page, context = nil)
assert_equal "MISS", x_cache_header(page), message_with_context("Expected X-Cache header to be be MISS", context)
end
# Helper methods
def header(page, key)
page.response[key]
end
def status_header(page)
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
def last_modified_header(page)
header(page, "last-modified".freeze)
end
def cache_control_header(page)
header(page, "cache-control".freeze)
end
def x_cache_header(page)
header(page, "x-cache".freeze)
end
def cache_private?(page)
cache_control_directives(page).include?("private")
end
def cache_public?(page)
cache_control_directives(page).include?("public")
end
def cache_no_cache?(page)
cache_control_header(page).downcase.strip == "no-cache"
end
def cache_must_revalidate?(page)
cache_control_directives(page).include?("must-revalidate")
end
def cache_has_max_age?(page)
cache_control_directives(page).any? { |v| v[/\Amax-age=\d\Z/] }
end
def cache_control_directives(page)
cache_control_header(page).downcase.split(',').map(&:strip)
end
def cache_max_age(page)
pattern = /\Amax-age\s?=\s?(\d+)\Z/
if found = cache_control_directives(page).detect("") { |v| pattern =~ v }
found[pattern, 1]
end
end
end