20 changed files with 619 additions and 926 deletions
@ -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 |
@ -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) |
Loading…
Reference in new issue