Browse Source

Manage grades

new-scoring
aeris 2 years ago
parent
commit
e0808a3937

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

@@ -1,6 +1,6 @@
1 1
 module CryptCheck
2 2
 	class Logger
3
-		LEVELS = %i(trace debug info warning error fatal none)
3
+		LEVELS  = %i(trace debug info warning error fatal none)
4 4
 		@@level = :info
5 5
 
6 6
 		def self.level=(level)
@@ -12,6 +12,16 @@ module CryptCheck
12 12
 			output.puts(string ? string : block.call)
13 13
 		end
14 14
 
15
+		if Object.respond_to? :ai
16
+			def self.ap(name, object)
17
+				self.debug { "#{name} : #{object.ai}" }
18
+			end
19
+		else
20
+			def self.ap(name, object)
21
+				self.debug { "#{name} : #{object}" }
22
+			end
23
+		end
24
+
15 25
 		LEVELS.each do |level|
16 26
 			class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
17 27
 				def self.#{level}(string=nil, output: $stdout, &block)

+ 95
- 0
lib/cryptcheck/state.rb View File

@@ -0,0 +1,95 @@
1
+module CryptCheck
2
+	module State
3
+		def states
4
+			@status ||= calculate_states
5
+		end
6
+
7
+		def status
8
+			State.status self.states.reject { |_, v| v.empty? }.keys
9
+		end
10
+
11
+		LEVELS   = %i(best perfect good warning error critical).freeze
12
+		PROBLEMS = %i(warning error critical).freeze
13
+
14
+		extend Enumerable
15
+
16
+		def self.each(&block)
17
+			LEVELS.each &block
18
+		end
19
+
20
+		def self.empty
21
+			self.collect { |s| [s, []] }.to_h
22
+		end
23
+
24
+		def self.status(states)
25
+			states = self.convert states
26
+			self.min LEVELS, states
27
+		end
28
+
29
+		class << self
30
+			alias_method :'[]', :status
31
+		end
32
+
33
+		def self.problem(states)
34
+			states = self.convert states
35
+			self.min PROBLEMS, states
36
+		end
37
+
38
+		def self.sort(states)
39
+			states.sort { |a, b| self.compare a, b }
40
+		end
41
+
42
+		def self.compare(a, b)
43
+			LEVELS.find_index(a.status) <=> LEVELS.find_index(b.status)
44
+		end
45
+
46
+		private
47
+		def self.convert(status)
48
+			status = [status] unless status.respond_to? :first
49
+			first  = status.first
50
+			status = status.collect &:status if first.respond_to? :status
51
+			status
52
+		end
53
+
54
+		def self.min(levels, states)
55
+			return nil if states.empty?
56
+			(levels & states).last
57
+		end
58
+
59
+		def self.merge(*states)
60
+			State.collect do |s|
61
+				state = states.collect { |ss| ss.fetch s, [] }
62
+								.inject(&:+).uniq
63
+				[s, state]
64
+			end.to_h
65
+		end
66
+
67
+		def children
68
+			[]
69
+		end
70
+
71
+		def perform_check(check)
72
+			name, check, level = check
73
+			result             = check.call self
74
+			return nil unless result
75
+			level ||= result
76
+			[level, name]
77
+		end
78
+
79
+		def personal_states
80
+			states = State.empty
81
+			checks.each do |check|
82
+				level, name = perform_check check
83
+				next unless level
84
+				states[level] << name
85
+			end
86
+			states
87
+		end
88
+
89
+		def calculate_states
90
+			children_states = children.collect(&:states)
91
+			states          = [personal_states] + children_states
92
+			State.merge *states
93
+		end
94
+	end
95
+end

+ 5
- 2
lib/cryptcheck/tls/engine.rb View File

@@ -29,8 +29,8 @@ module CryptCheck
29 29
 
30 30
 			attr_reader :certs, :keys, :dh, :supported_methods, :supported_ciphers, :supported_curves, :curves_preference
31 31
 
32
-			def initialize(hostname, family, ip, port)
33
-				@hostname, @family, @ip, @port = hostname, family, ip, port
32
+			def initialize(hostname, ip, family, port)
33
+				@hostname, @ip, @family, @port = hostname, ip, family, port
34 34
 				@dh                            = []
35 35
 
36 36
 				@name = "#@ip:#@port"
@@ -412,6 +412,7 @@ module CryptCheck
412 412
 				# Then, filter cert to keep uniq fingerprint
413 413
 				@certs = certs.uniq { |c| c.fingerprint }
414 414
 
415
+				@trusted = @valid = true
415 416
 				@certs.each do |cert|
416 417
 					key      = cert.key
417 418
 					identity = cert.valid?(@hostname || @ip)
@@ -422,11 +423,13 @@ module CryptCheck
422 423
 						Logger.info { '    Identity : ' + 'valid'.colorize(:good) }
423 424
 					else
424 425
 						Logger.info { '    Identity : ' + 'invalid'.colorize(:error) }
426
+						@valid = false
425 427
 					end
426 428
 					if trust == :trusted
427 429
 						Logger.info { '    Trust : ' + 'trusted'.colorize(:good) }
428 430
 					else
429 431
 						Logger.info { '    Trust : ' + 'untrusted'.colorize(:error) + ' - ' + trust }
432
+						@trusted = false
430 433
 					end
431 434
 				end
432 435
 				@keys = @certs.collect &:key

+ 52
- 44
lib/cryptcheck/tls/grade.rb View File

@@ -1,16 +1,17 @@
1 1
 module CryptCheck
2 2
 	module Tls
3 3
 		class Grade
4
-			attr_reader :server, :grade, :status
4
+			attr_reader :server, :grade
5 5
 
6 6
 			def initialize(server)
7 7
 				@server = server
8
-				@status = @server.status
9 8
 				@checks = checks
10
-				@grade  = calculate_grade
11
-			end
9
+				@states = @server.states
10
+				Logger.info { '' }
11
+				Logger.ap :checks, @checks
12
+				Logger.ap :states, @states
13
+				@grade = calculate_grade
12 14
 
13
-			def display
14 15
 				color = case @grade
15 16
 							when 'A', 'A+'
16 17
 								:best
@@ -24,76 +25,83 @@ module CryptCheck
24 25
 								:error
25 26
 							when 'G'
26 27
 								:critical
27
-							when 'M', 'T'
28
+							when 'T', 'V'
28 29
 								:unknown
29 30
 						end
30 31
 
31 32
 				Logger.info { "Grade : #{self.grade.colorize color }" }
32
-				Logger.info { '' }
33
-				State.each do |color|
34
-					states = @status[color]
35
-					Logger.info { "#{color.to_s.capitalize} : #{states.collect { |s| s.to_s.colorize color }.join ' '}" } unless states.empty?
36
-				end
37 33
 			end
38 34
 
39 35
 			private
40 36
 			CHECKS = {
41
-					critical: %i(
42
-						mdc2_sign md2_sign md4_sign md5_sign sha_sign sha1_sign
37
+					best:     %i(
38
+
39
+							  ),
40
+					perfect:  %i(
41
+						tlsv1_2_only
42
+						pfs_only
43
+						ecdhe_only
44
+					),
45
+					good:     %i(
46
+						tlsv1_2
47
+						pfs
48
+						ecdhe
49
+						aead
50
+					),
51
+					warning:  %i(
43 52
 						weak_key
44 53
 						weak_dh
45
-						sslv2 sslv3
54
+						dhe
46 55
 					),
47 56
 					error:    %i(
48 57
 						weak_key
49 58
 						weak_dh
50 59
 					),
51
-					warning:  %i(
60
+					critical: %i(
61
+						mdc2_sign md2_sign md4_sign md5_sign sha_sign sha1_sign
52 62
 						weak_key
53 63
 						weak_dh
54
-						dhe
55
-					),
56
-					good:     %i(
57
-						tls12
58
-					),
59
-					perfect:  %i(
60
-						tls12_only
64
+						sslv2 sslv3
61 65
 					),
62
-					best:     %i(
63
-
64
-							  )
65 66
 			}.freeze
66 67
 
67 68
 			def checks
68
-
69
+				CHECKS
69 70
 			end
70 71
 
71 72
 			def calculate_grade
73
+				return 'V' unless @server.valid?
74
+				return 'T' unless @server.trusted?
75
+
72 76
 				case
73
-					when !@status[:critical].empty?
77
+					when !@states[:critical].empty?
74 78
 						return 'G'
75
-					when !@status[:error].empty?
79
+					when !@states[:error].empty?
76 80
 						return 'F'
77
-					when !@status[:warning].empty?
81
+					when !@states[:warning].empty?
78 82
 						return 'E'
79 83
 				end
80 84
 
81
-				goods = @checks.select { |c| c.last == :good }.collect &:first
82
-				unless goods.empty?
83
-					return 'D' if @status[:good].empty?
84
-					return 'C' if @status[:good] != goods
85
-				end
86
-
87
-				perfects = @checks.select { |c| c.last == :perfect }.collect &:first
88
-				unless perfects.empty?
89
-					return 'C+' if @status[:perfect].empty?
90
-					return 'B' if @status[:perfect] != perfects
91
-				end
85
+				[[:good, 'D', 'C'],
86
+				 [:perfect, 'C', 'B'],
87
+				 [:best, 'B', 'A']].each do |type, score1, score2|
88
+					expected = @checks[type]
89
+					unless expected.empty?
90
+						available = @states[type]
91
+						return score1 if available.empty?
92
+						missed = expected - available
93
+						unless missed.empty?
94
+							Logger.info { "Missing #{type} : #{missed}" }
95
+							return score2
96
+						end
92 97
 
93
-				bests = @checks.select { |c| c.last == :best }.collect &:first
94
-				unless bests.empty?
95
-					return 'B+' if @status[:best].empty?
96
-					return 'A' if @status[:best] != bests
98
+						# I'm not error prone. The code yes.
99
+						additional = available - expected
100
+						unless additional.empty?
101
+							Logger.fatal { "Developper missed #{type} : #{additional}".colorize :critical }
102
+							exit -1
103
+						end
104
+					end
97 105
 				end
98 106
 
99 107
 				'A+'

+ 8
- 0
lib/cryptcheck/tls/https/grade.rb View File

@@ -2,6 +2,14 @@ module CryptCheck
2 2
 	module Tls
3 3
 		module Https
4 4
 			class Grade < Tls::Grade
5
+				CHECKS = {
6
+						good:    %i(hsts),
7
+						perfect: %i(hsts_long)
8
+				}
9
+
10
+				def checks
11
+					State.merge super, CHECKS
12
+				end
5 13
 			end
6 14
 		end
7 15
 	end

+ 8
- 0
lib/cryptcheck/tls/server.rb View File

@@ -53,6 +53,14 @@ module CryptCheck
53 53
 				@cert.extensions.any? { |e| e.oid == '1.3.6.1.5.5.7.1.24' }
54 54
 			end
55 55
 
56
+			def valid?
57
+				@valid
58
+			end
59
+
60
+			def trusted?
61
+				@trusted
62
+			end
63
+
56 64
 			include State
57 65
 
58 66
 			CHECKS = [

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

@@ -2,6 +2,12 @@ module CryptCheck
2 2
 	module Tls
3 3
 		module Smtp
4 4
 			class Grade < Tls::Grade
5
+				CHECKS = {
6
+				}
7
+
8
+				def checks
9
+					State.merge super, CHECKS
10
+				end
5 11
 			end
6 12
 		end
7 13
 	end

+ 0
- 3
lib/cryptcheck/tls/xmpp.rb View File

@@ -1,6 +1,3 @@
1
-require 'erb'
2
-require 'parallel'
3
-
4 1
 module CryptCheck
5 2
 	module Tls
6 3
 		module Xmpp

+ 5
- 3
lib/cryptcheck/tls/xmpp/grade.rb View File

@@ -2,10 +2,12 @@ module CryptCheck
2 2
 	module Tls
3 3
 		module Xmpp
4 4
 			class Grade < Tls::Grade
5
+				CHECKS = {
6
+						good: %i(required)
7
+				}
8
+
5 9
 				def checks
6
-					super + [
7
-							[:required, Proc.new { |s| s.required? }, :good],
8
-					]
10
+					State.merge super, CHECKS
9 11
 				end
10 12
 			end
11 13
 		end

Loading…
Cancel
Save