ssh_scan 0.0.21 → 0.0.22

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 504c40725362aee1beeb3795b3ea1fc7eb8af8a8
4
- data.tar.gz: fcd896ac346c21474fab15b925562ec5a7c6c3e6
3
+ metadata.gz: a161adf0202c747f948c9161675b567c6b715b4f
4
+ data.tar.gz: 5533118de4e0f7ef6eb26bc60adb915ffbde9cae
5
5
  SHA512:
6
- metadata.gz: ed59a66f3da720810d4834c4128c1e3b5effd68e572d46664a5d4151f89d5cc95f9a0094cdc69409d5413b1f643362fcf23289a1a9cfd4fc7f2857ffd31d199e
7
- data.tar.gz: e7a761c9eb05247ec41d1fa8acb0b11d7265636934c7b5d76a940d7541212fe6a3675686ee4e8055d8f9fe3fe91d0e080ddd3e7c6cdc444ae7c589f987937879
6
+ metadata.gz: 1c7f1fc49e71437001aa5e6cd8f8d63cadc127dde727d469e6d94002533a3f525eb69ce364d40dcf28cce05dcaf17dcb757bb51615a36f5b6497d6a289d6529d
7
+ data.tar.gz: 654a3c0ec780433fadd09db4bb0eb77f18bc60dc088c28e6073a8876d2f3fe717dd568180347bcc31ce8834fd5e30c4b83a19cff8ba01979587f16f40aacc403
data/.travis.yml CHANGED
@@ -34,6 +34,14 @@ matrix:
34
34
  - bundle install
35
35
  - chmod 755 ./spec/ssh_scan/integration.sh
36
36
  - ./spec/ssh_scan/integration.sh
37
+ - rvm: 2.3.0
38
+ env:
39
+ - LABEL=docker_integration_tests
40
+ services:
41
+ - docker
42
+ script:
43
+ - docker build -t mozilla/ssh_scan .
44
+ - docker run -it mozilla/ssh_scan /app/spec/ssh_scan/integration.sh
37
45
  - rvm: 2.3.0
38
46
  env:
39
47
  - LABEL=docker_build_push
@@ -42,7 +50,7 @@ matrix:
42
50
  script:
43
51
  - docker build -t mozilla/ssh_scan .
44
52
  - >
45
- if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then \
53
+ if [ [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "master" ] ]; then \
46
54
  docker login -u="$DOCKER_USER" -p="$DOCKER_PASS" ;\
47
55
  docker push mozilla/ssh_scan:latest ;\
48
56
  else \
data/README.md CHANGED
@@ -28,7 +28,7 @@ To run from a docker container, type:
28
28
 
29
29
  ```bash
30
30
  docker pull mozilla/ssh_scan
31
- docker run -it mozilla/ssh_scan /app/bin/ssh_scan -t github.com
31
+ docker run -it mozilla/ssh_scan /app/bin/ssh_scan -t sshscan.rubidus.com
32
32
  ```
33
33
 
34
34
  To install and run from source, type:
@@ -38,17 +38,6 @@ To install and run from source, type:
38
38
  git clone https://github.com/mozilla/ssh_scan.git
39
39
  cd ssh_scan
40
40
 
41
- # install rvm,
42
- # you might have to provide root to install missing packages
43
- gpg2 --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
44
- curl -sSL https://get.rvm.io | bash -s stable
45
-
46
- # install Ruby 2.3.1 with rvm,
47
- # again, you might have to install missing devel packages
48
- rvm install 2.3.1
49
- rvm use 2.3.1
50
-
51
- # resolve dependencies
52
41
  gem install bundler
53
42
  bundle install
54
43
 
@@ -60,7 +49,7 @@ bundle install
60
49
  Run `ssh_scan -h` to get this
61
50
 
62
51
  ```bash
63
- ssh_scan v0.0.17 (https://github.com/mozilla/ssh_scan)
52
+ ssh_scan v0.0.21 (https://github.com/mozilla/ssh_scan)
64
53
 
65
54
  Usage: ssh_scan [options]
66
55
  -t, --target [IP/Range/Hostname] IP/Ranges/Hostname to scan
data/bin/ssh_scan CHANGED
@@ -261,8 +261,7 @@ puts JSON.pretty_generate(results)
261
261
 
262
262
  if options["unit_test"] == true
263
263
  results.each do |result|
264
- if result["compliance"] &&
265
- result["compliance"][:compliant] == false
264
+ if result.compliant == false
266
265
  exit 1 #non-zero means a false
267
266
  else
268
267
  exit 0 #non-zero means pass
@@ -12,8 +12,5 @@ encryption:
12
12
  macs:
13
13
  - hmac-sha2-512
14
14
  - hmac-sha2-256
15
- compression:
16
- - none
17
- - zlib@openssh.com
18
15
  references:
19
16
  - https://wiki.mozilla.org/Security/Guidelines/OpenSSH
@@ -23,8 +23,5 @@ macs:
23
23
  - hmac-sha2-512
24
24
  - hmac-sha2-256
25
25
  - umac-128@openssh.com
26
- compression:
27
- - none
28
- - zlib@openssh.com
29
26
  references:
30
27
  - https://wiki.mozilla.org/Security/Guidelines/OpenSSH
data/lib/ssh_scan.rb CHANGED
@@ -13,6 +13,8 @@ require 'ssh_scan/scan_engine'
13
13
  require 'ssh_scan/target_parser'
14
14
  require 'ssh_scan/update'
15
15
  require 'ssh_scan/fingerprint_database'
16
+ require 'ssh_scan/grader'
17
+ require 'ssh_scan/result'
16
18
 
17
19
  #Monkey Patches
18
20
  require 'string_ext'
@@ -21,8 +21,8 @@ module SSHScan
21
21
  # or "SSH-number" then return the number, else return
22
22
  # "unknown"
23
23
  def ssh_version()
24
- if version = @string.match(/SSH-(\d+[\.\d+]+)/)[1]
25
- return version.to_f
24
+ if match = @string.match(/SSH-(\d+[\.\d+]+)/)
25
+ return match[1].to_f
26
26
  else
27
27
  return "unknown"
28
28
  end
@@ -6,19 +6,36 @@ require 'ssh_scan/error'
6
6
 
7
7
  module SSHScan
8
8
  class Client
9
- def initialize(target, port, timeout = 3)
10
- @target = target
9
+ def initialize(ip, port, timeout = 3)
10
+ @ip = ip
11
11
  @timeout = timeout
12
12
 
13
- @port = port
13
+ @port = port.to_i
14
14
  @client_banner = SSHScan::Constants::DEFAULT_CLIENT_BANNER
15
15
  @server_banner = nil
16
16
  @kex_init_raw = SSHScan::Constants::DEFAULT_KEY_INIT.to_binary_s
17
17
  end
18
18
 
19
+ def ip
20
+ @ip
21
+ end
22
+
23
+ def port
24
+ @port
25
+ end
26
+
27
+ def banner
28
+ @server_banner
29
+ end
30
+
19
31
  def connect()
32
+ @error = nil
33
+
20
34
  begin
21
- @sock = Socket.tcp(@target, @port, connect_timeout: @timeout)
35
+ @sock = Socket.tcp(@ip, @port, connect_timeout: @timeout)
36
+ rescue SocketError => e
37
+ @error = SSHScan::Error::ConnectionRefused.new(e.message)
38
+ @sock = nil
22
39
  rescue Errno::ETIMEDOUT => e
23
40
  @error = SSHScan::Error::ConnectTimeout.new(e.message)
24
41
  @sock = nil
@@ -53,58 +70,53 @@ module SSHScan
53
70
  end
54
71
  end
55
72
 
56
- def get_kex_result(kex_init_raw = @kex_init_raw)
57
- # Common options for all cases
58
- result = {}
59
- result[:ssh_scan_version] = SSHScan::VERSION
60
- result[:ip] = @target
61
- result[:port] = @port
73
+ def error?
74
+ !@error.nil?
75
+ end
62
76
 
77
+ def error
78
+ @error
79
+ end
80
+
81
+ def get_kex_result(kex_init_raw = @kex_init_raw)
63
82
  if !@sock
64
- result[:error] = @error
65
- return result
83
+ @error = "Socket is no longer valid"
84
+ return nil
66
85
  end
67
86
 
68
- # Assemble and print results
69
- result[:server_banner] = @server_banner.to_s
70
- result[:ssh_version] = @server_banner.ssh_version
71
- result[:os] = @server_banner.os_guess.common
72
- result[:os_cpe] = @server_banner.os_guess.cpe
73
- result[:ssh_lib] = @server_banner.ssh_lib_guess.common
74
- result[:ssh_lib_cpe] = @server_banner.ssh_lib_guess.cpe
75
-
76
87
  begin
77
88
  @sock.write(kex_init_raw)
78
89
  resp = @sock.read(4)
79
90
 
80
91
  if resp.nil?
81
- result[:error] = SSHScan::Error::NoKexResponse.new(
92
+ @error = SSHScan::Error::NoKexResponse.new(
82
93
  "service did not respond to our kex init request"
83
94
  )
84
95
  @sock = nil
85
- return result
96
+ return nil
86
97
  end
87
98
 
88
99
  resp += @sock.read(resp.unpack("N").first)
89
100
  @sock.close
90
101
 
91
102
  kex_exchange_init = SSHScan::KeyExchangeInit.read(resp)
92
- result.merge!(kex_exchange_init.to_hash)
93
103
  rescue Errno::ETIMEDOUT => e
94
104
  @error = SSHScan::Error::ConnectTimeout.new(e.message)
95
105
  @sock = nil
106
+ return nil
96
107
  rescue Errno::ECONNREFUSED,
97
108
  Errno::ENETUNREACH,
98
109
  Errno::ECONNRESET,
99
110
  Errno::EACCES,
100
111
  Errno::EHOSTUNREACH
101
- result[:error] = SSHScan::Error::NoKexResponse.new(
112
+ @error = SSHScan::Error::NoKexResponse.new(
102
113
  "service did not respond to our kex init request"
103
114
  )
104
115
  @sock = nil
116
+ return nil
105
117
  end
106
118
 
107
- return result
119
+ return kex_exchange_init.to_hash
108
120
  end
109
121
  end
110
122
  end
@@ -0,0 +1,32 @@
1
+ module SSHScan
2
+ # A very crude means of translating # of compliance recommendations into a a grade
3
+ # Basic formula is 100 - (# of recommendations * 10)
4
+ class Grader
5
+ GRADE_MAP = {
6
+ 91..100 => "A",
7
+ 81..90 => "B",
8
+ 71..80 => "C",
9
+ 61..70 => "D",
10
+ 0..60 => "F",
11
+ }
12
+
13
+ def initialize(result)
14
+ @result = result
15
+ end
16
+
17
+ def grade
18
+ score = 100
19
+
20
+ if @result.compliance_recommendations.each do |recommendation|
21
+ score -= 10
22
+ end
23
+ end
24
+
25
+ GRADE_MAP.each do |score_range,grade|
26
+ if score_range.include?(score)
27
+ return grade
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -15,7 +15,7 @@ module SSHScan
15
15
  @compression = opts['compression'] || []
16
16
  @references = opts['references'] || []
17
17
  @auth_methods = opts['auth_methods'] || []
18
- @ssh_version = opts['ssh_version'] || false
18
+ @ssh_version = opts['ssh_version'] || nil
19
19
  end
20
20
 
21
21
  # Generate a {SSHScan::Policy} object from YAML file.
@@ -7,9 +7,10 @@ module SSHScan
7
7
  end
8
8
 
9
9
  def out_of_policy_encryption
10
+ return [] if @policy.encryption.empty?
10
11
  target_encryption =
11
- @result[:encryption_algorithms_client_to_server] |
12
- @result[:encryption_algorithms_server_to_client]
12
+ @result.encryption_algorithms_client_to_server |
13
+ @result.encryption_algorithms_server_to_client
13
14
  outliers = []
14
15
  target_encryption.each do |target_enc|
15
16
  outliers << target_enc unless @policy.encryption.include?(target_enc)
@@ -18,9 +19,10 @@ module SSHScan
18
19
  end
19
20
 
20
21
  def missing_policy_encryption
22
+ return [] if @policy.encryption.empty?
21
23
  target_encryption =
22
- @result[:encryption_algorithms_client_to_server] |
23
- @result[:encryption_algorithms_server_to_client]
24
+ @result.encryption_algorithms_client_to_server |
25
+ @result.encryption_algorithms_server_to_client
24
26
  outliers = []
25
27
  @policy.encryption.each do |encryption|
26
28
  if target_encryption.include?(encryption) == false
@@ -31,9 +33,10 @@ module SSHScan
31
33
  end
32
34
 
33
35
  def out_of_policy_macs
36
+ return [] if @policy.macs.empty?
34
37
  target_macs =
35
- @result[:mac_algorithms_server_to_client] |
36
- @result[:mac_algorithms_client_to_server]
38
+ @result.mac_algorithms_server_to_client |
39
+ @result.mac_algorithms_client_to_server
37
40
  outliers = []
38
41
  target_macs.each do |target_mac|
39
42
  outliers << target_mac unless @policy.macs.include?(target_mac)
@@ -42,9 +45,10 @@ module SSHScan
42
45
  end
43
46
 
44
47
  def missing_policy_macs
48
+ return [] if @policy.macs.empty?
45
49
  target_macs =
46
- @result[:mac_algorithms_server_to_client] |
47
- @result[:mac_algorithms_client_to_server]
50
+ @result.mac_algorithms_server_to_client |
51
+ @result.mac_algorithms_client_to_server
48
52
  outliers = []
49
53
 
50
54
  @policy.macs.each do |mac|
@@ -56,7 +60,8 @@ module SSHScan
56
60
  end
57
61
 
58
62
  def out_of_policy_kex
59
- target_kexs = @result[:key_algorithms]
63
+ return [] if @policy.kex.empty?
64
+ target_kexs = @result.key_algorithms
60
65
  outliers = []
61
66
  target_kexs.each do |target_kex|
62
67
  outliers << target_kex unless @policy.kex.include?(target_kex)
@@ -65,7 +70,8 @@ module SSHScan
65
70
  end
66
71
 
67
72
  def missing_policy_kex
68
- target_kex = @result[:key_algorithms]
73
+ return [] if @policy.kex.empty?
74
+ target_kex = @result.key_algorithms
69
75
  outliers = []
70
76
 
71
77
  @policy.kex.each do |kex|
@@ -77,9 +83,10 @@ module SSHScan
77
83
  end
78
84
 
79
85
  def out_of_policy_compression
86
+ return [] if @policy.compression.empty?
80
87
  target_compressions =
81
- @result[:compression_algorithms_server_to_client] |
82
- @result[:compression_algorithms_client_to_server]
88
+ @result.compression_algorithms_server_to_client |
89
+ @result.compression_algorithms_client_to_server
83
90
  outliers = []
84
91
  target_compressions.each do |target_compression|
85
92
  outliers << target_compression unless
@@ -89,9 +96,10 @@ module SSHScan
89
96
  end
90
97
 
91
98
  def missing_policy_compression
99
+ return [] if @policy.compression.empty?
92
100
  target_compressions =
93
- @result[:compression_algorithms_server_to_client] |
94
- @result[:compression_algorithms_client_to_server]
101
+ @result.compression_algorithms_server_to_client |
102
+ @result.compression_algorithms_client_to_server
95
103
  outliers = []
96
104
 
97
105
  @policy.compression.each do |compression|
@@ -103,9 +111,9 @@ module SSHScan
103
111
  end
104
112
 
105
113
  def out_of_policy_auth_methods
106
- return [] if @result["auth_methods"].nil?
107
-
108
- target_auth_methods = @result["auth_methods"]
114
+ return [] if @policy.auth_methods.empty?
115
+ return [] if @result.auth_methods.empty?
116
+ target_auth_methods = @result.auth_methods
109
117
  outliers = []
110
118
 
111
119
  if not @policy.auth_methods.empty?
@@ -119,7 +127,8 @@ module SSHScan
119
127
  end
120
128
 
121
129
  def out_of_policy_ssh_version
122
- target_ssh_version = @result[:ssh_version]
130
+ return false if @policy.ssh_version.nil?
131
+ target_ssh_version = @result.ssh_version
123
132
  if @policy.ssh_version
124
133
  if target_ssh_version < @policy.ssh_version
125
134
  return true
@@ -0,0 +1,271 @@
1
+ require 'json'
2
+ require 'ssh_scan/banner'
3
+ require 'ipaddr'
4
+ require 'string_ext'
5
+ require 'set'
6
+
7
+ module SSHScan
8
+ class Result
9
+ def initialize()
10
+ @version = SSHScan::VERSION
11
+ @fingerprints = nil
12
+ @duplicate_host_key_ips = Set.new()
13
+ @compliance = {}
14
+ end
15
+
16
+ def version
17
+ @version
18
+ end
19
+
20
+ def ip
21
+ @ip
22
+ end
23
+
24
+ def ip=(ip)
25
+ unless ip.is_a?(String) && ip.ip_addr?
26
+ raise ArgumentError, "Invalid attempt to set IP to a non-IP address value"
27
+ end
28
+
29
+ @ip = ip
30
+ end
31
+
32
+ def port
33
+ @port
34
+ end
35
+
36
+ def port=(port)
37
+ unless port.is_a?(Integer) && port > 0 && port <= 65535
38
+ raise ArgumentError, "Invalid attempt to set port to a non-port value"
39
+ end
40
+
41
+ @port = port
42
+ end
43
+
44
+ def banner()
45
+ @banner || SSHScan::Banner.new("")
46
+ end
47
+
48
+ def hostname=(hostname)
49
+ @hostname = hostname
50
+ end
51
+
52
+ def hostname()
53
+ @hostname || ""
54
+ end
55
+
56
+ def banner=(banner)
57
+ unless banner.is_a?(SSHScan::Banner)
58
+ raise ArgumentError, "Invalid attempt to set banner with a non-banner object"
59
+ end
60
+
61
+ @banner = banner
62
+ end
63
+
64
+ def ssh_version
65
+ self.banner.ssh_version
66
+ end
67
+
68
+ def os_guess_common
69
+ self.banner.os_guess.common
70
+ end
71
+
72
+ def os_guess_cpe
73
+ self.banner.os_guess.cpe
74
+ end
75
+
76
+ def ssh_lib_guess_common
77
+ self.banner.ssh_lib_guess.common
78
+ end
79
+
80
+ def ssh_lib_guess_cpe
81
+ self.banner.ssh_lib_guess.cpe
82
+ end
83
+
84
+ def cookie
85
+ @cookie || ""
86
+ end
87
+
88
+ def key_algorithms
89
+ @hex_result_hash ? @hex_result_hash[:key_algorithms] : []
90
+ end
91
+
92
+ def server_host_key_algorithms
93
+ @hex_result_hash ? @hex_result_hash[:server_host_key_algorithms] : []
94
+ end
95
+
96
+ def encryption_algorithms_client_to_server
97
+ @hex_result_hash ? @hex_result_hash[:encryption_algorithms_client_to_server] : []
98
+ end
99
+
100
+ def encryption_algorithms_server_to_client
101
+ @hex_result_hash ? @hex_result_hash[:encryption_algorithms_server_to_client] : []
102
+ end
103
+
104
+ def mac_algorithms_client_to_server
105
+ @hex_result_hash ? @hex_result_hash[:mac_algorithms_client_to_server] : []
106
+ end
107
+
108
+ def mac_algorithms_server_to_client
109
+ @hex_result_hash ? @hex_result_hash[:mac_algorithms_server_to_client] : []
110
+ end
111
+
112
+ def compression_algorithms_client_to_server
113
+ @hex_result_hash ? @hex_result_hash[:compression_algorithms_client_to_server] : []
114
+ end
115
+
116
+ def compression_algorithms_server_to_client
117
+ @hex_result_hash ? @hex_result_hash[:compression_algorithms_server_to_client] : []
118
+ end
119
+
120
+ def languages_client_to_server
121
+ @hex_result_hash ? @hex_result_hash[:languages_client_to_server] : []
122
+ end
123
+
124
+ def languages_server_to_client
125
+ @hex_result_hash ? @hex_result_hash[:languages_server_to_client] : []
126
+ end
127
+
128
+ def set_kex_result(kex_result)
129
+ @hex_result_hash = kex_result.to_hash
130
+ end
131
+
132
+ def set_start_time
133
+ @start_time = Time.now
134
+ end
135
+
136
+ def start_time
137
+ @start_time
138
+ end
139
+
140
+ def set_end_time
141
+ @end_time = Time.now
142
+ end
143
+
144
+ def scan_duration
145
+ if start_time.nil?
146
+ raise "Cannot calculate scan duration without start_time set"
147
+ end
148
+
149
+ if end_time.nil?
150
+ raise "Cannot calculate scan duration without end_time set"
151
+ end
152
+
153
+ end_time - start_time
154
+ end
155
+
156
+ def end_time
157
+ @end_time
158
+ end
159
+
160
+ def auth_methods=(auth_methods)
161
+ @auth_methods = auth_methods
162
+ end
163
+
164
+ def fingerprints=(fingerprints)
165
+ @fingerprints = fingerprints
166
+ end
167
+
168
+ def fingerprints
169
+ @fingerprints
170
+ end
171
+
172
+ def duplicate_host_key_ips=(duplicate_host_key_ips)
173
+ @duplicate_host_key_ips = duplicate_host_key_ips
174
+ end
175
+
176
+ def duplicate_host_key_ips
177
+ @duplicate_host_key_ips
178
+ end
179
+
180
+ def auth_methods()
181
+ @auth_methods || []
182
+ end
183
+
184
+ def set_compliance=(compliance)
185
+ @compliance = compliance
186
+ end
187
+
188
+ def compliance_policy
189
+ @compliance[:policy]
190
+ end
191
+
192
+ def compliant?
193
+ @compliance[:compliant]
194
+ end
195
+
196
+ def compliance_references
197
+ @compliance[:references]
198
+ end
199
+
200
+ def compliance_recommendations
201
+ @compliance[:recommendations]
202
+ end
203
+
204
+ def set_client_attributes(client)
205
+ self.ip = client.ip
206
+ self.port = client.port || 22
207
+ self.banner = client.banner || SSHScan::Banner.new("")
208
+ end
209
+
210
+ def error=(error)
211
+ @error = error.to_s
212
+ end
213
+
214
+ def error?
215
+ !@error.nil?
216
+ end
217
+
218
+ def error
219
+ @error
220
+ end
221
+
222
+ def grade=(grade)
223
+ @compliance_grade = grade
224
+ end
225
+
226
+ def grade
227
+ @compliance_grade
228
+ end
229
+
230
+ def to_hash
231
+ hashed_object = {
232
+ "ssh_scan_version" => self.version,
233
+ "ip" => self.ip,
234
+ "hostname" => self.hostname,
235
+ "port" => self.port,
236
+ "server_banner" => self.banner.to_s,
237
+ "ssh_version" => self.ssh_version,
238
+ "os" => self.os_guess_common,
239
+ "os_cpe" => self.os_guess_cpe,
240
+ "ssh_lib" => self.ssh_lib_guess_common,
241
+ "ssh_lib_cpe" => self.ssh_lib_guess_cpe,
242
+ "key_algorithms" => self.key_algorithms,
243
+ "encryption_algorithms_client_to_server" => self.encryption_algorithms_client_to_server,
244
+ "encryption_algorithms_server_to_client" => self.encryption_algorithms_server_to_client,
245
+ "mac_algorithms_client_to_server" => self.mac_algorithms_client_to_server,
246
+ "mac_algorithms_server_to_client" => self.mac_algorithms_server_to_client,
247
+ "compression_algorithms_client_to_server" => self.compression_algorithms_client_to_server,
248
+ "compression_algorithms_server_to_client" => self.compression_algorithms_server_to_client,
249
+ "languages_client_to_server" => self.languages_client_to_server,
250
+ "languages_server_to_client" => self.languages_server_to_client,
251
+ "auth_methods" => self.auth_methods,
252
+ "fingerprints" => self.fingerprints,
253
+ "duplicate_host_key_ips" => self.duplicate_host_key_ips,
254
+ "compliance" => @compliance,
255
+ "start_time" => self.start_time,
256
+ "end_time" => self.end_time,
257
+ "scan_duration_seconds" => self.scan_duration,
258
+ }
259
+
260
+ if self.error?
261
+ hashed_object.error = self.error
262
+ end
263
+
264
+ hashed_object
265
+ end
266
+
267
+ def to_json
268
+ self.to_hash.to_json
269
+ end
270
+ end
271
+ end
@@ -4,6 +4,7 @@ require 'ssh_scan/crypto'
4
4
  #require 'ssh_scan/fingerprint_database'
5
5
  require 'net/ssh'
6
6
  require 'logger'
7
+ require 'open3'
7
8
 
8
9
  module SSHScan
9
10
  # Handle scanning of targets.
@@ -19,41 +20,65 @@ module SSHScan
19
20
  port = 22
20
21
  end
21
22
  timeout = opts["timeout"]
22
- result = []
23
+
24
+ result = SSHScan::Result.new()
25
+ result.port = port.to_i
23
26
 
24
- start_time = Time.now
27
+ # Start the scan timer
28
+ result.set_start_time
25
29
 
26
30
  if target.fqdn?
31
+ result.hostname = target
32
+
33
+ # If doesn't resolve as IPv6, we'll try IPv4
27
34
  if target.resolve_fqdn_as_ipv6.nil?
28
35
  client = SSHScan::Client.new(
29
36
  target.resolve_fqdn_as_ipv4.to_s, port, timeout
30
37
  )
31
38
  client.connect()
32
- result = client.get_kex_result()
33
- result[:hostname] = target
34
- return result if result.include?(:error)
39
+ result.set_client_attributes(client)
40
+ kex_result = client.get_kex_result()
41
+ result.set_kex_result(kex_result) unless kex_result.nil?
42
+ result.error = client.error if client.error?
43
+ # If it does resolve as IPv6, we're try IPv6
35
44
  else
36
45
  client = SSHScan::Client.new(
37
46
  target.resolve_fqdn_as_ipv6.to_s, port, timeout
38
47
  )
39
48
  client.connect()
40
- result = client.get_kex_result()
41
- if result.include?(:error)
49
+ result.set_client_attributes(client)
50
+ kex_result = client.get_kex_result()
51
+ result.set_kex_result(kex_result) unless kex_result.nil?
52
+ result.error = client.error if client.error?
53
+
54
+ # If resolves as IPv6, but somehow we get an client error, fall-back to IPv4
55
+ if result.error?
42
56
  client = SSHScan::Client.new(
43
57
  target.resolve_fqdn_as_ipv4.to_s, port, timeout
44
58
  )
45
59
  client.connect()
46
- result = client.get_kex_result()
47
- result[:hostname] = target
48
- return result if result.include?(:error)
60
+ result.set_client_attributes(client)
61
+ kex_result = client.get_kex_result()
62
+ result.set_kex_result(kex_result) unless kex_result.nil?
63
+ result.error = client.error if client.error?
49
64
  end
50
65
  end
51
66
  else
52
67
  client = SSHScan::Client.new(target, port, timeout)
53
68
  client.connect()
54
- result = client.get_kex_result()
55
- result[:hostname] = target.resolve_ptr
56
- return result if result.include?(:error)
69
+ result.set_client_attributes(client)
70
+ kex_result = client.get_kex_result()
71
+ result.set_kex_result(kex_result)
72
+
73
+ # Attempt to suppliment a hostname that wasn't provided
74
+ result.hostname = target.resolve_ptr
75
+
76
+ result.error = client.error if client.error?
77
+ end
78
+
79
+ if result.error?
80
+ result.set_end_time
81
+ return result
57
82
  end
58
83
 
59
84
  # Connect and get results (Net-SSH)
@@ -69,57 +94,54 @@ module SSHScan
69
94
  net_ssh_session, :auth_methods => ["none"]
70
95
  )
71
96
  auth_session.authenticate("none", "test", "test")
72
- result['auth_methods'] = auth_session.allowed_auth_methods
97
+ result.auth_methods = auth_session.allowed_auth_methods
73
98
  net_ssh_session.close
74
99
  rescue Net::SSH::ConnectionTimeout => e
75
- result[:error] = e
76
- result[:error] = SSHScan::Error::ConnectTimeout.new(e.message)
100
+ result.error = SSHScan::Error::ConnectTimeout.new(e.message)
77
101
  rescue Net::SSH::Disconnect => e
78
- result[:error] = e
79
- result[:error] = SSHScan::Error::Disconnected.new(e.message)
102
+ result.error = SSHScan::Error::Disconnected.new(e.message)
80
103
  rescue Net::SSH::Exception => e
81
104
  if e.to_s.match(/could not settle on/)
82
- result[:error] = e
105
+ result.error = e
83
106
  else
84
107
  raise e
85
108
  end
86
- else
87
- result['fingerprints'] = {}
88
- host_keys = `ssh-keyscan -t rsa,dsa #{target} 2>/dev/null`.split
89
- host_keys_len = host_keys.length - 1
90
-
91
- for i in 0..host_keys_len
92
- if host_keys[i].eql? "ssh-dss"
93
- pkey = SSHScan::Crypto::PublicKey.new(host_keys[i + 1])
94
- result['fingerprints'].merge!({
95
- "dsa" => {
96
- "known_bad" => pkey.bad_key?.to_s,
97
- "md5" => pkey.fingerprint_md5,
98
- "sha1" => pkey.fingerprint_sha1,
99
- "sha256" => pkey.fingerprint_sha256,
100
- }
101
- })
102
- end
109
+ end
103
110
 
104
- if host_keys[i].eql? "ssh-rsa"
105
- pkey = SSHScan::Crypto::PublicKey.new(host_keys[i + 1])
106
- result['fingerprints'].merge!({
107
- "rsa" => {
108
- "known_bad" => pkey.bad_key?.to_s,
109
- "md5" => pkey.fingerprint_md5,
110
- "sha1" => pkey.fingerprint_sha1,
111
- "sha256" => pkey.fingerprint_sha256,
112
- }
113
- })
114
- end
111
+ # Figure out what rsa or dsa fingerprints exist
112
+ fingerprints = {}
113
+
114
+ host_keys = `ssh-keyscan -t rsa,dsa #{target} 2>/dev/null`.split
115
+ host_keys_len = host_keys.length - 1
116
+
117
+ for i in 0..host_keys_len
118
+ if host_keys[i].eql? "ssh-dss"
119
+ pkey = SSHScan::Crypto::PublicKey.new(host_keys[i + 1])
120
+ fingerprints.merge!({
121
+ "dsa" => {
122
+ "known_bad" => pkey.bad_key?.to_s,
123
+ "md5" => pkey.fingerprint_md5,
124
+ "sha1" => pkey.fingerprint_sha1,
125
+ "sha256" => pkey.fingerprint_sha256,
126
+ }
127
+ })
128
+ end
129
+
130
+ if host_keys[i].eql? "ssh-rsa"
131
+ pkey = SSHScan::Crypto::PublicKey.new(host_keys[i + 1])
132
+ fingerprints.merge!({
133
+ "rsa" => {
134
+ "known_bad" => pkey.bad_key?.to_s,
135
+ "md5" => pkey.fingerprint_md5,
136
+ "sha1" => pkey.fingerprint_sha1,
137
+ "sha256" => pkey.fingerprint_sha256,
138
+ }
139
+ })
115
140
  end
116
141
  end
117
142
 
118
- # Add scan times
119
- end_time = Time.now
120
- result['start_time'] = start_time.to_s
121
- result['end_time'] = end_time.to_s
122
- result['scan_duration_seconds'] = end_time - start_time
143
+ result.fingerprints = fingerprints
144
+ result.set_end_time
123
145
 
124
146
  return result
125
147
  end
@@ -156,13 +178,14 @@ module SSHScan
156
178
  opts['fingerprint_database']
157
179
  )
158
180
  results.each do |result|
159
- fingerprint_db.clear_fingerprints(result[:ip])
160
- if result['fingerprints']
161
- result['fingerprints'].values.each do |host_key_algo|
181
+ fingerprint_db.clear_fingerprints(result.ip)
182
+
183
+ if result.fingerprints
184
+ result.fingerprints.values.each do |host_key_algo|
162
185
  host_key_algo.each do |fingerprint|
163
186
  key, value = fingerprint
164
187
  next if key == "known_bad"
165
- fingerprint_db.add_fingerprint(value, result[:ip])
188
+ fingerprint_db.add_fingerprint(value, result.ip)
166
189
  end
167
190
  end
168
191
  end
@@ -170,45 +193,48 @@ module SSHScan
170
193
 
171
194
  # Decorate all the results with duplicate keys
172
195
  results.each do |result|
173
- if result['fingerprints']
174
- ip = result[:ip]
175
- result['duplicate_host_key_ips'] = []
176
- result['fingerprints'].values.each do |host_key_algo|
196
+ if result.fingerprints
197
+ ip = result.ip
198
+ result.duplicate_host_key_ips = []
199
+ result.fingerprints.values.each do |host_key_algo|
177
200
  host_key_algo.each do |fingerprint|
178
201
  key, value = fingerprint
179
202
  next if key == "known_bad"
180
203
  fingerprint_db.find_fingerprints(value).each do |other_ip|
181
204
  next if ip == other_ip
182
- result['duplicate_host_key_ips'] << other_ip
205
+ result.duplicate_host_key_ips << other_ip
183
206
  end
184
207
  end
185
208
  end
186
- result['duplicate_host_key_ips'].uniq!
209
+ result.duplicate_host_key_ips
187
210
  end
188
211
  end
189
212
 
190
213
  # Decorate all the results with compliance information
191
214
  results.each do |result|
192
215
  # Do this only when we have all the information we need
193
- if !opts["policy"].nil? &&
194
- !result[:key_algorithms].nil? &&
195
- !result[:server_host_key_algorithms].nil? &&
196
- !result[:encryption_algorithms_client_to_server].nil? &&
197
- !result[:encryption_algorithms_server_to_client].nil? &&
198
- !result[:mac_algorithms_client_to_server].nil? &&
199
- !result[:mac_algorithms_server_to_client].nil? &&
200
- !result[:compression_algorithms_client_to_server].nil? &&
201
- !result[:compression_algorithms_server_to_client].nil? &&
202
- !result[:languages_client_to_server].nil? &&
203
- !result[:languages_server_to_client].nil?
216
+ if opts["policy"] &&
217
+ result.key_algorithms.any? &&
218
+ result.server_host_key_algorithms.any? &&
219
+ result.encryption_algorithms_client_to_server.any? &&
220
+ result.encryption_algorithms_server_to_client.any? &&
221
+ result.mac_algorithms_client_to_server.any? &&
222
+ result.mac_algorithms_server_to_client.any? &&
223
+ result.compression_algorithms_client_to_server.any? &&
224
+ result.compression_algorithms_server_to_client.any?
204
225
 
205
226
  policy = SSHScan::Policy.from_file(opts["policy"])
206
227
  policy_mgr = SSHScan::PolicyManager.new(result, policy)
207
- result['compliance'] = policy_mgr.compliance_results
228
+ result.set_compliance = policy_mgr.compliance_results
229
+
230
+ if result.compliance_policy
231
+ grader = SSHScan::Grader.new(result)
232
+ result.grade = grader.grade
233
+ end
208
234
  end
209
235
  end
210
236
 
211
- return results
237
+ return results.map {|r| r.to_hash}
212
238
  end
213
239
  end
214
240
  end
@@ -1,3 +1,3 @@
1
1
  module SSHScan
2
- VERSION = '0.0.21'
2
+ VERSION = '0.0.22'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ssh_scan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.21
4
+ version: 0.0.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Claudius
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2017-05-25 00:00:00.000000000 Z
15
+ date: 2017-06-08 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: bindata
@@ -351,6 +351,7 @@ files:
351
351
  - lib/ssh_scan/error/no_banner.rb
352
352
  - lib/ssh_scan/error/no_kex_response.rb
353
353
  - lib/ssh_scan/fingerprint_database.rb
354
+ - lib/ssh_scan/grader.rb
354
355
  - lib/ssh_scan/os.rb
355
356
  - lib/ssh_scan/os/centos.rb
356
357
  - lib/ssh_scan/os/cisco.rb
@@ -366,6 +367,7 @@ files:
366
367
  - lib/ssh_scan/policy.rb
367
368
  - lib/ssh_scan/policy_manager.rb
368
369
  - lib/ssh_scan/protocol.rb
370
+ - lib/ssh_scan/result.rb
369
371
  - lib/ssh_scan/scan_engine.rb
370
372
  - lib/ssh_scan/ssh_lib.rb
371
373
  - lib/ssh_scan/ssh_lib/ciscossh.rb