@@ -1,6 +1,6 @@ | |||
module CryptCheck | |||
class Logger | |||
LEVELS = %i(trace debug info warning error fatal none) | |||
LEVELS = %i(trace debug info warning error fatal none) | |||
@@level = :info | |||
def self.level=(level) | |||
@@ -12,6 +12,16 @@ module CryptCheck | |||
output.puts(string ? string : block.call) | |||
end | |||
if Object.respond_to? :ai | |||
def self.ap(name, object) | |||
self.debug { "#{name} : #{object.ai}" } | |||
end | |||
else | |||
def self.ap(name, object) | |||
self.debug { "#{name} : #{object}" } | |||
end | |||
end | |||
LEVELS.each do |level| | |||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 | |||
def self.#{level}(string=nil, output: $stdout, &block) | |||
@@ -0,0 +1,95 @@ | |||
module CryptCheck | |||
module State | |||
def states | |||
@status ||= calculate_states | |||
end | |||
def status | |||
State.status self.states.reject { |_, v| v.empty? }.keys | |||
end | |||
LEVELS = %i(best perfect good warning error critical).freeze | |||
PROBLEMS = %i(warning error critical).freeze | |||
extend Enumerable | |||
def self.each(&block) | |||
LEVELS.each &block | |||
end | |||
def self.empty | |||
self.collect { |s| [s, []] }.to_h | |||
end | |||
def self.status(states) | |||
states = self.convert states | |||
self.min LEVELS, states | |||
end | |||
class << self | |||
alias_method :'[]', :status | |||
end | |||
def self.problem(states) | |||
states = self.convert states | |||
self.min PROBLEMS, states | |||
end | |||
def self.sort(states) | |||
states.sort { |a, b| self.compare a, b } | |||
end | |||
def self.compare(a, b) | |||
LEVELS.find_index(a.status) <=> LEVELS.find_index(b.status) | |||
end | |||
private | |||
def self.convert(status) | |||
status = [status] unless status.respond_to? :first | |||
first = status.first | |||
status = status.collect &:status if first.respond_to? :status | |||
status | |||
end | |||
def self.min(levels, states) | |||
return nil if states.empty? | |||
(levels & states).last | |||
end | |||
def self.merge(*states) | |||
State.collect do |s| | |||
state = states.collect { |ss| ss.fetch s, [] } | |||
.inject(&:+).uniq | |||
[s, state] | |||
end.to_h | |||
end | |||
def children | |||
[] | |||
end | |||
def perform_check(check) | |||
name, check, level = check | |||
result = check.call self | |||
return nil unless result | |||
level ||= result | |||
[level, name] | |||
end | |||
def personal_states | |||
states = State.empty | |||
checks.each do |check| | |||
level, name = perform_check check | |||
next unless level | |||
states[level] << name | |||
end | |||
states | |||
end | |||
def calculate_states | |||
children_states = children.collect(&:states) | |||
states = [personal_states] + children_states | |||
State.merge *states | |||
end | |||
end | |||
end |
@@ -29,8 +29,8 @@ module CryptCheck | |||
attr_reader :certs, :keys, :dh, :supported_methods, :supported_ciphers, :supported_curves, :curves_preference | |||
def initialize(hostname, family, ip, port) | |||
@hostname, @family, @ip, @port = hostname, family, ip, port | |||
def initialize(hostname, ip, family, port) | |||
@hostname, @ip, @family, @port = hostname, ip, family, port | |||
@dh = [] | |||
@name = "#@ip:#@port" | |||
@@ -412,6 +412,7 @@ module CryptCheck | |||
# Then, filter cert to keep uniq fingerprint | |||
@certs = certs.uniq { |c| c.fingerprint } | |||
@trusted = @valid = true | |||
@certs.each do |cert| | |||
key = cert.key | |||
identity = cert.valid?(@hostname || @ip) | |||
@@ -422,11 +423,13 @@ module CryptCheck | |||
Logger.info { ' Identity : ' + 'valid'.colorize(:good) } | |||
else | |||
Logger.info { ' Identity : ' + 'invalid'.colorize(:error) } | |||
@valid = false | |||
end | |||
if trust == :trusted | |||
Logger.info { ' Trust : ' + 'trusted'.colorize(:good) } | |||
else | |||
Logger.info { ' Trust : ' + 'untrusted'.colorize(:error) + ' - ' + trust } | |||
@trusted = false | |||
end | |||
end | |||
@keys = @certs.collect &:key | |||
@@ -1,16 +1,17 @@ | |||
module CryptCheck | |||
module Tls | |||
class Grade | |||
attr_reader :server, :grade, :status | |||
attr_reader :server, :grade | |||
def initialize(server) | |||
@server = server | |||
@status = @server.status | |||
@checks = checks | |||
@grade = calculate_grade | |||
end | |||
@states = @server.states | |||
Logger.info { '' } | |||
Logger.ap :checks, @checks | |||
Logger.ap :states, @states | |||
@grade = calculate_grade | |||
def display | |||
color = case @grade | |||
when 'A', 'A+' | |||
:best | |||
@@ -24,76 +25,83 @@ module CryptCheck | |||
:error | |||
when 'G' | |||
:critical | |||
when 'M', 'T' | |||
when 'T', 'V' | |||
:unknown | |||
end | |||
Logger.info { "Grade : #{self.grade.colorize color }" } | |||
Logger.info { '' } | |||
State.each do |color| | |||
states = @status[color] | |||
Logger.info { "#{color.to_s.capitalize} : #{states.collect { |s| s.to_s.colorize color }.join ' '}" } unless states.empty? | |||
end | |||
end | |||
private | |||
CHECKS = { | |||
critical: %i( | |||
mdc2_sign md2_sign md4_sign md5_sign sha_sign sha1_sign | |||
best: %i( | |||
), | |||
perfect: %i( | |||
tlsv1_2_only | |||
pfs_only | |||
ecdhe_only | |||
), | |||
good: %i( | |||
tlsv1_2 | |||
pfs | |||
ecdhe | |||
aead | |||
), | |||
warning: %i( | |||
weak_key | |||
weak_dh | |||
sslv2 sslv3 | |||
dhe | |||
), | |||
error: %i( | |||
weak_key | |||
weak_dh | |||
), | |||
warning: %i( | |||
critical: %i( | |||
mdc2_sign md2_sign md4_sign md5_sign sha_sign sha1_sign | |||
weak_key | |||
weak_dh | |||
dhe | |||
), | |||
good: %i( | |||
tls12 | |||
), | |||
perfect: %i( | |||
tls12_only | |||
sslv2 sslv3 | |||
), | |||
best: %i( | |||
) | |||
}.freeze | |||
def checks | |||
CHECKS | |||
end | |||
def calculate_grade | |||
return 'V' unless @server.valid? | |||
return 'T' unless @server.trusted? | |||
case | |||
when !@status[:critical].empty? | |||
when !@states[:critical].empty? | |||
return 'G' | |||
when !@status[:error].empty? | |||
when !@states[:error].empty? | |||
return 'F' | |||
when !@status[:warning].empty? | |||
when !@states[:warning].empty? | |||
return 'E' | |||
end | |||
goods = @checks.select { |c| c.last == :good }.collect &:first | |||
unless goods.empty? | |||
return 'D' if @status[:good].empty? | |||
return 'C' if @status[:good] != goods | |||
end | |||
perfects = @checks.select { |c| c.last == :perfect }.collect &:first | |||
unless perfects.empty? | |||
return 'C+' if @status[:perfect].empty? | |||
return 'B' if @status[:perfect] != perfects | |||
end | |||
[[:good, 'D', 'C'], | |||
[:perfect, 'C', 'B'], | |||
[:best, 'B', 'A']].each do |type, score1, score2| | |||
expected = @checks[type] | |||
unless expected.empty? | |||
available = @states[type] | |||
return score1 if available.empty? | |||
missed = expected - available | |||
unless missed.empty? | |||
Logger.info { "Missing #{type} : #{missed}" } | |||
return score2 | |||
end | |||
bests = @checks.select { |c| c.last == :best }.collect &:first | |||
unless bests.empty? | |||
return 'B+' if @status[:best].empty? | |||
return 'A' if @status[:best] != bests | |||
# I'm not error prone. The code yes. | |||
additional = available - expected | |||
unless additional.empty? | |||
Logger.fatal { "Developper missed #{type} : #{additional}".colorize :critical } | |||
exit -1 | |||
end | |||
end | |||
end | |||
'A+' | |||
@@ -2,6 +2,14 @@ module CryptCheck | |||
module Tls | |||
module Https | |||
class Grade < Tls::Grade | |||
CHECKS = { | |||
good: %i(hsts), | |||
perfect: %i(hsts_long) | |||
} | |||
def checks | |||
State.merge super, CHECKS | |||
end | |||
end | |||
end | |||
end | |||
@@ -53,6 +53,14 @@ module CryptCheck | |||
@cert.extensions.any? { |e| e.oid == '1.3.6.1.5.5.7.1.24' } | |||
end | |||
def valid? | |||
@valid | |||
end | |||
def trusted? | |||
@trusted | |||
end | |||
include State | |||
CHECKS = [ | |||
@@ -2,6 +2,12 @@ module CryptCheck | |||
module Tls | |||
module Smtp | |||
class Grade < Tls::Grade | |||
CHECKS = { | |||
} | |||
def checks | |||
State.merge super, CHECKS | |||
end | |||
end | |||
end | |||
end | |||
@@ -1,6 +1,3 @@ | |||
require 'erb' | |||
require 'parallel' | |||
module CryptCheck | |||
module Tls | |||
module Xmpp | |||
@@ -2,10 +2,12 @@ module CryptCheck | |||
module Tls | |||
module Xmpp | |||
class Grade < Tls::Grade | |||
CHECKS = { | |||
good: %i(required) | |||
} | |||
def checks | |||
super + [ | |||
[:required, Proc.new { |s| s.required? }, :good], | |||
] | |||
State.merge super, CHECKS | |||
end | |||
end | |||
end | |||