@@ -0,0 +1,19 @@ | |||
#!/usr/bin/env ruby | |||
$:.unshift 'lib' | |||
require 'logging' | |||
require 'cryptcheck' | |||
name = ARGV[0] || 'index' | |||
file = ::File.join 'output', "#{name}.yml" | |||
if ::File.exist? file | |||
::CryptCheck::Tls::Https.analyze_from_file "output/#{name}.yml", "output/#{name}.html" | |||
else | |||
::Logging.logger.root.appenders = ::Logging.appenders.stdout | |||
::Logging.logger.root.level = :warn | |||
server = ::CryptCheck::Tls::Https::Server.new(ARGV[0], ARGV[1] || 443) | |||
p grade = ::CryptCheck::Tls::Https::Grade.new(server) | |||
end | |||
@@ -0,0 +1,19 @@ | |||
#!/usr/bin/env ruby | |||
$:.unshift 'lib' | |||
require 'logging' | |||
require 'cryptcheck' | |||
::Logging.logger.root.appenders = ::Logging.appenders.stdout | |||
::Logging.logger.root.level = :error | |||
hosts = [] | |||
::File.open('top-1m.csv', 'r') do |file| | |||
i = 0 | |||
while line = file.gets | |||
hosts << ['Top 100 Alexa', line.strip.split(',')[1]] | |||
i += 1 | |||
break if i == 100 | |||
end | |||
end | |||
::CryptCheck::Tls::Https.analyze hosts, 'output/alexa.html' |
@@ -0,0 +1,12 @@ | |||
module CryptCheck | |||
module Tls | |||
autoload :Server, 'cryptcheck/tls/server' | |||
autoload :Grade, 'cryptcheck/tls/grade' | |||
autoload :Https, 'cryptcheck/tls/https' | |||
module Https | |||
autoload :Server, 'cryptcheck/tls/https/server' | |||
autoload :Grade, 'cryptcheck/tls/https/grade' | |||
end | |||
end | |||
end |
@@ -0,0 +1,111 @@ | |||
module CryptCheck | |||
module Tls | |||
class TlsNotSupportedGrade | |||
attr_reader :server, :score, :grade | |||
def initialize(server) | |||
@server, @score, @grade = server, -1, 'X' | |||
end | |||
end | |||
class Grade | |||
attr_reader :server, :score, :grade, :warning, :success | |||
def initialize(server) | |||
@server = server | |||
protocol_score | |||
key_exchange_score | |||
cipher_strengths_score | |||
@score = @protocol_score*0.3 + @key_exchange_score*0.3 + @cipher_strengths_score*0.4 | |||
calculate_grade | |||
warning | |||
success | |||
perfect | |||
end | |||
private | |||
def calculate_grade | |||
@grade = case @score | |||
when 0...20 then 'F' | |||
when 20...35 then 'E' | |||
when 35...50 then 'D' | |||
when 50...65 then 'C' | |||
when 65...80 then 'B' | |||
else 'A' | |||
end | |||
@grade = [@grade, 'B'].max if !@server.tlsv1_2? or @server.key_size < 2048 | |||
@grade = [@grade, 'C'].max if @server.des3? | |||
@grade = [@grade, 'E'].max if @server.rc4? or @server.des? | |||
@grade = [@grade, 'F'].max if @server.ssl? or @server.key_size < 1024 | |||
@grade = 'M' unless @server.cert_valid | |||
@grade = 'T' unless @server.cert_trusted | |||
end | |||
def warning | |||
@warning = [] | |||
@warning << :md5_sig if @server.md5_sig? | |||
@warning << :sha1_sig if @server.sha1_sig? | |||
@warning << :md5 if @server.md5? | |||
#@warning << :sha1 if @server.sha1? | |||
@warning << :rc4 if @server.rc4? | |||
@warning << :des if @server.des? | |||
@warning << :des3 if @server.des3? | |||
end | |||
def success | |||
@success = [] | |||
@success << :pfs if @server.pfs_only? | |||
end | |||
ALL_WARNING = %i(md5_sig md5 rc4 des) | |||
def all_warning | |||
ALL_WARNING | |||
end | |||
ALL_SUCCESS = %i(pfs hsts hsts_long) | |||
def all_success | |||
ALL_SUCCESS | |||
end | |||
def perfect | |||
@grade = 'A+' if @grade == 'A' and (all_warning & @warning).empty? and (all_success & @success) == all_success | |||
end | |||
METHODS_SCORES = { SSLv2: 0, SSLv3: 80, TLSv1: 90, TLSv1_1: 95, TLSv1_2: 100 } | |||
def protocol_score | |||
methods = @server.supported_methods | |||
worst, best = methods[:worst], methods[:best] | |||
@protocol_score = (METHODS_SCORES[worst] + METHODS_SCORES[best]) / 2 | |||
end | |||
def key_exchange_score | |||
@key_exchange_score = case @server.key_size | |||
when 0 then 0 | |||
when 0...512 then 20 | |||
when 512...1024 then 40 | |||
when 1024...2048 then 80 | |||
when 2048...4096 then 90 | |||
else 100 | |||
end | |||
end | |||
def cipher_strength_score(cipher_strength) | |||
case cipher_strength | |||
when 0 then 0 | |||
when 0...128 then 20 | |||
when 128...256 then 80 | |||
else 100 | |||
end | |||
end | |||
def cipher_strengths_score | |||
strength = @server.cipher_size | |||
worst, best = strength[:min], strength[:max] | |||
@cipher_strengths_score = (cipher_strength_score(worst) + cipher_strength_score(best)) / 2 | |||
end | |||
end | |||
end | |||
end |
@@ -0,0 +1,73 @@ | |||
require 'erb' | |||
require 'logging' | |||
require 'parallel' | |||
module CryptCheck | |||
module Tls | |||
module Https | |||
MAX_ANALYSIS_DURATION = 600 | |||
PARALLEL_ANALYSIS = 10 | |||
@@log = ::Logging.logger[Https] | |||
def self.grade(hostname, port=443) | |||
timeout MAX_ANALYSIS_DURATION do | |||
Grade.new Server.new hostname, port | |||
end | |||
rescue ::Exception => e | |||
@@log.error { "Error during #{hostname}:#{port} analysis : #{e}" } | |||
TlsNotSupportedGrade.new TlsNotSupportedServer.new hostname, port | |||
end | |||
def self.analyze(hosts, output, groups = nil) | |||
results = {} | |||
semaphore = ::Mutex.new | |||
::Parallel.each hosts, progress: 'Analysing', in_threads: PARALLEL_ANALYSIS, | |||
finish: lambda { |item, _, _| puts item[1] } do |description, host| | |||
result = grade host.strip | |||
semaphore.synchronize do | |||
if results.include? description | |||
results[description] << result | |||
else | |||
results[description] = [result] | |||
end | |||
end | |||
end | |||
results = ::Hash[groups.collect { |g| [g, results[g]] }] if groups | |||
results.each do |d, _| | |||
results[d].sort! do |a, b| | |||
cmp = score(a) <=> score(b) | |||
if cmp == 0 | |||
cmp = b.score <=> a.score | |||
if cmp == 0 | |||
cmp = a.server.hostname <=> b.server.hostname | |||
end | |||
end | |||
cmp | |||
end | |||
end | |||
::File.write output, ::ERB.new(::File.read('output/https.erb')).result(binding) | |||
end | |||
def self.analyze_from_file(file, output) | |||
config = ::YAML.load_file file | |||
hosts = [] | |||
groups = [] | |||
config.each do |c| | |||
d, hs = c['description'], c['hostnames'] | |||
groups << d | |||
hs.each { |host| hosts << [d, host] } | |||
end | |||
self.analyze hosts, output, groups | |||
end | |||
private | |||
SCORES = %w(A+ A A- B C D E F T M X) | |||
def self.score(a) | |||
SCORES.index a.grade | |||
end | |||
end | |||
end | |||
end |
@@ -0,0 +1,18 @@ | |||
module CryptCheck | |||
module Tls | |||
module Https | |||
class Grade < Tls::Grade | |||
private | |||
def success | |||
super | |||
@success << :hsts if @server.hsts? | |||
@success << :hsts_long if @server.hsts_long? | |||
end | |||
def all_success | |||
super + %i(hsts hsts_long) | |||
end | |||
end | |||
end | |||
end | |||
end |
@@ -0,0 +1,54 @@ | |||
require 'socket' | |||
require 'openssl' | |||
require 'httparty' | |||
module CryptCheck | |||
module Tls | |||
module Https | |||
class Server < Tls::Server | |||
attr_reader :hsts | |||
def initialize(hostname, port=443, methods: EXISTING_METHODS) | |||
super | |||
fetch_hsts | |||
end | |||
def fetch_hsts | |||
port = @port == 443 ? '' : ":#{@port}" | |||
response = nil | |||
@methods.each do |method| | |||
begin | |||
next unless SUPPORTED_METHODS.include? method | |||
@log.debug { "Check HSTS with #{method}" } | |||
response = ::HTTParty.head "https://#{@hostname}#{port}/", { follow_redirects: false, verify: false, ssl_version: method, timeout: SSL_TIMEOUT } | |||
break | |||
rescue Exception => e | |||
@log.debug { "#{method} not supported : #{e}" } | |||
end | |||
end | |||
if response and header = response.headers['strict-transport-security'] | |||
name, value = header.split '=' | |||
if name == 'max-age' | |||
@hsts = value.to_i | |||
@log.info { "HSTS : #{@hsts}" } | |||
return | |||
end | |||
end | |||
@log.info { 'No HSTS' } | |||
@hsts = nil | |||
end | |||
def hsts? | |||
!@hsts.nil? | |||
end | |||
def hsts_long? | |||
hsts? and @hsts >= 6*30*24*60*60 | |||
end | |||
end | |||
end | |||
end | |||
end |
@@ -0,0 +1,309 @@ | |||
require 'socket' | |||
require 'openssl' | |||
require 'httparty' | |||
module CryptCheck | |||
module Tls | |||
class TlsNotSupportedServer | |||
attr_reader :hostname, :port | |||
def initialize(hostname, port) | |||
@hostname, @port = hostname, port | |||
end | |||
end | |||
class Server | |||
TCP_TIMEOUT = 10 | |||
SSL_TIMEOUT = 2*TCP_TIMEOUT | |||
EXISTING_METHODS = %i(TLSv1_2 TLSv1_1 TLSv1 SSLv3 SSLv2) | |||
SUPPORTED_METHODS = ::OpenSSL::SSL::SSLContext::METHODS | |||
class TLSException < ::Exception | |||
end | |||
class TLSNotAvailableException < TLSException | |||
end | |||
class CipherNotAvailable < TLSException | |||
end | |||
class Timeout < TLSException | |||
end | |||
class TLSTimeout < TLSException | |||
end | |||
class ConnectionError < TLSException | |||
end | |||
attr_reader :hostname, :port, :prefered_ciphers, :cert, :cert_valid, :cert_trusted | |||
def initialize(hostname, port, methods: EXISTING_METHODS) | |||
@log = Logging.logger[hostname] | |||
@hostname = hostname | |||
@port = port | |||
@methods = methods | |||
@log.error { "Begin analysis" } | |||
extract_cert | |||
fetch_prefered_ciphers | |||
check_supported_cipher | |||
fetch_hsts | |||
@log.error { "End analysis" } | |||
end | |||
def supported_methods | |||
worst = EXISTING_METHODS.find { |method| !@prefered_ciphers[method].nil? } | |||
best = EXISTING_METHODS.reverse.find { |method| !@prefered_ciphers[method].nil? } | |||
{ worst: worst, best: best } | |||
end | |||
def key | |||
key = @cert.public_key | |||
case key | |||
when ::OpenSSL::PKey::RSA then | |||
[:rsa, key.n.num_bits] | |||
when ::OpenSSL::PKey::DSA then | |||
[:dsa, key.p.num_bits] | |||
when ::OpenSSL::PKey::EC then | |||
[:ecc, key.group.degree] | |||
end | |||
end | |||
def key_size | |||
type, size = self.key | |||
if type == :ecc | |||
size = case size | |||
when 160 then | |||
1024 | |||
when 224 then | |||
2048 | |||
when 256 then | |||
3072 | |||
when 384 then | |||
7680 | |||
when 521 then | |||
15360 | |||
end | |||
end | |||
size | |||
end | |||
def cipher_size | |||
cipher_strengths = supported_ciphers.collect { |c| c[2] }.uniq.sort | |||
worst, best = cipher_strengths.first, cipher_strengths.last | |||
{ worst: worst, best: best } | |||
end | |||
EXISTING_METHODS.each do |method| | |||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 | |||
def #{method.to_s.downcase}? | |||
!prefered_ciphers[:#{method}].nil? | |||
end | |||
RUBY_EVAL | |||
end | |||
{ | |||
md2: %w(md2WithRSAEncryption), | |||
md5: %w(md5WithRSAEncryption md5WithRSA), | |||
sha1: %w(sha1WithRSAEncryption sha1WithRSA dsaWithSHA1 dsaWithSHA1_2 ecdsa_with_SHA1) | |||
}.each do |name, signature| | |||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 | |||
def #{name}_sig? | |||
#{signature}.include? @cert.signature_algorithm | |||
end | |||
RUBY_EVAL | |||
end | |||
{ | |||
md5: %w(MD5), | |||
sha1: %w(SHA), | |||
rc4: %w(RC4), | |||
des3: %w(3DES DES-CBC3), | |||
des: %w(DES-CBC) | |||
}.each do |name, ciphers| | |||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 | |||
def #{name}? | |||
supported_ciphers.any? { |supported| #{ciphers}.any? { |available| /(^|-)#\{available\}(-|$)/ =~ supported[0] } } | |||
end | |||
RUBY_EVAL | |||
end | |||
def ssl? | |||
sslv2? or sslv3? | |||
end | |||
def tls? | |||
tlsv1? or tlsv1_1? or tlsv1_2? | |||
end | |||
def tls_only? | |||
tls? and !ssl? | |||
end | |||
PFS_CIPHERS = [/^DHE-RSA-/, /^DHE-DSS-/, /^ECDHE-RSA-/, /^ECDHE-ECDSA-/] | |||
def pfs? | |||
supported_ciphers.any? { |cipher| PFS_CIPHERS.any? { |pc| pc =~ cipher[0] } } | |||
end | |||
def pfs_only? | |||
supported_ciphers.all? { |cipher| PFS_CIPHERS.any? { |pc| pc =~ cipher[0] } } | |||
end | |||
def supported_ciphers | |||
@supported_ciphers.values.flatten(1).uniq | |||
end | |||
def supported_ciphers_by_method | |||
@supported_ciphers | |||
end | |||
private | |||
def connect(family, host, port, &block) | |||
socket = ::Socket.new family, ::Socket::SOCK_STREAM | |||
sockaddr = ::Socket.sockaddr_in port, host | |||
@log.debug { "Connecting to #{host}:#{port}" } | |||
begin | |||
status = socket.connect_nonblock sockaddr | |||
@log.debug { "Connecting to #{host}:#{port} status : #{status}" } | |||
raise ConnectionError, status unless status == 0 | |||
@log.debug { "Connected to #{host}:#{port}" } | |||
block_given? ? block.call(socket) : nil | |||
rescue ::IO::WaitReadable | |||
@log.debug { "Waiting for read to #{host}:#{port}" } | |||
raise Timeout unless IO.select [socket], nil, nil, TCP_TIMEOUT | |||
retry | |||
rescue ::IO::WaitWritable | |||
@log.debug { "Waiting for write to #{host}:#{port}" } | |||
raise Timeout unless IO.select nil, [socket], nil, TCP_TIMEOUT | |||
retry | |||
ensure | |||
socket.close | |||
end | |||
end | |||
def ssl_connect(socket, context, method, &block) | |||
ssl_socket = ::OpenSSL::SSL::SSLSocket.new socket, context | |||
ssl_socket.hostname = @hostname unless method == :SSLv2 | |||
@log.debug { "SSL connecting to #{@hostname}:#{@port}" } | |||
begin | |||
ssl_socket.connect_nonblock | |||
@log.debug { "SSL connected to #{@hostname}:#{@port}" } | |||
return block_given? ? block.call(ssl_socket) : nil | |||
rescue ::IO::WaitReadable | |||
@log.debug { "Waiting for SSL read to #{@hostname}:#{@port}" } | |||
raise TLSTimeout unless IO.select [socket], nil, nil, SSL_TIMEOUT | |||
retry | |||
rescue ::IO::WaitWritable | |||
@log.debug { "Waiting for SSL write to #{@hostname}:#{@port}" } | |||
raise TLSTimeout unless IO.select nil, [socket], nil, SSL_TIMEOUT | |||
retry | |||
rescue ::OpenSSL::SSL::SSLError => e | |||
raise TLSException, e | |||
ensure | |||
ssl_socket.close | |||
end | |||
end | |||
def ssl_client(method, ciphers = nil, &block) | |||
ssl_context = ::OpenSSL::SSL::SSLContext.new method | |||
ssl_context.ciphers = ciphers if ciphers | |||
@log.debug { "Try #{method} connection with #{ciphers}" } | |||
[::Socket::AF_INET, ::Socket::AF_INET6].each do |family| | |||
@log.debug { "Try connection for family #{family}" } | |||
addrs = begin | |||
::Socket.getaddrinfo @hostname, nil, family, :STREAM | |||
rescue ::SocketError => e | |||
@log.debug { "Unable to resolv #{@hostname} : #{e}" } | |||
next | |||
end | |||
addrs.each do |addr| | |||
connect family, addr[3], @port do |socket| | |||
ssl_connect socket, ssl_context, method do |ssl_socket| | |||
return block_given? ? block.call(ssl_socket) : nil | |||
end | |||
end | |||
end | |||
end | |||
@log.debug { "No SSL available on #{@hostname}" } | |||
raise CipherNotAvailable | |||
end | |||
def extract_cert | |||
@methods.each do |method| | |||
next unless SUPPORTED_METHODS.include? method | |||
begin | |||
@cert, @chain = ssl_client(method) { |s| [s.peer_cert, s.peer_cert_chain] } | |||
@log.warn { "Certificate #{@cert.subject}" } | |||
break | |||
rescue TLSException => e | |||
@log.info { "Method #{method} not supported : #{e}" } | |||
end | |||
end | |||
raise TLSNotAvailableException unless @cert | |||
@cert_valid = ::OpenSSL::SSL.verify_certificate_identity @cert, @hostname | |||
@cert_trusted = verify_trust @chain, @cert | |||
end | |||
def prefered_cipher(method) | |||
cipher = ssl_client(method, %w(ALL:COMPLEMENTOFALL)) { |s| s.cipher } | |||
@log.warn { "Prefered cipher for #{method} : #{cipher[0]}" } | |||
cipher | |||
rescue Exception => e | |||
@log.info { "Method #{method} not supported : #{e}" } | |||
nil | |||
end | |||
def fetch_prefered_ciphers | |||
@prefered_ciphers = {} | |||
@methods.each do |method| | |||
next unless SUPPORTED_METHODS.include? method | |||
@prefered_ciphers[method] = prefered_cipher method | |||
end | |||
raise TLSNotAvailableException.new unless @prefered_ciphers.any? { |_, c| !c.nil? } | |||
end | |||
def available_ciphers(method) | |||
::OpenSSL::SSL::SSLContext.new(method).ciphers | |||
end | |||
def supported_cipher?(method, cipher) | |||
ssl_client method, [cipher] | |||
@log.warn { "Verify #{method} / #{cipher[0]} : OK" } | |||
true | |||
rescue TLSException => e | |||
@log.info { "Verify #{method} / #{cipher[0]} : NOK (#{e})" } | |||
false | |||
end | |||
def check_supported_cipher | |||
@supported_ciphers = {} | |||
@methods.each do |method| | |||
next unless SUPPORTED_METHODS.include? method and @prefered_ciphers[method] | |||
@supported_ciphers[method] = available_ciphers(method).select { |cipher| supported_cipher? method, cipher } | |||
end | |||
end | |||
def verify_trust(chain, cert) | |||
store = ::OpenSSL::X509::Store.new | |||
store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT | |||
%w(mozilla cacert).each do |directory| | |||
::Dir.glob(::File.join '/usr/share/ca-certificates', directory, '*').each do |file| | |||
::File.open file, 'r' do |file| | |||
cert = ::OpenSSL::X509::Certificate.new file.read | |||
begin | |||
store.add_cert cert | |||
rescue ::OpenSSL::X509::StoreError | |||
end | |||
end | |||
end | |||
end | |||
chain.each do |cert| | |||
begin | |||
store.add_cert cert | |||
rescue ::OpenSSL::X509::StoreError | |||
end | |||
end | |||
store.verify cert | |||
end | |||
end | |||
end | |||
end |
@@ -1,77 +0,0 @@ | |||
require 'erb' | |||
require 'logging' | |||
require 'parallel' | |||
require 'thread' | |||
require 'yaml' | |||
module SSLCheck | |||
module SSLLabs | |||
autoload :API, 'sslcheck/ssllabs/api' | |||
end | |||
autoload :Server, 'sslcheck/server' | |||
autoload :Grade, 'sslcheck/grade' | |||
PARALLEL_ANALYSIS = 20 | |||
SYN_TIMEOUT = 600 | |||
@@log = Logging.logger[SSLCheck] | |||
def self.grade(hostname, port=443) | |||
timeout SYN_TIMEOUT do | |||
Grade.new Server.new hostname, port | |||
end | |||
rescue Exception => e | |||
@@log.error { "Error during #{hostname}:#{port} analysis : #{e}" } | |||
NoSslTlsGrade.new NoSslTlsServer.new hostname, port | |||
end | |||
def self.analyze(hosts, output, groups = nil) | |||
results = {} | |||
semaphore = Mutex.new | |||
Parallel.each hosts, progress: 'Analysing', in_threads: PARALLEL_ANALYSIS, | |||
finish: lambda { |item, _, _| puts item[1] } do |description, host| | |||
result = SSLCheck.grade host.strip | |||
semaphore.synchronize do | |||
if results.include? description | |||
results[description] << result | |||
else | |||
results[description] = [result] | |||
end | |||
end | |||
end | |||
results = Hash[groups.collect { |g| [g, results[g]] }] if groups | |||
results.each do |d, _| | |||
results[d].sort! do |a, b| | |||
cmp = score(a) <=> score(b) | |||
if cmp == 0 | |||
cmp = a.score <=> b.score | |||
if cmp == 0 | |||
cmp = a.server.hostname <=> b.server.hostname | |||
end | |||
end | |||
cmp | |||
end | |||
end | |||
File.write output, ERB.new(File.read('output/sslcheck.erb')).result(binding) | |||
end | |||
def self.analyze_from_file(file, output) | |||
config = YAML.load_file file | |||
hosts = [] | |||
groups = [] | |||
config.each do |c| | |||
d, hs = c['description'], c['hostnames'] | |||
groups << d | |||
hs.each { |host| hosts << [d, host] } | |||
end | |||
self.analyze hosts, output, groups | |||
end | |||
private | |||
SCORES = %w(A+ A A- B C D E F T M X) | |||
def self.score(a) | |||
SCORES.index a.grade | |||
end | |||
end |
@@ -1,107 +0,0 @@ | |||
require 'timeout' | |||
module SSLCheck | |||
class NoSslTlsGrade | |||
attr_reader :server, :score, :grade | |||
def initialize(server) | |||
@server, @score, @grade = server, -1, 'X' | |||
end | |||
end | |||
class Grade | |||
attr_reader :server, :score, :grade, :warning, :success | |||
def initialize(server) | |||
@server = server | |||
protocol_score | |||
key_exchange_score | |||
cipher_strengths_score | |||
@score = @protocol_score*0.3 + @key_exchange_score*0.3 + @cipher_strengths_score*0.4 | |||
calculate_grade | |||
warning | |||
success | |||
perfect | |||
end | |||
private | |||
def calculate_grade | |||
@grade = case @score | |||
when 0...20 then 'F' | |||
when 20...35 then 'E' | |||
when 35...50 then 'D' | |||
when 50...65 then 'C' | |||
when 65...80 then 'B' | |||
else 'A' | |||
end | |||
@grade = [@grade, 'B'].max if !@server.tlsv1_2? or @server.key_size < 2048 | |||
@grade = [@grade, 'C'].max if @server.des3? | |||
@grade = [@grade, 'E'].max if @server.rc4? or @server.des? | |||
@grade = [@grade, 'F'].max if @server.ssl? or @server.key_size < 1024 | |||
@grade = 'M' unless @server.cert_valid | |||
@grade = 'T' unless @server.cert_trusted | |||
end | |||
def warning | |||
@warning = [] | |||
@warning << :md5_sig if @server.md5_sig? | |||
@warning << :sha1_sig if @server.sha1_sig? | |||
@warning << :md5 if @server.md5? | |||
#@warning << :sha1 if @server.sha1? | |||
@warning << :rc4 if @server.rc4? | |||
@warning << :des if @server.des? | |||
@warning << :des3 if @server.des3? | |||
end | |||
def success | |||
@success = [] | |||
@success << :pfs if @server.pfs_only? | |||
@success << :hsts if @server.hsts? | |||
@success << :hsts_long if @server.hsts_long? | |||
end | |||
ALL_WARNING = %i(md5_sig md5 rc4 des) | |||
ALL_SUCCESS = %i(pfs hsts hsts_long) | |||
def perfect | |||
@grade = 'A+' if @grade == 'A' and (ALL_WARNING & @warning).empty? and (ALL_SUCCESS & @success) == ALL_SUCCESS | |||
end | |||
METHODS_SCORES = { SSLv2: 0, SSLv3: 80, TLSv1: 90, TLSv1_1: 95, TLSv1_2: 100 } | |||
def protocol_score | |||
methods = @server.supported_methods | |||
worst, best = methods[:worst], methods[:best] | |||
@protocol_score = (METHODS_SCORES[worst] + METHODS_SCORES[best]) / 2 | |||
end | |||
def key_exchange_score | |||
@key_exchange_score = case @server.key_size | |||
when 0 then 0 | |||
when 0...512 then 20 | |||
when 512...1024 then 40 | |||
when 1024...2048 then 80 | |||
when 2048...4096 then 90 | |||
else 100 | |||
end | |||
end | |||
def cipher_strength_score(cipher_strength) | |||
case cipher_strength | |||
when 0 then 0 | |||
when 0...128 then 20 | |||
when 128...256 then 80 | |||
else 100 | |||
end | |||
end | |||
def cipher_strengths_score | |||
strength = @server.cipher_size | |||
worst, best = strength[:min], strength[:max] | |||
@cipher_strengths_score = (cipher_strength_score(worst) + cipher_strength_score(best)) / 2 | |||
end | |||
end | |||
end |
@@ -1,334 +0,0 @@ | |||
require 'socket' | |||
require 'openssl' | |||
require 'httparty' | |||
require 'parallel' | |||
require 'tcp_timeout' | |||
module SSLCheck | |||
class NoSslTlsServer | |||
attr_reader :hostname, :port | |||
def initialize(hostname, port=443) | |||
@hostname, @port = hostname, port | |||
end | |||
end | |||
class Server | |||
TCP_TIMEOUT = 10 | |||
SSL_TIMEOUT = 2*TCP_TIMEOUT | |||
EXISTING_METHODS = %i(TLSv1_2 TLSv1_1 TLSv1 SSLv3 SSLv2) | |||
SUPPORTED_METHODS = OpenSSL::SSL::SSLContext::METHODS | |||
class TLSException < Exception; end | |||
class TLSNotAvailableException < TLSException; end | |||
class CipherNotAvailable < TLSException; end | |||
class Timeout < TLSException; end | |||
class TLSTimeout < TLSException; end | |||
class ConnectionError < TLSException; end | |||
attr_reader :hostname, :port, :prefered_ciphers, :cert, :hsts, :cert_valid, :cert_trusted | |||
def initialize(hostname, port=443, methods: EXISTING_METHODS) | |||
@log = Logging.logger[hostname] | |||
@hostname = hostname | |||
@port = port | |||
@methods = methods | |||
@log.error { "Begin analysis" } | |||
extract_cert | |||
fetch_prefered_ciphers | |||
check_supported_cipher | |||
fetch_hsts | |||
@log.error { "End analysis" } | |||
end | |||
def supported_methods | |||
worst = EXISTING_METHODS.find { |method| !@prefered_ciphers[method].nil? } | |||
best = EXISTING_METHODS.reverse.find { |method| !@prefered_ciphers[method].nil? } | |||
{worst: worst, best: best} | |||
end | |||
def key | |||
key = @cert.public_key | |||
case key | |||
when OpenSSL::PKey::RSA then | |||
[:rsa, key.n.num_bits] | |||
when OpenSSL::PKey::DSA then | |||
[:dsa, key.p.num_bits] | |||
when OpenSSL::PKey::EC then | |||
[:ecc, key.group.degree] | |||
end | |||
end | |||
def key_size | |||
type, size = self.key | |||
if type == :ecc | |||
size = case size | |||
when 160 then 1024 | |||
when 224 then 2048 | |||
when 256 then 3072 | |||
when 384 then 7680 | |||
when 521 then 15360 | |||
end | |||
end | |||
size | |||
end | |||
def cipher_size | |||
cipher_strengths = supported_ciphers.collect { |c| c[2] }.uniq.sort | |||
worst, best = cipher_strengths.first, cipher_strengths.last | |||
{worst: worst, best: best} | |||
end | |||
EXISTING_METHODS.each do |method| | |||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 | |||
def #{method.to_s.downcase}? | |||
!prefered_ciphers[:#{method}].nil? | |||
end | |||
RUBY_EVAL | |||
end | |||
{ | |||
md2: %w(md2WithRSAEncryption), | |||
md5: %w(md5WithRSAEncryption md5WithRSA), | |||
sha1: %w(sha1WithRSAEncryption sha1WithRSA dsaWithSHA1 dsaWithSHA1_2 ecdsa_with_SHA1) | |||
}.each do |name, signature| | |||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 | |||
def #{name}_sig? | |||
#{signature}.include? @cert.signature_algorithm | |||
end | |||
RUBY_EVAL | |||
end | |||
{ | |||
md5: %w(MD5), | |||
sha1: %w(SHA), | |||
rc4: %w(RC4), | |||
des3: %w(3DES DES-CBC3), | |||
des: %w(DES-CBC) | |||
}.each do |name, ciphers| | |||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 | |||
def #{name}? | |||
supported_ciphers.any? { |supported| #{ciphers}.any? { |available| /(^|-)#\{available\}(-|$)/ =~ supported[0] } } | |||
end | |||
RUBY_EVAL | |||
end | |||
def ssl? | |||
sslv2? or sslv3? | |||
end | |||
def tls? | |||
tlsv1? or tlsv1_1? or tlsv1_2? | |||
end | |||
def tls_only? | |||
tls? and !ssl? | |||
end | |||
PFS_CIPHERS = [/^DHE-RSA-/, /^DHE-DSS-/, /^ECDHE-RSA-/, /^ECDHE-ECDSA-/] | |||
def pfs? | |||
supported_ciphers.any? { |cipher| PFS_CIPHERS.any? { |pc| pc =~ cipher[0] } } | |||
end | |||
def pfs_only? | |||
supported_ciphers.all? { |cipher| PFS_CIPHERS.any? { |pc| pc =~ cipher[0] } } | |||
end | |||
def supported_ciphers | |||
@supported_ciphers.values.flatten(1).uniq | |||
end | |||
def supported_ciphers_by_method | |||
@supported_ciphers | |||
end | |||
def hsts? | |||
!@hsts.nil? | |||
end | |||
def hsts_long? | |||
hsts? and @hsts >= 6*30*24*60*60 | |||
end | |||
private | |||
def connect(family, host, port, &block) | |||
socket = Socket.new family, Socket::SOCK_STREAM | |||
sockaddr = Socket.sockaddr_in port, host | |||
@log.debug { "Connecting to #{host}:#{port}" } | |||
begin | |||
status = socket.connect_nonblock sockaddr | |||
@log.debug { "Connecting to #{host}:#{port} status : #{status}" } | |||
raise ConnectionError.new status unless status == 0 | |||
@log.debug { "Connected to #{host}:#{port}" } | |||
block_given? ? block.call(socket) : nil | |||
rescue IO::WaitReadable | |||
@log.debug { "Waiting for read to #{host}:#{port}" } | |||
raise Timeout.new unless IO.select [socket], nil, nil, TCP_TIMEOUT | |||
retry | |||
rescue IO::WaitWritable | |||
@log.debug { "Waiting for write to #{host}:#{port}" } | |||
raise Timeout.new unless IO.select nil, [socket], nil, TCP_TIMEOUT | |||
retry | |||
ensure | |||
socket.close | |||
end | |||
end | |||
def ssl_connect(socket, context, method, &block) | |||
ssl_socket = OpenSSL::SSL::SSLSocket.new socket, context | |||
ssl_socket.hostname = @hostname unless method == :SSLv2 | |||
@log.debug { "SSL connecting to #{@hostname}:#{@port}" } | |||
begin | |||
ssl_socket.connect_nonblock | |||
@log.debug { "SSL connected to #{@hostname}:#{@port}" } | |||
return block_given? ? block.call(ssl_socket) : nil | |||
rescue IO::WaitReadable | |||
@log.debug { "Waiting for SSL read to #{@hostname}:#{@port}" } | |||
raise TLSTimeout.new unless IO.select [socket], nil, nil, SSL_TIMEOUT | |||
retry | |||
rescue IO::WaitWritable | |||
@log.debug { "Waiting for SSL write to #{@hostname}:#{@port}" } | |||
raise TLSTimeout.new unless IO.select nil, [socket], nil, SSL_TIMEOUT | |||
retry | |||
rescue OpenSSL::SSL::SSLError => e | |||
raise TLSException, e | |||
ensure | |||
ssl_socket.close | |||
end | |||
end | |||
def ssl_client(method, ciphers = nil, &block) | |||
ssl_context = OpenSSL::SSL::SSLContext.new method | |||
ssl_context.ciphers = ciphers if ciphers | |||
@log.debug { "Try #{method} connection with #{ciphers}" } | |||
[Socket::AF_INET, Socket::AF_INET6].each do |family| | |||
@log.debug { "Try connection for family #{family}" } | |||
addrs = begin | |||
Socket.getaddrinfo @hostname, nil, family, :STREAM | |||
rescue SocketError => e | |||
@log.debug { "Unable to resolv #{@hostname} : #{e}" } | |||
next | |||
end | |||
addrs.each do |addr| | |||
connect family, addr[3], @port do |socket| | |||
ssl_connect socket, ssl_context, method do |ssl_socket| | |||
return block_given? ? block.call(ssl_socket) : nil | |||
end | |||
end | |||
end | |||
end | |||
@log.debug { "No SSL available on #{@hostname}" } | |||
raise CipherNotAvailable.new | |||
end | |||
def extract_cert | |||
@methods.each do |method| | |||
next unless SUPPORTED_METHODS.include? method | |||
begin | |||
@cert, @chain = ssl_client(method) { |s| [s.peer_cert, s.peer_cert_chain] } | |||
@log.warn { "Certificate #{@cert.subject}" } | |||
break | |||
rescue TLSException => e | |||
@log.info { "Method #{method} not supported : #{e}" } | |||
end | |||
end | |||
raise TLSNotAvailableException.new unless @cert | |||
@cert_valid = OpenSSL::SSL.verify_certificate_identity @cert, @hostname | |||
@cert_trusted = verify_trust @chain, @cert | |||
end | |||
def prefered_cipher(method) | |||
cipher = ssl_client(method, %w(ALL:COMPLEMENTOFALL)) { |s| s.cipher } | |||
@log.warn { "Prefered cipher for #{method} : #{cipher[0]}" } | |||
cipher | |||
rescue Exception => e | |||
@log.info { "Method #{method} not supported : #{e}" } | |||
nil | |||
end | |||
def fetch_prefered_ciphers | |||
@prefered_ciphers = {} | |||
@methods.each do |method| | |||
next unless SUPPORTED_METHODS.include? method | |||
@prefered_ciphers[method] = prefered_cipher method | |||
end | |||
raise TLSNotAvailableException.new unless @prefered_ciphers.any? { |_, c| !c.nil? } | |||
end | |||
def available_ciphers(method) | |||
OpenSSL::SSL::SSLContext.new(method).ciphers | |||
end | |||
def supported_cipher?(method, cipher) | |||
ssl_client method, [cipher] | |||
@log.warn { "Verify #{method} / #{cipher[0]} : OK" } | |||
true | |||
rescue TLSException => e | |||
@log.info { "Verify #{method} / #{cipher[0]} : NOK (#{e})" } | |||
false | |||
end | |||
def check_supported_cipher | |||
@supported_ciphers = {} | |||
@methods.each do |method| | |||
next unless SUPPORTED_METHODS.include? method and @prefered_ciphers[method] | |||
@supported_ciphers[method] = available_ciphers(method).select { |cipher| supported_cipher? method, cipher } | |||
end | |||
end | |||
def fetch_hsts | |||
port = @port == 443 ? '' : ":#{@port}" | |||
response = nil | |||
@methods.each do |method| | |||
begin | |||
next unless SUPPORTED_METHODS.include? method | |||
@log.debug { "Check HSTS with #{method}" } | |||
response = HTTParty.head "https://#{@hostname}#{port}/", {follow_redirects: false, verify: false, ssl_version: method, timeout: SSL_TIMEOUT} | |||
break | |||
rescue Exception => e | |||
@log.debug { "#{method} not supported : #{e}" } | |||
end | |||
end | |||
if response and header = response.headers['strict-transport-security'] | |||
name, value = header.split '=' | |||
if name == 'max-age' | |||
@hsts = value.to_i | |||
@log.info { "HSTS : #{@hsts}" } | |||
return | |||
end | |||
end | |||
@log.info { 'No HSTS' } | |||
@hsts = nil | |||
end | |||
def verify_trust(chain, cert) | |||
store = OpenSSL::X509::Store.new | |||
#store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT | |||
%w(mozilla cacert).each do |directory| | |||
Dir.glob(File.join '/usr/share/ca-certificates', directory, '*').each do |file| | |||
File.open file, 'r' do |file| | |||
cert = OpenSSL::X509::Certificate.new file.read | |||
begin | |||
store.add_cert cert | |||
rescue OpenSSL::X509::StoreError | |||
end | |||
end | |||
end | |||
end | |||
chain.each do |cert| | |||
begin | |||
store.add_cert cert | |||
rescue OpenSSL::X509::StoreError | |||
end | |||
end | |||
store.verify cert | |||
end | |||
end | |||
end |
@@ -1,148 +0,0 @@ | |||
require 'httparty' | |||
require 'nokogiri' | |||
module SSLCheck | |||
module SSLLabs | |||
class Error < StandardError; end | |||
class WaitingError < Error; end | |||
class ServerError < Error; end | |||
class NoEncryptionError < Error; end | |||
class API | |||
include HTTParty | |||
#debug_output $stdout | |||
base_uri 'https://www.ssllabs.com/ssltest' | |||
attr_reader :hostname, :ip, :rank, :ssl, :tls, :bits, :rc4, :pfs, :hsts | |||
def initialize(hostname, debug: false) | |||
@debug = debug | |||
@hostname = hostname | |||
@ip = nil | |||
html = content hostname | |||
@rank = html.css('#rating div')[1].text.strip | |||
parse_configuration html | |||
end | |||
private | |||
def content(hostname, ip=nil) | |||
#puts "host: #{hostname}, ip: #{ip}" | |||
options = {query: {d: hostname}} | |||
options[:query][:s] = ip unless ip.nil? | |||
html = nil | |||
loop do | |||
response = self.class.get '/analyze.html', options | |||
raise ServerError, response.code unless response.success? | |||
html = Nokogiri::HTML response.body | |||
File.write File.join('html', hostname), html if @debug | |||
break if not resolving_domain? html | |||
end | |||
waiting? html | |||
html = resolve_multiple_servers html | |||
encrypted? html | |||
@hostname = html.at_css('.url').content.strip | |||
ip = html.at_css '.ip' | |||
unless ip.nil? | |||
@ip = ip.content.strip.gsub /[()]/, '' | |||
else | |||
@ip = '' | |||
end | |||
html | |||
end | |||
def waiting?(html) | |||
warning = html.at_css '#warningBox' | |||
raise WaitingError if not warning.nil? and warning.content.include? 'Please wait...' | |||
end | |||
def encrypted?(html) | |||
warning = html.at_css '#warningBox' | |||
raise NoEncryptionError if not warning.nil? and \ | |||
warning.content.include? 'Assessment failed: Unable to connect to server' | |||
end | |||
def resolving_domain?(html) | |||
warning = html.at_css('#warningBox') | |||
not warning.nil? and warning.content.strip == 'Please wait... (Resolving domain names)' | |||
end | |||
def resolve_multiple_servers(html) | |||
servers = html.at_css('#multiTable') | |||
return html if servers.nil? | |||
servers.css('tr').each do |server| | |||
td = server.css('td')[4] | |||
next if td.nil? | |||
rank = td.content.strip | |||
unless rank == '-' | |||
ip = server.at_css('.ip').content | |||
html = content hostname, ip | |||
waiting? html | |||
return html | |||
end | |||
end | |||
raise NoEncryptionError | |||
end | |||
def parse_configuration(html) | |||
configuration = html.css('.reportSection')[2] | |||
parse_protocols configuration | |||
parse_handshakes configuration | |||
parse_details configuration | |||
end | |||
def parse_protocols(configuration) | |||
protocols = configuration.css('.reportTable')[0].css('tr.tableRow') | |||
@tls = true | |||
@ssl = false | |||
protocols.each do |row| | |||
cells = row.css 'td' | |||
next unless cells.size >= 2 | |||
name = cells[0].content.strip | |||
value = cells[1].content.strip | |||
case name | |||
when /^TLS 1.2/ then | |||
@tls = value == 'Yes' | |||
when /^SSL 2/ then | |||
@ssl |= value != 'No' | |||
when /^SSL 3/ then | |||
@ssl |= value != 'No' | |||
end | |||
end | |||
end | |||
def parse_handshakes(configuration) | |||
@bits = nil | |||
handshakes = configuration.css('.reportTable')[2].css('td.tableRight') | |||
handshakes.each do |cell| | |||
value = cell.content.strip | |||
begin | |||
i = Integer value | |||
@bits = @bits.nil? ? i : [@bits, i].min | |||
rescue | |||
end | |||
end | |||
end | |||
def parse_details(configuration) | |||
@rc4 = @pfs = @hsts = nil | |||
details = configuration.css('.reportTable')[3].css('tr.tableRow') | |||
details.each do |row| | |||
cells = row.css 'td' | |||
name = cells[0].content.strip | |||
value = cells[1].content.strip | |||
case name | |||
when 'RC4' then | |||
@rc4 = value != 'No' | |||
when 'Forward Secrecy' then | |||
@pfs = value == 'Yes (with most browsers) ROBUST (more info)' | |||
when 'Strict Transport Security (HSTS)' then | |||
@hsts = value.start_with? 'Yes' | |||
end | |||
end | |||
end | |||
end | |||
end | |||
end |
@@ -84,7 +84,7 @@ | |||
<%= s.hostname %> | |||
</a> | |||
</th> | |||
<% if s.is_a? SSLCheck::NoSslTlsServer %> | |||
<% if s.is_a? SSLCheck::TlsNotSupportedServer %> | |||
<td class="critical" colspan="16"> | |||
No SSL/TLS | |||
</td> | |||
@@ -94,7 +94,7 @@ | |||
when 'A+' then :info | |||
when 'A', 'A-' then :success | |||
when 'B', 'C' then :warning | |||
when 'T', 'X' then :critical | |||
when 'T', 'M' then :critical | |||
else :danger | |||
end | |||
%> |
@@ -21,13 +21,13 @@ | |||
- www.laquadrature.net | |||
- fsf.org | |||
- ubuntu-paris.org | |||
- parinux.org | |||
- www.parinux.org | |||
- aful.org | |||
- rmll.info | |||
- ubuntu-fr.org | |||
- linuxfr.org | |||
- lea-linux.org | |||
- framasoft.net | |||
- framasoft.org | |||
- gnu.org | |||
- description: Banques en ligne | |||
hostnames: | |||
@@ -197,7 +197,6 @@ | |||
- spideroak.com | |||
- hubic.com | |||
- box.com | |||
- mega.co.nz | |||
- description: Divers | |||
hostnames: | |||
- www.mailden.net | |||
@@ -1,110 +0,0 @@ | |||
<!DOCTYPE html> | |||
<html lang="fr"> | |||
<head> | |||
<meta charset="utf-8"> | |||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |||
<meta name="viewport" content="width=device-width, initial-scale=1"> | |||
<title>Status SSL/TLS banque & commerce en ligne</title> | |||
<link rel="stylesheet" href="bootstrap.min.css"> | |||
<style> | |||
body { | |||
margin-top: 10px; | |||
} | |||
td { | |||
text-align: center; | |||
} | |||
</style> | |||
</head> | |||
<body> | |||
<div class="container-fluid"> | |||
<div class="row"> | |||
<div class="col-md-12"> | |||
<table class="table table-bordered table-hover table-condensed"> | |||
<tbody> | |||
<% | |||
first = true | |||
results.each do |r| | |||
unless first | |||
%> | |||
<tr> | |||
<th colspan="8"> </th> | |||
</tr> | |||
<% | |||
end | |||
first = false | |||
%> | |||
<tr> | |||
<th colspan="8" id="<%= r[0] %>"><%= r[0] %></th> | |||
</tr> | |||
<tr> | |||
<th>Site (IP)</th> | |||
<td>Rang</td> | |||
<td>Sécurité (bits)</td> | |||
<td class="danger">SSL 2/3 (obsolète)</td> | |||
<td class="success">TLS 1.2 (actuel)</td> | |||
<td class="danger">RC4</td> | |||
<td class="info">PFS</td> | |||
<td class="info">HSTS</td> | |||
</tr> | |||
<% r[1].each do |s| | |||
rank_color = case s.rank | |||
when 'A+' then :info | |||
when 'A', 'A-' then :success | |||
when 'B', 'C' then :warning | |||
else :danger | |||
end | |||
%> | |||
<tr> | |||
<th id="<%= s.hostname %>"> | |||
<a href="https://www.ssllabs.com/ssltest/analyze.html?d=<%= s.hostname %>&s=<%= s.ip %>" target="_blank"> | |||
<%= s.hostname %> (<%= s.ip %>) | |||
</a> | |||
</th> | |||
<td class="<%= rank_color %>"> | |||
<%= s.rank %> | |||
</td> | |||
<td class="<%= s.bits < 128 ? :danger : :success %>"> | |||
<%= s.bits %> | |||
(<%= s.bits < 128 ? '☹' : '☺' %>) | |||
</td> | |||
<td class="<%= s.ssl ? :danger : :success %>"> | |||
<%= s.ssl ? '✓' : '✗' %> | |||
(<%= s.ssl ? '☹' : '☺' %>) | |||
</td> | |||
<td class="<%= s.tls ? :success : :danger %>"> | |||
<%= s.tls ? '✓' : '✗' %> | |||
(<%= s.tls ? '☺' : '☹' %>) | |||
</td> | |||
<td class="<%= s.rc4 ? :danger : :success %>"> | |||
<%= s.rc4 ? '✓' : '✗' %> | |||
(<%= s.rc4 ? '☹' : '☺' %>) | |||
</td> | |||
<td class="<%= s.pfs ? :success : :danger %>"> | |||
<%= s.pfs ? '✓' : '✗' %> | |||
(<%= s.pfs ? '☺' : '☹' %>) | |||
</td> | |||
<td class="<%= s.hsts ? :success : :danger %>"> | |||
<%= s.hsts ? '✓' : '✗' %> | |||
(<%= s.hsts ? '☺' : '☹' %>) | |||
</td> | |||
</tr> | |||
<% end %> | |||
<tr> | |||
<th>Site (IP)</th> | |||
<td>Rang</td> | |||
<td>Sécurité (bits)</td> | |||
<td class="danger">SSL 2/3 (obsolète)</td> | |||
<td class="success">TLS 1.2 (actuel)</td> | |||
<td class="danger">RC4</td> | |||
<td class="success">PFS</td> | |||
<td class="success">HSTS</td> | |||
</tr> | |||
<% end %> | |||
</tbody> | |||
</table> | |||
</div> | |||
</div> | |||
</div> | |||
</body> | |||
</html> |
@@ -1,51 +0,0 @@ | |||
require 'sslcheck' | |||
module SSLCheck::SSLLabs | |||
describe API do | |||
URL = 'https://www.ssllabs.com/ssltest/analyze.html' | |||
it 'error' do | |||
stub_request(:get, URL).with(query: { d: 'imirhil.fr'}) | |||
.to_return(status: 500) | |||
expect { API.new 'imirhil.fr' } .to raise_error ServerError, '500' | |||
end | |||
it 'waiting' do | |||
stub_request(:get, URL).with(query: { d: 'imirhil.fr'}) | |||
.to_return(status: 200, body: File.read('spec/html/waiting.html')) | |||
expect { API.new 'imirhil.fr' } .to raise_error WaitingError | |||
end | |||
it 'single' do | |||
stub_request(:get, URL).with(query: { d: 'imirhil.fr'}) | |||
.to_return(status: 200, body: File.read('spec/html/perfect.html')) | |||
results = API.new 'imirhil.fr' | |||
expect(results.hostname).to eq 'imirhil.fr' | |||
expect(results.ip).to eq '5.135.187.37' | |||
expect(results.rank).to eq 'A+' | |||
expect(results.ssl).to be false | |||
expect(results.tls).to be true | |||
expect(results.rc4).to be false | |||
expect(results.pfs).to be true | |||
expect(results.hsts).to be true | |||
expect(results.bits).to be 128 | |||
end | |||
it 'multiple' do | |||
stub_request(:get, URL).with(query: { d: 'fortuneo.fr'}) | |||
.to_return(status: 200, body: File.read('spec/html/multiple.html')) | |||
stub_request(:get, URL).with(query: { d: 'fortuneo.fr', s: '93.20.46.72'}) | |||
.to_return(status: 200, body: File.read('spec/html/results.html')) | |||
results = API.new 'fortuneo.fr' | |||
expect(results.hostname).to eq 'fortuneo.fr' | |||
expect(results.ip).to eq '194.51.217.72' | |||
expect(results.rank).to eq 'B' | |||
expect(results.ssl).to be true | |||
expect(results.tls).to be false | |||
expect(results.rc4).to be true | |||
expect(results.pfs).to be false | |||
expect(results.hsts).to be false | |||
expect(results.bits).to be 128 | |||
end | |||
end | |||
end |
@@ -1,10 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
$:.unshift 'lib' | |||
require 'logging' | |||
require 'sslcheck' | |||
Logging.logger.root.appenders = Logging.appenders.stdout | |||
Logging.logger.root.level = :warn | |||
server = SSLCheck::Server.new(ARGV[0], ARGV[1] || 443) | |||
p grade = SSLCheck::Grade.new(server) |
@@ -1,19 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
require 'yaml' | |||
$:.unshift 'lib' | |||
require 'sslcheck' | |||
Logging.logger.root.appenders = Logging.appenders.stdout | |||
Logging.logger.root.level = :error | |||
hosts = [] | |||
File.open('top-1m.csv', 'r') do |file| | |||
i = 0 | |||
while line = file.gets | |||
hosts << ['Top 100 Alexa', line.strip.split(',')[1]] | |||
i += 1 | |||
break if i == 100 | |||
end | |||
end | |||
SSLCheck.analyze hosts, 'output/alexa.html' |
@@ -1,5 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
$:.unshift 'lib' | |||
require 'sslcheck' | |||
name = ARGV[0] || 'index' | |||
SSLCheck.analyze_from_file "output/#{name}.yml", "output/#{name}.html" |
@@ -1,60 +0,0 @@ | |||
#!/usr/bin/env ruby | |||
require 'erb' | |||
require 'yaml' | |||
$:.unshift 'lib' | |||
require 'sslcheck' | |||
SCORES = %w(A+ A A- B C D E F T M) | |||
def score(a); SCORES.index a.rank; end | |||
def check(hostname) | |||
hostname.strip! | |||
print ' ', hostname, ' : ' | |||
begin | |||
result = SSLCheck::SSLLabs::API.new hostname | |||
puts result.rank | |||
result | |||
rescue SSLCheck::SSLLabs::NoEncryptionError | |||
puts 'no encryption' | |||
raise | |||
rescue => e | |||
puts e | |||
raise | |||
end | |||
end | |||
config = YAML.load_file 'hosts.yml' | |||
results = Hash[config.collect { |c| [c['description'], []] }] | |||
loop do | |||
waiting = false | |||
config.each do |c| | |||
description, hosts = c['description'], c['hostnames'] | |||
puts description | |||
hosts.clone.each do |host| | |||
begin | |||
results[description] << check(host) | |||
hosts.delete host | |||
rescue SSLCheck::SSLLabs::WaitingError | |||
waiting = true | |||
rescue SSLCheck::SSLLabs::Error | |||
rescue => e | |||
p e.backtrace | |||
end | |||
end | |||
end | |||
break if not waiting | |||
puts 'Waiting end of analyze' | |||
sleep 1*60 | |||
end | |||
results.each do |d, _| | |||
results[d].sort! do |a, b| | |||
cmp = score(a) <=> score(b) | |||
cmp != 0 ? cmp : a.hostname <=> b.hostname | |||
end | |||
end | |||
puts 'Generate results' | |||
File.write 'output/index.html', ERB.new(File.read('index.erb')).result(binding) |