ssh_scan 0.0.21 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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