Browse Source

Refactor checks & states

new-scoring
aeris 2 years ago
parent
commit
2b8f102bb5

+ 1
- 1
cryptcheck.gemspec View File

@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)

Gem::Specification.new do |spec|
spec.name = 'cryptcheck'
spec.version = '1.0.0'
spec.version = '2.0.0'
spec.authors = ['Aeris']
spec.email = ['aeris+tls@imirhil.fr']


+ 1
- 1
lib/cryptcheck/fixture.rb View File

@@ -6,7 +6,7 @@ class String
error: :red,
warning: :light_red,
good: :green,
perfect: :blue,
great: :blue,
best: :magenta,
unknown: { background: :black }
}

+ 1
- 1
lib/cryptcheck/logger.rb View File

@@ -1,11 +1,11 @@
module CryptCheck
class Logger
LEVELS = %i(trace debug info warning error fatal none)
@@level = :info

def self.level=(level)
@@level = level.to_sym
end
self.level = ENV.fetch 'LOG', :info

def self.log(level, string=nil, output: $stdout, &block)
return unless enabled? level

+ 54
- 36
lib/cryptcheck/state.rb View File

@@ -1,15 +1,31 @@
module CryptCheck
module State
def states
@status ||= calculate_states
# Remove duplicated test for each level
@states ||= self.checks.group_by { |c| c[1] }.collect do |level, checks|
states = checks.group_by(&:first).collect do |name, checks|
states = checks.collect &:last
# true > false > nil
state = if states.include? true
true
elsif states.include? false
false
else
nil
end
[name, state]
end.to_h
[level, states]
end.to_h
end

def status
State.status self.states.reject { |_, v| v.empty? }.keys
@status ||= State.status self.checks.select { |c| c.last == true }.collect { |c| c[1] }
end

LEVELS = %i(best perfect good warning error critical).freeze
PROBLEMS = %i(warning error critical).freeze
BADS = %i(critical error warning).freeze
GOODS = %i(good great best).freeze
LEVELS = (BADS + GOODS).freeze

extend Enumerable

@@ -32,7 +48,7 @@ module CryptCheck

def self.problem(states)
states = self.convert states
self.min PROBLEMS, states
self.min BADS, states
end

def self.sort(states)
@@ -40,7 +56,9 @@ module CryptCheck
end

def self.compare(a, b)
LEVELS.find_index(a.status) <=> LEVELS.find_index(b.status)
a = LEVELS.find_index(a.status) || (LEVELS.size - 1) / 2.0
b = LEVELS.find_index(b.status) || (LEVELS.size - 1) / 2.0
a <=> b
end

def performed_checks
@@ -48,6 +66,11 @@ module CryptCheck
@performed_checks
end

protected
def checks
@checks ||= self.available_checks.collect { |c| perform_check c }.flatten(1) + children.collect(&:checks).flatten(1)
end

private
def self.convert(status)
status = [status] unless status.respond_to? :first
@@ -57,6 +80,11 @@ module CryptCheck
end

def self.min(levels, states)
return nil if states.empty?
(levels & states).first
end

def self.max(levels, states)
return nil if states.empty?
(levels & states).last
end
@@ -74,37 +102,27 @@ module CryptCheck
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
performed_checks = checks
performed_checks.each do |check|
level, name = perform_check check
next unless level
states[level] << name
name, levels, check = check
result = check.call self
case levels
when Symbol # Expected result is true/false/nil
return [[name, levels, result]]
else # Expected result is the best/worst case
# N/A, so return all levels as N/A
return levels.collect { |l| [name, l, nil] } if result.nil?

checks = []
if BADS.include? result
checks += (GOODS & levels).collect { |l| [name, l, false] }
index = BADS.index result
checks += (BADS & levels).collect { |l| [name, l, BADS.index(l) >= index] }
else
checks += (BADS & levels).collect { |l| [name, l, false] }
index = GOODS.index result
checks += (GOODS & levels).collect { |l| [name, l, GOODS.index(l) <= index] }
end
return checks
end

performed_checks = [
performed_checks
.collect { |n, _, l| [l, n] }
.group_by(&:first)
.map { |k, v| [k, v.collect(&:last)] }.to_h
] + children.collect(&:performed_checks)
@performed_checks = State.merge *performed_checks

states
end

def calculate_states
children_states = children.collect(&:states)
states = [personal_states] + children_states
State.merge *states
end
end
end

+ 3
- 2
lib/cryptcheck/tls/cert.rb View File

@@ -107,15 +107,16 @@ module CryptCheck
@cert.issuer
end

protected
include State

CHECKS = WEAK_SIGN.collect do |level, hashes|
hashes.collect do |hash|
["#{hash}_sign".to_sym, -> (s) { s.send "#{hash}?" }, level]
["#{hash}_sign".to_sym, level, -> (s) { s.send "#{hash}?" }]
end
end.flatten(1).freeze

def checks
def available_checks
CHECKS
end


+ 16
- 18
lib/cryptcheck/tls/cipher.rb View File

@@ -104,7 +104,7 @@ module CryptCheck
def to_s(type = :long)
case type
when :long
states = self.states.collect { |k, vs| vs.collect { |v| v.to_s.colorize k } }.flatten.join ' '
states = self.states.collect { |k, vs| vs.select { |_, c| c == true }.collect { |v| v.first.to_s.colorize k } }.flatten.join ' '
"#{@method} #{@name.colorize self.status} [#{states}]"
when :short
@name.colorize self.status
@@ -227,28 +227,26 @@ module CryptCheck
end
end

protected
include State

CHECKS = [
[:dss, -> (c) { c.dss? }, :critical],
[:anonymous, -> (c) { c.anonymous? }, :critical],
[:null, -> (c) { c.null? }, :critical],
[:export, -> (c) { c.export? }, :critical],
[:des, -> (c) { c.des? }, :critical],
[:md5, -> (c) { c.md5? }, :critical],

[:rc4, -> (c) { c.rc4? }, :error],
[:sweet32, -> (c) { c.sweet32? }, :error],

[:no_pfs, -> (c) { not c.pfs? }, :warning],
[:pfs, -> (c) { c.pfs? }, :good],
[:dhe, -> (c) { c.dhe? }, :warning],
[:ecdhe, -> (c) { c.ecdhe? }, :good],

[:aead, -> (c) { c.aead? }, :good]
[:dss, :critical, -> (c) { c.dss? }],
[:anonymous, :critical, -> (c) { c.anonymous? }],
[:null, :critical, -> (c) { c.null? }],
[:export, :critical, -> (c) { c.export? }],
[:des, :critical, -> (c) { c.des? }],
[:md5, :critical, -> (c) { c.md5? }],
[:rc4, :critical, -> (c) { c.rc4? }],
[:sweet32, :critical, -> (c) { c.sweet32? }],

[:pfs, :error, -> (c) { not c.pfs? }],
[:dhe, :warning, -> (c) { c.dhe? }],

[:aead, :good, -> (c) { c.aead? }]
].freeze

def checks
def available_checks
CHECKS
end


+ 3
- 1
lib/cryptcheck/tls/curve.rb View File

@@ -40,11 +40,13 @@ module CryptCheck
end
end

protected
include State

CHECKS = [].freeze

def checks
protected
def available_checks
CHECKS
end
end

+ 13
- 8
lib/cryptcheck/tls/fixture.rb View File

@@ -17,10 +17,11 @@ class ::OpenSSL::PKey::EC
"ECC #{self.size} bits"
end

protected
include ::CryptCheck::State

CHECKS = [
[:weak_key, -> (s) do
[:ecc, %i(critical error warning), -> (s) do
case s.size
when 0...160
:critical
@@ -32,7 +33,7 @@ class ::OpenSSL::PKey::EC
end]
].freeze

def checks
def available_checks
CHECKS
end
end
@@ -50,10 +51,11 @@ class ::OpenSSL::PKey::RSA
"RSA #{self.size} bits"
end

protected
include ::CryptCheck::State

CHECKS = [
[:weak_key, -> (s) do
[:rsa, %i(critical error), -> (s) do
case s.size
when 0...1024
:critical
@@ -63,7 +65,7 @@ class ::OpenSSL::PKey::RSA
end]
].freeze

def checks
def available_checks
CHECKS
end
end
@@ -84,10 +86,11 @@ class ::OpenSSL::PKey::DSA
include ::CryptCheck::State

CHECKS = [
[:weak_key, -> (_) { :critical }]
[:dsa, :critical, -> (_) { true }]
].freeze

def checks
protected
def available_checks
CHECKS
end
end
@@ -105,10 +108,11 @@ class ::OpenSSL::PKey::DH
"DH #{self.size} bits"
end

protected
include ::CryptCheck::State

CHECKS = [
[:weak_dh, -> (s) do
[:dh, %i(critical error), -> (s) do
case s.size
when 0...1024
:critical
@@ -118,7 +122,8 @@ class ::OpenSSL::PKey::DH
end]
].freeze

def checks
protected
def available_checks
CHECKS
end
end

+ 6
- 2
lib/cryptcheck/tls/grade.rb View File

@@ -16,7 +16,7 @@ module CryptCheck
when 'A', 'A+'
:best
when 'B', 'B+'
:perfect
:great
when 'C', 'C+'
:good
when 'E'
@@ -32,6 +32,10 @@ module CryptCheck
Logger.info { "Grade : #{self.grade.colorize color }" }
end

def to_h
{ checks: @checks, states: @states }
end

private
def calculate_grade
return 'V' unless @server.valid?
@@ -47,7 +51,7 @@ module CryptCheck
end

[[:good, 'D', 'C'],
[:perfect, 'C', 'B'],
[:great, 'C', 'B'],
[:best, 'B', 'A']].each do |type, score1, score2|
expected = @checks[type]
unless expected.empty?

+ 26
- 14
lib/cryptcheck/tls/host.rb View File

@@ -44,25 +44,37 @@ module CryptCheck
end
[[@hostname, ip, @port], result]
end.to_h
# rescue StandardError
# raise
rescue => e
@error = e
end

def to_json
JSON.generate(@servers.collect do |host, result|
hostname, ip, _ = host
json = {
hostname: hostname,
ip: ip,
}
case result
when Grade
json[:result] = result.to_json
else
json[:error] = result.message
def to_h
target = {
target: { hostname: @hostname, port: @port },
}
if @error
target[:error] = @error
else
target[:hosts] = @servers.collect do |host, grade|
hostname, ip, port = host
host = {
hostname: hostname,
ip: ip,
port: port
}
case grade
when Grade
host[:analysis] = grade.server.to_h
host[:status] = grade.to_h
else
host[:error] = grade.message
end
host
end
json
end)
end
target
end

private

+ 4
- 4
lib/cryptcheck/tls/https/server.rb View File

@@ -48,11 +48,11 @@ module CryptCheck
hsts? and @hsts >= LONG_HSTS
end

def checks
protected
def available_checks
super + [
[:hsts, -> (s) { s.hsts? }, :good],
[:hsts_long, -> (s) { s.hsts_long? }, :perfect],
#[:must_staple, -> (s) { s.must_staple? }, :best],
[:hsts, %i(warning good great), -> (s) { s.hsts_long? ? :great : s.hsts? ? :good : :warning }],
#[:must_staple, :best, -> (s) { s.must_staple? }],
]
end
end

+ 6
- 4
lib/cryptcheck/tls/method.rb View File

@@ -36,12 +36,14 @@ module CryptCheck
include State

CHECKS = [
[:sslv2, -> (s) { s == :SSLv2 }, :critical],
[:sslv3, -> (s) { s == :SSLv3 }, :critical],
[:tlsv1_2, -> (s) { s == :TLSv1_2 }, :good]
[:sslv2, :critical, -> (s) { s == :SSLv2 }],
[:sslv3, :critical, -> (s) { s == :SSLv3 }],
[:tlsv1_0, :error, -> (s) { s == :TLSv1 }],
[:tlsv1_1, :warning, -> (s) { s == :TLSv1_1 }]
]

def checks
protected
def available_checks
CHECKS
end
end

+ 12
- 13
lib/cryptcheck/tls/server.rb View File

@@ -61,24 +61,23 @@ module CryptCheck
@trusted
end

def to_h

end

protected
include State

CHECKS = [
[:tlsv1_2_only, -> (s) { s.tlsv1_2_only? }, :perfect],
[:pfs_only, -> (s) { s.pfs_only? }, :perfect],
[:ecdhe_only, -> (s) { s.ecdhe_only? }, :perfect],
#[:aead_only, -> (s) { s.aead_only? }, :best],
[:fallback_scsv, :good, -> (s) { s.fallback_scsv? }]
# [:tlsv1_2_only, -> (s) { s.tlsv1_2_only? }, :great],
# [:pfs_only, -> (s) { s.pfs_only? }, :great],
# [:ecdhe_only, -> (s) { s.ecdhe_only? }, :great],
#[:aead_only, -> (s) { s.aead_only? }, :best],
].freeze

def checks
checks = CHECKS
unless self.fallback_scsv? == nil
checks += [
[:no_fallback_scsv, -> (s) { not s.fallback_scsv? }, :error],
[:fallback_scsv, -> (s) { s.fallback_scsv? }, :good]
]
end
checks
def available_checks
CHECKS
end

def children

+ 145
- 48
spec/cryptcheck/status_spec.rb View File

@@ -13,7 +13,7 @@ describe CryptCheck::State do
[:critical, :warning] => :critical,
[:critical, nil] => :critical,
[:critical, :good] => :critical,
[:critical, :perfect] => :critical,
[:critical, :great] => :critical,
[:critical, :best] => :critical,

[:error, :critical] => :critical,
@@ -21,7 +21,7 @@ describe CryptCheck::State do
[:error, :warning] => :error,
[:error, nil] => :error,
[:error, :good] => :error,
[:error, :perfect] => :error,
[:error, :great] => :error,
[:error, :best] => :error,

[:warning, :critical] => :critical,
@@ -29,7 +29,7 @@ describe CryptCheck::State do
[:warning, :warning] => :warning,
[:warning, nil] => :warning,
[:warning, :good] => :warning,
[:warning, :perfect] => :warning,
[:warning, :great] => :warning,
[:warning, :best] => :warning,

[:good, :critical] => :critical,
@@ -37,23 +37,23 @@ describe CryptCheck::State do
[:good, :warning] => :warning,
[:good, nil] => :good,
[:good, :good] => :good,
[:good, :perfect] => :good,
[:good, :great] => :good,
[:good, :best] => :good,

[:perfect, :critical] => :critical,
[:perfect, :error] => :error,
[:perfect, :warning] => :warning,
[:perfect, nil] => :perfect,
[:perfect, :good] => :good,
[:perfect, :perfect] => :perfect,
[:perfect, :best] => :perfect,
[:great, :critical] => :critical,
[:great, :error] => :error,
[:great, :warning] => :warning,
[:great, nil] => :great,
[:great, :good] => :good,
[:great, :great] => :great,
[:great, :best] => :great,

[:best, :critical] => :critical,
[:best, :error] => :error,
[:best, :warning] => :warning,
[:best, nil] => :best,
[:best, :good] => :good,
[:best, :perfect] => :perfect,
[:best, :great] => :great,
[:best, :best] => :best
}.each do |levels, result|
got = CryptCheck::State.status levels
@@ -76,7 +76,7 @@ describe CryptCheck::State do
[:critical, :warning] => :critical,
[:critical, nil] => :critical,
[:critical, :good] => :critical,
[:critical, :perfect] => :critical,
[:critical, :great] => :critical,
[:critical, :best] => :critical,

[:error, :critical] => :critical,
@@ -84,7 +84,7 @@ describe CryptCheck::State do
[:error, :warning] => :error,
[:error, nil] => :error,
[:error, :good] => :error,
[:error, :perfect] => :error,
[:error, :great] => :error,
[:error, :best] => :error,

[:warning, :critical] => :critical,
@@ -92,7 +92,7 @@ describe CryptCheck::State do
[:warning, :warning] => :warning,
[:warning, nil] => :warning,
[:warning, :good] => :warning,
[:warning, :perfect] => :warning,
[:warning, :great] => :warning,
[:warning, :best] => :warning,

[:good, :critical] => :critical,
@@ -100,23 +100,23 @@ describe CryptCheck::State do
[:good, :warning] => :warning,
[:good, nil] => nil,
[:good, :good] => nil,
[:good, :perfect] => nil,
[:good, :great] => nil,
[:good, :best] => nil,

[:perfect, :critical] => :critical,
[:perfect, :error] => :error,
[:perfect, :warning] => :warning,
[:perfect, nil] => nil,
[:perfect, :good] => nil,
[:perfect, :perfect] => nil,
[:perfect, :best] => nil,
[:great, :critical] => :critical,
[:great, :error] => :error,
[:great, :warning] => :warning,
[:great, nil] => nil,
[:great, :good] => nil,
[:great, :great] => nil,
[:great, :best] => nil,

[:best, :critical] => :critical,
[:best, :error] => :error,
[:best, :warning] => :warning,
[:best, nil] => nil,
[:best, :good] => nil,
[:best, :perfect] => nil,
[:best, :great] => nil,
[:best, :best] => nil
}.each do |levels, result|
got = CryptCheck::State.problem levels
@@ -133,7 +133,6 @@ describe CryptCheck::State do

describe '#states' do
def match_states(actual, **expected)
expected = ::CryptCheck::State.empty.merge expected
expect(actual.states).to eq expected
end

@@ -141,7 +140,7 @@ describe CryptCheck::State do
Class.new do
include ::CryptCheck::State

def checks
def available_checks
[]
end
end.new
@@ -150,11 +149,11 @@ describe CryptCheck::State do
Class.new do
include ::CryptCheck::State

def checks
def available_checks
[
[:foo, -> (_) { true }, :critical],
[:bar, -> (_) { :error }],
[:baz, -> (_) { false }]
[:foo, :critical, -> (_) { true }],
[:bar, :error, -> (_) { true }],
[:baz, :warning, -> (_) { false }]
]
end
end.new
@@ -163,8 +162,8 @@ describe CryptCheck::State do
child = Class.new do
include ::CryptCheck::State

def checks
[[:bar, -> (_) { :error }]]
def available_checks
[[:bar, :error, -> (_) { true }]]
end
end.new
Class.new do
@@ -174,21 +173,21 @@ describe CryptCheck::State do
@child = child
end

def checks
[[:foo, -> (_) { :critical }]]
def available_checks
[[:foo, :critical, -> (_) { true }]]
end

def children
[@child]
end
end.new(child)
end.new child
end
let(:duplicated) do
child = Class.new do
include ::CryptCheck::State

def checks
[[:foo, -> (_) { :critical }]]
def available_checks
[[:foo, :error, -> (_) { true }]]
end
end.new
Class.new do
@@ -198,8 +197,8 @@ describe CryptCheck::State do
@child = child
end

def checks
[[:foo, -> (_) { :critical }]]
def available_checks
[[:foo, :critical, -> (_) { true }]]
end

def children
@@ -208,20 +207,118 @@ describe CryptCheck::State do
end.new(child)
end

it 'must return the level if single level specified' do
obj = Class.new do
include ::CryptCheck::State

def available_checks
[[:foo, :critical, -> (_) { true }]]
end
end.new
expect(obj.states).to eq({ critical: { foo: true } })

obj = Class.new do
include ::CryptCheck::State

def available_checks
[[:foo, :critical, -> (_) { false }]]
end
end.new
expect(obj.states).to eq({ critical: { foo: false } })

obj = Class.new do
include ::CryptCheck::State

def available_checks
[[:foo, :critical, -> (_) { nil }]]
end
end.new
expect(obj.states).to eq({ critical: { foo: nil } })
end

it 'must return all levels if multiple levels specified' do
obj = Class.new do
include ::CryptCheck::State

def available_checks
[[:foo, %i(critical error good great), -> (_) { :critical }]]
end
end.new
expect(obj.states).to eq({
critical: { foo: true },
error: { foo: true },
good: { foo: false },
great: { foo: false } })

obj = Class.new do
include ::CryptCheck::State

def available_checks
[[:foo, %i(critical error good great), -> (_) { :error }]]
end
end.new
expect(obj.states).to eq({
critical: { foo: false },
error: { foo: true },
good: { foo: false },
great: { foo: false } })


obj = Class.new do
include ::CryptCheck::State

def available_checks
[[:foo, %i(critical error good great), -> (_) { :great }]]
end
end.new
expect(obj.states).to eq({
critical: { foo: false },
error: { foo: false },
good: { foo: true },
great: { foo: true } })


obj = Class.new do
include ::CryptCheck::State

def available_checks
[[:foo, %i(critical error good great), -> (_) { :good }]]
end
end.new
expect(obj.states).to eq({
critical: { foo: false },
error: { foo: false },
good: { foo: true },
great: { foo: false } })

obj = Class.new do
include ::CryptCheck::State

def available_checks
[[:foo, %i(critical error good great), -> (_) { nil }]]
end
end.new
expect(obj.states).to eq({
critical: { foo: nil },
error: { foo: nil },
good: { foo: nil },
great: { foo: nil } })
end

it 'must return empty if no check nor child' do
match_states empty
end

it 'must return personal status if no child' do
match_states childless, critical: %i(foo), error: %i(bar)
match_states childless, critical: { foo: true }, error: { bar: true }, warning: { baz: false }
end

it 'must return personal and children statuses' do
match_states parent, critical: %i(foo), error: %i(bar)
match_states parent, critical: { foo: true }, error: { bar: true}
end

it 'must return remove duplicated status' do
match_states duplicated, critical: %i(foo)
match_states duplicated, critical: { foo: true }, error: { foo: true }
end
end

@@ -230,7 +327,7 @@ describe CryptCheck::State do
empty = Class.new do
include ::CryptCheck::State

def checks
def available_checks
[]
end
end.new
@@ -241,8 +338,8 @@ describe CryptCheck::State do
empty = Class.new do
include ::CryptCheck::State

def checks
[[:foo, -> (_) { :critical }]]
def available_checks
[[:foo, :critical, -> (_) { true }]]
end
end.new
expect(empty.status).to be :critical
@@ -252,9 +349,9 @@ describe CryptCheck::State do
empty = Class.new do
include ::CryptCheck::State

def checks
[[:foo, -> (_) { :critical }],
[:bar, -> (_) { :error }]]
def available_checks
[[:foo, :critical, -> (_) { true }],
[:bar, :error, -> (_) { true }]]
end
end.new
expect(empty.status).to be :critical

Loading…
Cancel
Save