You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

196 lines
4.5KB

  1. require 'colorize'
  2. require 'ipaddr'
  3. require 'timeout'
  4. require 'yaml'
  5. Dir[File.join __dir__, 'cryptcheck', 'fixtures', '*.rb'].each { |f| require f }
  6. module CryptCheck
  7. MAX_ANALYSIS_DURATION = 120
  8. PARALLEL_ANALYSIS = 10
  9. class AnalysisFailure
  10. attr_reader :error
  11. def initialize(error)
  12. @error = error
  13. end
  14. def to_s
  15. @error.to_s
  16. end
  17. end
  18. class NoTLSAvailableServer
  19. attr_reader :server
  20. def initialize(server)
  21. @server = OpenStruct.new hostname: server
  22. end
  23. def grade
  24. 'X'
  25. end
  26. def score
  27. 0
  28. end
  29. end
  30. autoload :Logger, 'cryptcheck/logger'
  31. autoload :Tls, 'cryptcheck/tls'
  32. module Tls
  33. autoload :Cipher, 'cryptcheck/tls/cipher'
  34. autoload :Server, 'cryptcheck/tls/server'
  35. autoload :TcpServer, 'cryptcheck/tls/server'
  36. autoload :UdpServer, 'cryptcheck/tls/server'
  37. autoload :Grade, 'cryptcheck/tls/grade'
  38. autoload :Https, 'cryptcheck/tls/https'
  39. module Https
  40. autoload :Server, 'cryptcheck/tls/https/server'
  41. autoload :Grade, 'cryptcheck/tls/https/grade'
  42. end
  43. autoload :Xmpp, 'cryptcheck/tls/xmpp'
  44. module Xmpp
  45. autoload :Server, 'cryptcheck/tls/xmpp/server'
  46. autoload :Grade, 'cryptcheck/tls/xmpp/grade'
  47. end
  48. autoload :Smtp, 'cryptcheck/tls/smtp'
  49. module Smtp
  50. autoload :Server, 'cryptcheck/tls/smtp/server'
  51. autoload :Grade, 'cryptcheck/tls/smtp/grade'
  52. end
  53. end
  54. autoload :Ssh, 'cryptcheck/ssh'
  55. module Ssh
  56. autoload :Packet, 'cryptcheck/ssh/packet'
  57. autoload :Server, 'cryptcheck/ssh/server'
  58. autoload :SshNotSupportedServer, 'cryptcheck/ssh/server'
  59. autoload :Grade, 'cryptcheck/ssh/grade'
  60. end
  61. private
  62. def self.addresses(host)
  63. begin
  64. begin
  65. ip = IPAddr.new host
  66. return [[ip.family, ip.to_s, nil]]
  67. rescue IPAddr::InvalidAddressError
  68. end
  69. ::Addrinfo.getaddrinfo(host, nil, nil, :STREAM)
  70. .collect { |a| [a.afamily, a.ip_address, host] }
  71. end.reject do |family, *_|
  72. (ENV['DISABLE_IPv6'] && family == Socket::AF_INET6) ||
  73. (ENV['DISABLE_IPv4'] && family == Socket::AF_INET)
  74. end
  75. end
  76. def self.analyze_address(host, family, ip, port, server, grade, *args, **kargs)
  77. a = [host, family, ip, port, *args]
  78. ::Timeout::timeout MAX_ANALYSIS_DURATION do
  79. s = if kargs.empty?
  80. server.new *a
  81. else
  82. server.new *a, **kargs
  83. end
  84. if grade
  85. g = grade.new s
  86. Logger.info { '' }
  87. g.display
  88. g
  89. else
  90. s
  91. end
  92. end
  93. rescue ::Timeout::Error
  94. e = "Too long analysis (max #{MAX_ANALYSIS_DURATION.humanize})"
  95. Logger.error e
  96. AnalysisFailure.new e
  97. rescue => e
  98. Logger.error e
  99. AnalysisFailure.new e
  100. end
  101. def self.analyze_addresses(host, addresses, port, server, grade, *args, **kargs)
  102. first = true
  103. addresses.collect do |family, ip|
  104. first ? (first = false) : Logger.info { '' }
  105. key = [host, ip, port]
  106. result = analyze_address host, family, ip, port, server, grade, *args, **kargs
  107. [key, result]
  108. end.to_h
  109. end
  110. def self.analyze(host, port, server, grade, *args, **kargs)
  111. addresses = begin
  112. addresses host
  113. rescue ::SocketError => e
  114. Logger.error e
  115. key = [host, nil, port]
  116. error = AnalysisFailure.new "Unable to resolve #{host}"
  117. return { key => error }
  118. end
  119. analyze_addresses host, addresses, port, server, grade, *args, **kargs
  120. end
  121. def self.analyze_hosts(hosts, template, output, groups: nil, &block)
  122. results = {}
  123. semaphore = ::Mutex.new
  124. ::Parallel.each hosts, progress: 'Analysing', in_threads: PARALLEL_ANALYSIS, finish: lambda { |item, _, _| puts item[1] } do |description, host|
  125. #hosts.each do |description, host|
  126. result = block.call host.strip
  127. result = result.values.first
  128. result = NoTLSAvailableServer.new(host) if result.is_a? AnalysisFailure
  129. semaphore.synchronize do
  130. if results.include? description
  131. results[description] << result
  132. else
  133. results[description] = [result]
  134. end
  135. end
  136. end
  137. results = ::Hash[groups.collect { |g| [g, results[g]] }] if groups
  138. results.each do |d, _|
  139. results[d].sort! do |a, b|
  140. cmp = score(a) <=> score(b)
  141. if cmp.zero?
  142. cmp = b.score <=> a.score
  143. if cmp.zero?
  144. cmp = a.server.hostname <=> b.server.hostname
  145. end
  146. end
  147. cmp
  148. end
  149. end
  150. ::File.write output, ::ERB.new(::File.read template).result(binding)
  151. end
  152. def self.analyze_file(input, template, output, &block)
  153. config = ::YAML.load_file input
  154. hosts = []
  155. groups = []
  156. config.each do |c|
  157. d, hs = c['description'], c['hostnames']
  158. groups << d
  159. hs.each { |host| hosts << [d, host] }
  160. end
  161. self.analyze_hosts hosts, template, output, groups: groups, &block
  162. end
  163. private
  164. SCORES = %w(A+ A A- B C D E F T M X)
  165. def self.score(a)
  166. SCORES.index a.grade
  167. end
  168. end