From c1499fc6e60fa1fc8b1c13dd409c36a49c6ffcfc Mon Sep 17 00:00:00 2001 From: Aeris Date: Mon, 22 Feb 2016 00:01:39 +0100 Subject: [PATCH] Move up host resolving for speed ehanchement --- bin/check_https.rb | 12 +- bin/check_https_alexa | 1 - bin/check_https_alexa.rb | 21 ---- bin/check_smtp.rb | 16 ++- bin/check_ssh.rb | 13 +- bin/check_tls.rb | 7 +- bin/check_xmpp.rb | 22 ++-- lib/cryptcheck.rb | 93 +++++++++++++++ lib/cryptcheck/logger.rb | 2 +- lib/cryptcheck/ssh.rb | 9 ++ lib/cryptcheck/ssh/grade.rb | 134 --------------------- lib/cryptcheck/ssh/server.rb | 23 ++-- lib/cryptcheck/tls.rb | 71 ++--------- lib/cryptcheck/tls/https.rb | 14 ++- lib/cryptcheck/tls/https/server.rb | 2 +- lib/cryptcheck/tls/server.rb | 67 +++++------ lib/cryptcheck/tls/smtp.rb | 21 +++- lib/cryptcheck/tls/smtp/server.rb | 14 +-- lib/cryptcheck/tls/xmpp.rb | 61 ++++------ lib/cryptcheck/tls/xmpp/server.rb | 37 +++--- output/alexa.yml | 101 ++++++++++++++++ output/index.yml | 1 + output/smtp.erb | 4 +- output/xmpp.erb | 184 +++++++++++++++-------------- 24 files changed, 456 insertions(+), 474 deletions(-) delete mode 120000 bin/check_https_alexa delete mode 100755 bin/check_https_alexa.rb create mode 100644 lib/cryptcheck/ssh.rb delete mode 100644 lib/cryptcheck/ssh/grade.rb create mode 100644 output/alexa.yml diff --git a/bin/check_https.rb b/bin/check_https.rb index eb11cd8..6df6d09 100755 --- a/bin/check_https.rb +++ b/bin/check_https.rb @@ -6,14 +6,10 @@ require 'cryptcheck' name = ARGV[0] || 'index' file = ::File.join 'output', "#{name}.yml" - if ::File.exist? file - ::CryptCheck::Logger.level = :none - ::CryptCheck::Tls::Https.analyze_from_file "output/#{name}.yml", "output/#{name}.html" + ::CryptCheck::Logger.level = ENV['LOG'] || :none + ::CryptCheck::Tls::Https.analyze_file file, "output/#{name}.html" else - ::CryptCheck::Logger.level = (ARGV[1] || :info).to_sym - server = ::CryptCheck::Tls::Https::Server.new ARGV[0] - grade = ::CryptCheck::Tls::Https::Grade.new server - ::CryptCheck::Logger.info { '' } - grade.display + ::CryptCheck::Logger.level = ENV['LOG'] || :info + ::CryptCheck::Tls::Https.analyze ARGV[0], (ARGV[1] || 443) end diff --git a/bin/check_https_alexa b/bin/check_https_alexa deleted file mode 120000 index 22882cf..0000000 --- a/bin/check_https_alexa +++ /dev/null @@ -1 +0,0 @@ -runner \ No newline at end of file diff --git a/bin/check_https_alexa.rb b/bin/check_https_alexa.rb deleted file mode 100755 index 82989db..0000000 --- a/bin/check_https_alexa.rb +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env ruby -$:.unshift File.expand_path File.join File.dirname(__FILE__), '../lib' -require 'rubygems' -require 'bundler/setup' -require 'cryptcheck' - -GROUP_NAME = 'Top 100 Alexa' - -::CryptCheck::Logger.level = :none - -hosts = [] -::File.open('top-1m.csv', 'r') do |file| - i = 0 - while line = file.gets - hosts << [GROUP_NAME, line.strip.split(',')[1]] - i += 1 - break if i == 100 - end -end - -::CryptCheck::Tls::Https.analyze hosts, 'output/alexa.html' diff --git a/bin/check_smtp.rb b/bin/check_smtp.rb index 538a7c5..0f54149 100755 --- a/bin/check_smtp.rb +++ b/bin/check_smtp.rb @@ -4,16 +4,14 @@ require 'rubygems' require 'bundler/setup' require 'cryptcheck' -name = ARGV[0] -if name - ::CryptCheck::Logger.level = (ARGV[1] || :info).to_sym - server = ::CryptCheck::Tls::Smtp::Server.new ARGV[0] - grade = ::CryptCheck::Tls::Smtp::Grade.new server - ::CryptCheck::Logger.info { '' } - grade.display +name = ARGV[0] || 'smtp' +file = ::File.join 'output', "#{name}.yml" +if ::File.exist? file + ::CryptCheck::Logger.level = ENV['LOG'] || :none + ::CryptCheck::Tls::Smtp.analyze_file file, "output/#{name}.html" else - ::CryptCheck::Logger.level = :none - ::CryptCheck::Tls::Smtp.analyze_from_file 'output/smtp.yml', 'output/smtp.html' + ::CryptCheck::Logger.level = ENV['LOG'] || :info + ::CryptCheck::Tls::Smtp.analyze ARGV[0] end diff --git a/bin/check_ssh.rb b/bin/check_ssh.rb index ddc7f5a..1bd2524 100755 --- a/bin/check_ssh.rb +++ b/bin/check_ssh.rb @@ -4,14 +4,5 @@ require 'rubygems' require 'bundler/setup' require 'cryptcheck' -name, port, level = case ARGV.length - when 1 then [ARGV[0], 22, :info] - when 2 then [ARGV[0], ARGV[1], :info] - when 3 then [ARGV[0], ARGV[1], ARGV[2]] -end - -::CryptCheck::Logger.level = level -server = ::CryptCheck::Ssh::Server.new name, port -#grade = ::CryptCheck::Ssh::Grade.new server -#::CryptCheck::Logger.info { '' } -#grade.display +::CryptCheck::Logger.level = ENV['LOG'] || :info +::CryptCheck::Ssh.analyze ARGV[0], (ARGV[1] || 22) diff --git a/bin/check_tls.rb b/bin/check_tls.rb index ab2e40e..1fe2fb0 100755 --- a/bin/check_tls.rb +++ b/bin/check_tls.rb @@ -4,8 +4,5 @@ require 'rubygems' require 'bundler/setup' require 'cryptcheck' -::CryptCheck::Logger.level = (ARGV[2] || :info).to_sym -server = ::CryptCheck::Tls::TcpServer.new ARGV[0], ARGV[1] -grade = ::CryptCheck::Tls::Grade.new server -::CryptCheck::Logger.info { '' } -grade.display +::CryptCheck::Logger.level = ENV['LOG'] || :info +::CryptCheck::Tls::analyze ARGV[0], ARGV[1] diff --git a/bin/check_xmpp.rb b/bin/check_xmpp.rb index e4011c6..b6a43ff 100755 --- a/bin/check_xmpp.rb +++ b/bin/check_xmpp.rb @@ -4,20 +4,12 @@ require 'rubygems' require 'bundler/setup' require 'cryptcheck' -name, type, level = case ARGV.length - when 1 then [ARGV[0], :s2s, :info] - when 2 then [ARGV[0], ARGV[1].to_sym, :info] - when 3 then [ARGV[0], ARGV[1].to_sym, ARGV[2].to_sym] - end - -if name - ::CryptCheck::Logger.level = level - server = ::CryptCheck::Tls::Xmpp::Server.new name, type - grade = ::CryptCheck::Tls::Xmpp::Grade.new server - ::CryptCheck::Logger.info { '' } - grade.display +name = ARGV[0] || 'xmpp' +file = ::File.join 'output', "#{name}.yml" +if ::File.exist? file + ::CryptCheck::Logger.level = ENV['LOG'] || :none + ::CryptCheck::Tls::Xmpp.analyze_file file, "output/#{name}.html" else - ::CryptCheck::Logger.level = :none - ::CryptCheck::Tls::Xmpp.analyze_from_file 'output/xmpp.yml', 'output/xmpp.html' + ::CryptCheck::Logger.level = ENV['LOG'] || :info + ::CryptCheck::Tls::Xmpp.analyze ARGV[0], type: (ARGV[1] || :s2s).to_sym end - diff --git a/lib/cryptcheck.rb b/lib/cryptcheck.rb index edd77d3..5941a90 100644 --- a/lib/cryptcheck.rb +++ b/lib/cryptcheck.rb @@ -1,7 +1,13 @@ require 'colorize' +require 'ipaddr' +require 'timeout' +require 'yaml' require 'cryptcheck/tls/fixture' module CryptCheck + MAX_ANALYSIS_DURATION = 600 + PARALLEL_ANALYSIS = 10 + autoload :Logger, 'cryptcheck/logger' autoload :Tls, 'cryptcheck/tls' module Tls @@ -32,9 +38,96 @@ module CryptCheck end end + autoload :Ssh, 'cryptcheck/ssh' module Ssh autoload :Packet, 'cryptcheck/ssh/packet' autoload :Server, 'cryptcheck/ssh/server' autoload :SshNotSupportedServer, 'cryptcheck/ssh/server' + autoload :Grade, 'cryptcheck/ssh/grade' + end + + private + def self.addresses(host) + begin + ip = IPAddr.new host + [[ip.family, ip.to_s, nil]] + rescue IPAddr::InvalidAddressError + begin + ::Addrinfo.getaddrinfo(host, nil, nil, :STREAM) + .collect { |a| [a.afamily, a.ip_address, host] } + rescue ::SocketError => e + Logger.error { "Unable to resolv #{host} : #{e}" } + [] + end + end + end + + def self.analyze_addresses(host, addresses, port, on_error = TLS_NOT_AVAILABLE, &block) + begin + ::Timeout::timeout MAX_ANALYSIS_DURATION do + addresses.each { |family, ip, host| return block.call family, ip, host } + end + rescue ::Exception => e + Logger.error e + end + on_error.call host, port + end + + def self.analyze(host, port, on_error = Tls::TLS_NOT_AVAILABLE, &block) + analyze_addresses host, addresses(host), port, on_error, &block + end + + def self.analyze_hosts(hosts, template, output, groups: nil, &block) + results = {} + semaphore = ::Mutex.new + ::Parallel.each hosts, progress: 'Analysing', in_threads: PARALLEL_ANALYSIS, finish: lambda { |item, _, _| puts item[1] } do |description, host| + #hosts.each do |description, host| + result = block.call 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 template).result(binding) + end + + def self.analyze_file(input, template, output, &block) + config = ::YAML.load_file input + hosts = [] + groups = [] + + config.each do |c| + d, hs = c['description'], c['hostnames'] + groups << d + hs.each { |host| hosts << [d, host] } + end + + self.analyze_hosts hosts, template, output, groups: groups, &block + 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 diff --git a/lib/cryptcheck/logger.rb b/lib/cryptcheck/logger.rb index bcd60c5..e7bf052 100644 --- a/lib/cryptcheck/logger.rb +++ b/lib/cryptcheck/logger.rb @@ -4,7 +4,7 @@ module CryptCheck @@level = :info def self.level=(level) - @@level = level + @@level = level.to_sym end def self.log(level, string=nil, output: $stdout, &block) diff --git a/lib/cryptcheck/ssh.rb b/lib/cryptcheck/ssh.rb new file mode 100644 index 0000000..a24a85d --- /dev/null +++ b/lib/cryptcheck/ssh.rb @@ -0,0 +1,9 @@ +module CryptCheck + module Ssh + def self.analyze(host, port=22) + ::CryptCheck.analyze(host, port, Proc.new { SshNotSupportedServer.new host, port }) do |_, ip, host| + Server.new ip, port, hostname: host + end + end + end +end diff --git a/lib/cryptcheck/ssh/grade.rb b/lib/cryptcheck/ssh/grade.rb deleted file mode 100644 index a9ea317..0000000 --- a/lib/cryptcheck/ssh/grade.rb +++ /dev/null @@ -1,134 +0,0 @@ -module CryptCheck - module Ssh - class SshNotSupportedGrade - attr_reader :server, :score, :grade - - def initialize(server) - @server, @score, @grade = server, -1, 'X' - end - end - - class Grade - attr_reader :server, :protocol_score, :key_exchange_score, :cipher_strengths_score, :score, :grade, :error, :warning, :success - - def initialize(server) - @server = server - end - - def display - color = case self.grade - when 'A+' then :blue - when 'A' then :green - when 'B', 'C' then :yellow - when 'E', 'F' then :red - when 'M', 'T' then { color: :white, background: :red } - end - - Logger.info { "Grade : #{self.grade.colorize color }" } - Logger.info { '' } - Logger.info { "Protocole : #{self.protocol_score} / 100" } - Logger.info { "Key exchange : #{self.key_exchange_score} / 100" } - Logger.info { "Ciphers strength : #{self.cipher_strengths_score} / 100" } - Logger.info { "Overall score : #{self.score} / 100" } - Logger.info { '' } - Logger.info { "Errors : #{self.error.join(' ').colorize :red }" } unless self.error.empty? - Logger.info { "Warnings : #{self.warning.join(' ').colorize :yellow }" } unless self.warning.empty? - Logger.info { "Best practices : #{self.success.join(' ').colorize :green }" } unless self.success.empty? - 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, 'F'].max unless @error.empty? - - @grade = 'M' unless @server.cert_valid - @grade = 'T' unless @server.cert_trusted - - @grade = 'A+' if @grade == 'A' and @error.empty? and @warning.empty? and (all_success & @success) == all_success - end - - def calculate_error - @error = [] - - @error << :md5_sig if @server.md5_sig? - @error << :sslv2 if @server.sslv2? - @error << :sslv3 if @server.sslv3? - - @error << :md5 if @server.md5? - @error << :anonymous if @server.anonymous? - @error << :dss if @server.dss? - - @error << :null if @server.null? - @error << :export if @server.export? - @error << :des if @server.des? - @error << :rc4 if @server.rc4? - end - - def calculate_warning - @warning = [] - - @warning << :sha1_sig if @server.sha1_sig? - - #@warning << :sha1 if @server.sha1? - - @warning << :des3 if @server.des3? - end - - def calculate_success - @success = [] - @success << :pfs if @server.pfs_only? - end - - ALL_ERROR = %i(md5_sig md5 anonymous dss null export des rc4) - def all_error - ALL_ERROR - end - - ALL_WARNING = %i(sha1_sig des3) - def all_warning - ALL_WARNING - end - - ALL_SUCCESS = %i(pfs) - def all_success - ALL_SUCCESS - end - - METHODS_SCORES = { SSLv2: 0, SSLv3: 20, TLSv1: 60, TLSv1_1: 80, TLSv1_2: 100 } - def calculate_protocol_score - @protocol_score = @server.supported_protocols.collect { |p| METHODS_SCORES[p] }.min - end - - def calculate_key_exchange_score - @key_exchange_score = case @server.key_size - when 0 then 0 - when 0...512 then 10 - when 512...1024 then 20 - when 1024...2048 then 50 - when 2048...4096 then 90 - else 100 - end - end - - def calculate_cipher_strengths_score - @cipher_strengths_score = case @server.cipher_size - when 0 then 0 - when 0...112 then 10 - when 112...128 then 50 - when 128...256 then 90 - else 100 - end - end - end - end -end diff --git a/lib/cryptcheck/ssh/server.rb b/lib/cryptcheck/ssh/server.rb index d859f95..0253147 100644 --- a/lib/cryptcheck/ssh/server.rb +++ b/lib/cryptcheck/ssh/server.rb @@ -3,10 +3,10 @@ require 'socket' module CryptCheck module Ssh class SshNotSupportedServer - attr_reader :hostname, :port + attr_reader :host, :port - def initialize(hostname, port) - @hostname, @port = hostname, port + def initialize(host, port) + @host, @port = host, port end end @@ -15,7 +15,7 @@ module CryptCheck class SshNotAvailableException < Exception end - attr_reader :hostname, :port, :kex, :encryption, :hmac, :compression, :key + attr_reader :ip, :port, :hostname, :kex, :encryption, :hmac, :compression, :key KEX = { 'curve25519-sha256@libssh.org' => :green, @@ -89,11 +89,11 @@ module CryptCheck 'ssh-dss-cert-v00@openssh.com' => :red, # DSA } - def initialize(hostname, port=22) - @hostname, @port = hostname, port + def initialize(ip, port=22, hostname:) + @ip, @port, @hostname = ip, port, hostname - Logger.info { "#{hostname}:#{port}".colorize :blue } - kex = ::Socket.tcp hostname, port, connect_timeout: TCP_TIMEOUT do |socket| + Logger.info { name.colorize :blue } + kex = ::Socket.tcp ip, port, connect_timeout: TCP_TIMEOUT do |socket| socket.readline socket.write "SSH-2.0-CryptCheck\r\n" Packet.read_kex_init socket @@ -115,6 +115,13 @@ module CryptCheck Logger.debug { "SSH not supported : #{e}" } raise SshNotAvailableException, e end + + private + def name + name = "#{@hostname || @ip}:#@port" + name += " [#@ip]" if @hostname + name + end end end end diff --git a/lib/cryptcheck/tls.rb b/lib/cryptcheck/tls.rb index 63d5305..e085880 100644 --- a/lib/cryptcheck/tls.rb +++ b/lib/cryptcheck/tls.rb @@ -3,60 +3,18 @@ require 'parallel' module CryptCheck module Tls - MAX_ANALYSIS_DURATION = 600 - PARALLEL_ANALYSIS = 10 - - def self.grade(hostname, port, server_class:, grade_class:) - timeout MAX_ANALYSIS_DURATION do - grade_class.new server_class.new hostname, port + TLS_NOT_AVAILABLE = Proc.new { |host, port| + TlsNotSupportedGrade.new TlsNotSupportedServer.new host, port + } + + def self.analyze(host, port) + ::CryptCheck.analyze host, port do |family, ip, host| + s = TcpServer.new family, ip, port, hostname: host + g = Grade.new s + Logger.info { '' } + g.display + g end - rescue ::Exception => e - Logger.error { "Error during #{hostname}:#{port} analysis : #{e}" } - TlsNotSupportedGrade.new TlsNotSupportedServer.new hostname, port - end - - def self.analyze(hosts, template, output, groups = nil, port:, server_class:, grade_class:) - 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, port, server_class: server_class, grade_class: grade_class - 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(template)).result(binding) - end - - def self.analyze_from_file(file, template, output, port:, server_class:, grade_class:) - 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, template, output, groups, port: port, server_class: server_class, grade_class: grade_class end def self.colorize(cipher) @@ -80,12 +38,5 @@ module CryptCheck end "#{key.type.to_s.upcase.colorize type_color} #{key.size.to_s.colorize size_color} bits" 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 diff --git a/lib/cryptcheck/tls/https.rb b/lib/cryptcheck/tls/https.rb index baa73eb..33314f0 100644 --- a/lib/cryptcheck/tls/https.rb +++ b/lib/cryptcheck/tls/https.rb @@ -1,12 +1,18 @@ module CryptCheck module Tls module Https - def self.analyze(hosts, output) - Tls.analyze hosts, 'output/https.erb', output, nil, port: 443, server_class: Server, grade_class: Grade + def self.analyze(host, port=443) + ::CryptCheck.analyze host, port do |family, ip, host| + s = Server.new family, ip, port, hostname: host + g = Grade.new s + Logger.info { '' } + g.display + g + end end - def self.analyze_from_file(file, output) - Tls.analyze_from_file file, 'output/https.erb', output, port: 443, server_class: Server, grade_class: Grade + def self.analyze_file(input, output) + ::CryptCheck.analyze_file(input, 'output/https.erb', output) { |host| self.analyze host } end end end diff --git a/lib/cryptcheck/tls/https/server.rb b/lib/cryptcheck/tls/https/server.rb index d970a96..9eb8b65 100644 --- a/lib/cryptcheck/tls/https/server.rb +++ b/lib/cryptcheck/tls/https/server.rb @@ -6,7 +6,7 @@ module CryptCheck class Server < Tls::TcpServer attr_reader :hsts - def initialize(hostname, port=443) + def initialize(family, ip, port = 443, hostname: nil) super fetch_hsts end diff --git a/lib/cryptcheck/tls/server.rb b/lib/cryptcheck/tls/server.rb index d163583..844e082 100644 --- a/lib/cryptcheck/tls/server.rb +++ b/lib/cryptcheck/tls/server.rb @@ -7,8 +7,8 @@ module CryptCheck class TlsNotSupportedServer attr_reader :hostname, :port - def initialize(hostname, port) - @hostname, @port = hostname, port + def initialize(host, port) + @hostname, @port = host, port end end @@ -32,12 +32,12 @@ module CryptCheck class ConnectionError < TLSException end - attr_reader :hostname, :port, :prefered_ciphers, :cert, :cert_valid, :cert_trusted, :dh + attr_reader :family, :ip, :port, :hostname, :prefered_ciphers, :cert, :cert_valid, :cert_trusted, :dh - def initialize(hostname, port) - @hostname, @port = hostname, port + def initialize(family, ip, port, hostname: nil) + @family, @ip, @port, @hostname = family, ip, port, hostname @dh = [] - Logger.info { "#{hostname}:#{port}".colorize :blue } + Logger.info { name.colorize :blue } extract_cert Logger.info { '' } Logger.info { "Key : #{Tls.key_to_s self.key}" } @@ -119,22 +119,28 @@ module CryptCheck end private - def connect(family, host, port, &block) - socket = ::Socket.new family, sock_type - sockaddr = ::Socket.sockaddr_in port, host - Logger.trace { "Connecting to #{host}:#{port}" } + def name + name = "#{@hostname || @ip}:#@port" + name += " [#@ip]" if @hostname + name + end + + def connect(&block) + socket = ::Socket.new @family, sock_type + sockaddr = ::Socket.sockaddr_in @port, @ip + Logger.trace { "Connecting to #{name}" } begin status = socket.connect_nonblock sockaddr - Logger.trace { "Connecting to #{host}:#{port} status : #{status}" } + Logger.trace { "Connecting to #{name} status : #{status}" } raise ConnectionError, status unless status == 0 - Logger.trace { "Connected to #{host}:#{port}" } + Logger.trace { "Connected to #{name}" } block_given? ? block.call(socket) : nil rescue ::IO::WaitReadable - Logger.trace { "Waiting for read to #{host}:#{port}" } + Logger.trace { "Waiting for read to #{name}" } raise Timeout unless IO.select [socket], nil, nil, TCP_TIMEOUT retry rescue ::IO::WaitWritable - Logger.trace { "Waiting for write to #{host}:#{port}" } + Logger.trace { "Waiting for write to #{name}" } raise Timeout unless IO.select nil, [socket], nil, TCP_TIMEOUT retry rescue => e @@ -150,18 +156,18 @@ module CryptCheck def ssl_connect(socket, context, method, &block) ssl_socket = ::OpenSSL::SSL::SSLSocket.new socket, context - ssl_socket.hostname = @hostname unless method == :SSLv2 - Logger.trace { "SSL connecting to #{@hostname}:#{@port}" } + ssl_socket.hostname = @hostname if @hostname and method != :SSLv2 + Logger.trace { "SSL connecting to #{name}" } begin ssl_socket.connect_nonblock - Logger.trace { "SSL connected to #{@hostname}:#{@port}" } + Logger.trace { "SSL connected to #{name}" } return block_given? ? block.call(ssl_socket) : nil rescue ::IO::WaitReadable - Logger.trace { "Waiting for SSL read to #{@hostname}:#{@port}" } + Logger.trace { "Waiting for SSL read to #{name}" } raise TLSTimeout unless IO.select [socket], nil, nil, SSL_TIMEOUT retry rescue ::IO::WaitWritable - Logger.trace { "Waiting for SSL write to #{@hostname}:#{@port}" } + Logger.trace { "Waiting for SSL write to #{name}" } raise TLSTimeout unless IO.select nil, [socket], nil, SSL_TIMEOUT retry rescue ::OpenSSL::SSL::SSLError => e @@ -189,26 +195,13 @@ module CryptCheck ssl_context = ::OpenSSL::SSL::SSLContext.new method ssl_context.ciphers = ciphers if ciphers Logger.trace { "Try #{method} connection with #{ciphers}" } - - [::Socket::AF_INET, ::Socket::AF_INET6].each do |family| - Logger.trace { "Try connection for family #{family}" } - addrs = begin - ::Socket.getaddrinfo @hostname, nil, family, :STREAM - rescue ::SocketError => e - Logger.error { "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 + connect do |socket| + ssl_connect socket, ssl_context, method do |ssl_socket| + return block_given? ? block.call(ssl_socket) : nil end end - Logger.debug { "No SSL available on #{@hostname}" } + Logger.debug { "No SSL available on #{name}" } raise CipherNotAvailable end @@ -223,7 +216,7 @@ module CryptCheck end end raise TLSNotAvailableException unless @cert - @cert_valid = ::OpenSSL::SSL.verify_certificate_identity @cert, @hostname + @cert_valid = ::OpenSSL::SSL.verify_certificate_identity @cert, (@hostname || @ip) @cert_trusted = verify_trust @chain, @cert end diff --git a/lib/cryptcheck/tls/smtp.rb b/lib/cryptcheck/tls/smtp.rb index 1c5806d..3f7d433 100644 --- a/lib/cryptcheck/tls/smtp.rb +++ b/lib/cryptcheck/tls/smtp.rb @@ -1,8 +1,25 @@ module CryptCheck module Tls module Smtp - def self.analyze_from_file(file, output) - Tls.analyze_from_file file, 'output/smtp.erb', output, port: 25, server_class: Server, grade_class: Grade + def self.analyze(host, port=25, domain: nil) + domain ||= host + ::CryptCheck.analyze host, port do |family, ip, host| + s = Server.new family, ip, port, hostname: host, domain: domain + g = Grade.new s + Logger.info { '' } + g.display + g + end + end + + def self.analyze_domain(domain) + srv = Resolv::DNS.new.getresources(domain, Resolv::DNS::Resource::IN::MX).sort_by(&:preference).first + hostname = srv ? srv.exchange.to_s : domain + self.analyze hostname, domain: domain + end + + def self.analyze_file(input, output) + ::CryptCheck.analyze_file(input, 'output/smtp.erb', output) { |host| self.analyze_domain host } end end end diff --git a/lib/cryptcheck/tls/smtp/server.rb b/lib/cryptcheck/tls/smtp/server.rb index e99cf90..b47a992 100644 --- a/lib/cryptcheck/tls/smtp/server.rb +++ b/lib/cryptcheck/tls/smtp/server.rb @@ -1,22 +1,12 @@ -require 'resolv' - module CryptCheck module Tls module Smtp class Server < Tls::TcpServer - RESOLVER = Resolv::DNS.new - attr_reader :domain - def initialize(domain, port=25) + def initialize(family, ip, port, hostname: nil, domain:) @domain = domain - srv = RESOLVER.getresources(domain, Resolv::DNS::Resource::IN::MX).sort_by(&:preference).first - if srv - hostname = srv.exchange.to_s - else # DNS is not correctly set, guess config… - hostname = domain - end - super hostname, port + super family, ip, port, hostname: hostname end def ssl_connect(socket, context, method, &block) diff --git a/lib/cryptcheck/tls/xmpp.rb b/lib/cryptcheck/tls/xmpp.rb index b333557..5593adb 100644 --- a/lib/cryptcheck/tls/xmpp.rb +++ b/lib/cryptcheck/tls/xmpp.rb @@ -4,49 +4,36 @@ require 'parallel' module CryptCheck module Tls module Xmpp - MAX_ANALYSIS_DURATION = 600 - PARALLEL_ANALYSIS = 10 - - def self.grade(hostname, type=:s2s) - timeout MAX_ANALYSIS_DURATION do - Grade.new Server.new hostname, type + def self.analyze(host, port=nil, domain: nil, type: :s2s) + domain ||= host + ::CryptCheck.analyze host, port do |family, ip, host| + s = Server.new family, ip, port, hostname: host, type: type, domain: domain + g = Grade.new s + Logger.info { '' } + g.display + g end - rescue ::Exception => e - Logger.error { "Error during #{hostname}:#{type} analysis : #{e}" } - TlsNotSupportedGrade.new TlsNotSupportedServer.new hostname, type end - def self.analyze(hosts, output) - servers = [] - semaphore = ::Mutex.new - ::Parallel.each hosts, progress: 'Analysing', in_threads: PARALLEL_ANALYSIS, finish: lambda { |item, _, _| puts item } do |host| - result = grade host.strip - semaphore.synchronize { servers << result } - end - servers.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 + def self.analyze_domain(domain, type: :s2s) + service, port = case type + when :s2s + ['_xmpp-server', 5269] + when :c2s + ['_xmpp-client', 5222] + end + srv = Resolv::DNS.new.getresources("#{service}._tcp.#{domain}", Resolv::DNS::Resource::IN::SRV) + .sort_by(&:priority).first + if srv + hostname, port = srv.target.to_s, srv.port + else # DNS is not correctly set, guess config… + hostname = domain end - - ::File.write output, ::ERB.new(::File.read('output/xmpp.erb')).result(binding) + self.analyze hostname, port, domain: domain, type: type end - def self.analyze_from_file(file, output) - hosts = ::YAML.load_file file - self.analyze hosts, output - end - - private - SCORES = %w(A+ A A- B C D E F T M X) - - def self.score(a) - SCORES.index a.grade + def self.analyze_file(input, output) + ::CryptCheck.analyze_file(input, 'output/xmpp.erb', output) { |host| self.analyze_domain host } end end end diff --git a/lib/cryptcheck/tls/xmpp/server.rb b/lib/cryptcheck/tls/xmpp/server.rb index 425d978..15e5dbb 100644 --- a/lib/cryptcheck/tls/xmpp/server.rb +++ b/lib/cryptcheck/tls/xmpp/server.rb @@ -1,45 +1,40 @@ require 'nokogiri' -require 'resolv' module CryptCheck module Tls module Xmpp TLS_NAMESPACE = 'urn:ietf:params:xml:ns:xmpp-tls' - RESOLVER = Resolv::DNS.new class Server < Tls::TcpServer attr_reader :domain - def initialize(domain, type=:s2s, hostname: nil) + def initialize(family, ip, port=nil, hostname: nil, domain: nil, type: :s2s) + domain ||= hostname @type, @domain = type, domain - service, port = case type - when :s2s then ['_xmpp-server', 5269] - when :c2s then ['_xmpp-client', 5222] - end - unless hostname - srv = RESOLVER.getresources("#{service}._tcp.#{domain}", Resolv::DNS::Resource::IN::SRV).sort_by(&:priority).first - if srv - hostname, port = srv.target.to_s, srv.port - else # DNS is not correctly set, guess config… - hostname = domain - end - end - super hostname, port + port = case type + when :s2s + 5269 + when :c2s + 5222 + end unless port + super family, ip, port, hostname: hostname Logger.info { '' } Logger.info { self.required? ? 'Required'.colorize(:green) : 'Not required'.colorize(:yellow) } end def ssl_connect(socket, context, method, &block) type = case @type - when :s2s then 'jabber:server' - when :c2s then 'jabber:client' - end + when :s2s then + 'jabber:server' + when :c2s then + 'jabber:client' + end socket.write "" response = '' loop do response += socket.recv 1024 - xml = ::Nokogiri::XML response - error = xml.xpath '//stream:error' + xml = ::Nokogiri::XML response + error = xml.xpath '//stream:error' raise Exception, error.text unless error.empty? unless xml.xpath('//stream:features').empty? response = xml diff --git a/output/alexa.yml b/output/alexa.yml new file mode 100644 index 0000000..4063d06 --- /dev/null +++ b/output/alexa.yml @@ -0,0 +1,101 @@ +- description: Top 100 Alexa + - google.com + - facebook.com + - youtube.com + - baidu.com + - yahoo.com + - amazon.com + - wikipedia.org + - qq.com + - google.co.in + - twitter.com + - live.com + - taobao.com + - sina.com.cn + - msn.com + - yahoo.co.jp + - linkedin.com + - google.co.jp + - weibo.com + - bing.com + - yandex.ru + - vk.com + - hao123.com + - ebay.com + - instagram.com + - google.de + - amazon.co.jp + - mail.ru + - 360.cn + - google.ru + - google.co.uk + - t.co + - pinterest.com + - google.com.br + - reddit.com + - netflix.com + - google.fr + - tmall.com + - sohu.com + - paypal.com + - microsoft.com + - wordpress.com + - blogspot.com + - google.it + - tumblr.com + - google.es + - onclickads.net + - imgur.com + - aliexpress.com + - imdb.com + - apple.com + - ok.ru + - xvideos.com + - ask.com + - fc2.com + - stackoverflow.com + - google.com.mx + - gmw.cn + - amazon.de + - google.com.hk + - alibaba.com + - office.com + - google.ca + - google.com.tr + - rakuten.co.jp + - xinhuanet.com + - pornhub.com + - soso.com + - tianya.cn + - google.co.id + - amazon.in + - haosou.com + - blogger.com + - craigslist.org + - github.com + - amazon.co.uk + - nicovideo.jp + - kat.cr + - outbrain.com + - bongacams.com + - go.com + - googleusercontent.com + - 360.com + - naver.com + - google.pl + - adnetworkperformance.com + - cnn.com + - dropbox.com + - pixnet.net + - google.com.au + - flipkart.com + - chinadaily.com.cn + - adobe.com + - xhamster.com + - jd.com + - whatsapp.com + - microsoftonline.com + - chase.com + - coccoc.com + - bbc.co.uk + - indiatimes.com diff --git a/output/index.yml b/output/index.yml index 942e8d8..49c3702 100644 --- a/output/index.yml +++ b/output/index.yml @@ -23,6 +23,7 @@ - tcit.fr - aplu.fr - tdelmas.ovh + - gordon.re - description: Associations hostnames: - april.org diff --git a/output/smtp.erb b/output/smtp.erb index 853983d..e1fb926 100644 --- a/output/smtp.erb +++ b/output/smtp.erb @@ -92,7 +92,9 @@ else :danger end %> - <%= s.domain %> + + <%= s.domain %> + <%= n.grade %> diff --git a/output/xmpp.erb b/output/xmpp.erb index c2585d4..84db564 100644 --- a/output/xmpp.erb +++ b/output/xmpp.erb @@ -30,7 +30,22 @@
- + + <% + first = true + results.each do |r| + unless first + %> + + + + <% + end + first = false + %> + + + @@ -57,8 +72,88 @@ - - + <% r[1].each do |n| + s = n.server + %> + + + <% if s.is_a? Tls::TlsNotSupportedServer %> + + <% else + rank_color = case n.grade + when 'A+' then :info + when 'A', 'A-' then :success + when 'B', 'C' then :warning + when 'T', 'M' then :critical + else :danger + end %> + + + <% key = s.key %> + + + + + + + + + + + + + + + + + <% end %> + + <% end %> @@ -85,89 +180,6 @@ - - - <% servers.each do |n| - s = n.server - %> - - - <% if s.is_a? Tls::TlsNotSupportedServer %> - - <% else - rank_color = case n.grade - when 'A+' then :info - when 'A', 'A-' then :success - when 'B', 'C' then :warning - when 'T', 'M' then :critical - else :danger - end %> - - - <% key = s.key %> - - - - - - - - - - - - - - - - - <% end %> - <% end %>
 
<%= r[0] %>
Host GradePFS Required
+ <%= s.domain %> + + No SSL/TLS + + <%= n.grade %> + + <%= "#{key.size} (#{key.type.to_s.upcase})" %> + (<%= key.size < 2048 ? '☹' : '☺' %>) + + <%= s.sha1_sig? ? '✓' : '✗' %> + (<%= s.sha1_sig? ? '☹' : '☺' %>) + + <%= s.sslv2? ? '✓' : '✗' %> + (<%= s.sslv2? ? '☹' : '☺' %>) + + <%= s.sslv3? ? '✓' : '✗' %> + (<%= s.sslv3? ? '☹' : '☺' %>) + + <%= s.tlsv1_2? ? '✓' : '✗' %> + (<%= s.tlsv1_2? ? '☺' : '☹' %>) + + <%= s.tls? ? '✓' : '✗' %> + (<%= s.tls? ? '☺' : '☹' %>) + + <%= s.cipher_size %> + (<%= s.cipher_size < 128 ? '☹' : '☺' %>) + + <%= s.md5? ? '✓' : '✗' %> + (<%= s.md5? ? '☹' : '☺' %>) + + <%= s.sha1? ? '✓' : '✗' %> + (<%= s.sha1? ? '☹' : '☺' %>) + + <%= (s.rc4? or s.des?) ? '✓' : '✗' %> + (<%= (s.rc4? or s.des?) ? '☹' : '☺' %>) + + <%= s.des3? ? '✓' : '✗' %> + (<%= s.des3? ? '☹' : '☺' %>) + + <%= s.pfs? ? '✓' : '✗' %> + (<%= s.pfs? ? '☺' : '☹' %>) + + <%= s.required? ? '✓' : '✗' %> + (<%= s.required? ? '☺' : '☹' %>) +
Host GradePFS Required
- <%= s.hostname %> - - No SSL/TLS - - <%= n.grade %> - - <%= "#{key.size} (#{key.type.to_s.upcase})" %> - (<%= key.size < 2048 ? '☹' : '☺' %>) - - <%= s.sha1_sig? ? '✓' : '✗' %> - (<%= s.sha1_sig? ? '☹' : '☺' %>) - - <%= s.sslv2? ? '✓' : '✗' %> - (<%= s.sslv2? ? '☹' : '☺' %>) - - <%= s.sslv3? ? '✓' : '✗' %> - (<%= s.sslv3? ? '☹' : '☺' %>) - - <%= s.tlsv1_2? ? '✓' : '✗' %> - (<%= s.tlsv1_2? ? '☺' : '☹' %>) - - <%= s.tls? ? '✓' : '✗' %> - (<%= s.tls? ? '☺' : '☹' %>) - - <%= s.cipher_size %> - (<%= s.cipher_size < 128 ? '☹' : '☺' %>) - - <%= s.md5? ? '✓' : '✗' %> - (<%= s.md5? ? '☹' : '☺' %>) - - <%= s.sha1? ? '✓' : '✗' %> - (<%= s.sha1? ? '☹' : '☺' %>) - - <%= (s.rc4? or s.des?) ? '✓' : '✗' %> - (<%= (s.rc4? or s.des?) ? '☹' : '☺' %>) - - <%= s.des3? ? '✓' : '✗' %> - (<%= s.des3? ? '☹' : '☺' %>) - - <%= s.pfs? ? '✓' : '✗' %> - (<%= s.pfs? ? '☺' : '☹' %>) - - <%= s.required? ? '✓' : '✗' %> - (<%= s.required? ? '☺' : '☹' %>) -