ssh_scan 0.0.40 → 0.0.41
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 +4 -4
- data/CONTRIBUTING.md +3 -3
- data/bin/ssh_scan +19 -4
- data/lib/ssh_scan/public_key.rb +3 -2
- data/lib/ssh_scan/result.rb +11 -2
- data/lib/ssh_scan/scan_engine.rb +10 -0
- data/lib/ssh_scan/ssh_fp.rb +47 -0
- data/lib/ssh_scan/tests/test_dns_key_verification.rb +43 -0
- data/lib/ssh_scan/version.rb +1 -1
- data/lib/string_ext.rb +25 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 30ae6fc2118a4c57d91680ce36dfb5f05f04887b27e1c29c11e87a19440a220c
|
4
|
+
data.tar.gz: 8bb716eafcb9ddedd8141466463c12a1d18323b68a7bb01bc7c6edb02fe899ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b54db17fe59790ef8bec162fb4bb2949ab087ff7738217263a1454ec220fae500ccb21da7776407d8196dee9fa1a729c1cc472de8a200d64cb99e87e63cb0f93
|
7
|
+
data.tar.gz: f8516f26b7066e50d237bec19c56c4ae81f2a007c04d1a06af4e524f900807bb5e8924a7bcc58be5c9d6cea7435430ba7f13a244038ca24c3af6dac1116b8ff5
|
data/CONTRIBUTING.md
CHANGED
@@ -27,9 +27,9 @@ inclusion, but sending a pull request is much more awesome.
|
|
27
27
|
|
28
28
|
If you want your pull requests to be accepted, please follow the following guidelines:
|
29
29
|
|
30
|
-
- [**Add tests!**](
|
30
|
+
- [**Add tests!**](https://rspec.info/) Your patch won't be accepted (or will be delayed) if it doesn't have tests.
|
31
31
|
|
32
|
-
- [**Document any change in behaviour**](
|
32
|
+
- [**Document any change in behaviour**](https://yardoc.org/) Make sure the README and any other
|
33
33
|
relevant documentation are kept up-to-date.
|
34
34
|
|
35
35
|
- [**Create topic branches**](https://github.com/dchelimsky/rspec/wiki/Topic-Branches) Don't ask us to pull from your master branch.
|
@@ -37,7 +37,7 @@ If you want your pull requests to be accepted, please follow the following guide
|
|
37
37
|
- [**One pull request per feature**](https://help.github.com/articles/using-pull-requests) If you want to do more than one thing, send
|
38
38
|
multiple pull requests.
|
39
39
|
|
40
|
-
- [**Send coherent history**](
|
40
|
+
- [**Send coherent history**](https://stackoverflow.com/questions/6934752/git-combining-multiple-commits-before-pushing) Make sure each individual commit in your pull
|
41
41
|
request is meaningful. If you had to make multiple intermediate commits while
|
42
42
|
developing, please squash them before sending them to us.
|
43
43
|
|
data/bin/ssh_scan
CHANGED
@@ -20,7 +20,7 @@ options = {
|
|
20
20
|
"verbosity" => nil,
|
21
21
|
"logger" => Logger.new(STDERR),
|
22
22
|
"fingerprint_database" => ENV['HOME']+'/.ssh_scan_fingerprints.yml',
|
23
|
-
"output_type" =>
|
23
|
+
"output_type" => nil
|
24
24
|
}
|
25
25
|
|
26
26
|
# Reorder arguments before parsing
|
@@ -106,7 +106,8 @@ scan") do |file|
|
|
106
106
|
|
107
107
|
opts.on("-o", "--output [FilePath]",
|
108
108
|
"File to write JSON output to") do |file|
|
109
|
-
|
109
|
+
options["output"] = file
|
110
|
+
# $stdout.reopen(file, "w")
|
110
111
|
end
|
111
112
|
|
112
113
|
opts.on("--output-type [json, yaml]",
|
@@ -238,9 +239,23 @@ options["policy_file"] = SSHScan::Policy.from_file(options["policy"])
|
|
238
239
|
scan_engine = SSHScan::ScanEngine.new()
|
239
240
|
results = scan_engine.scan(options)
|
240
241
|
|
241
|
-
if options["output_type"] == "yaml"
|
242
|
+
if options["output_type"] == "yaml" && (options["output"].nil? || options["output"].empty?)
|
242
243
|
puts YAML.dump(results)
|
243
|
-
elsif options["output_type"] == "json"
|
244
|
+
elsif options["output_type"] == "json" && (options["output"].nil? || options["output"].empty?)
|
245
|
+
puts JSON.pretty_generate(results)
|
246
|
+
elsif (options["output_type"].nil? || options["output_type"].empty?) && (!options["output"].nil? && options["output"].split(".").last.downcase == "yml")
|
247
|
+
open(options["output"], 'w') do |f|
|
248
|
+
f.puts YAML.dump(results)
|
249
|
+
end
|
250
|
+
elsif (options["output_type"].nil? || options["output_type"].empty?) && (!options["output"].nil? && options["output"].split(".").last.downcase == "json")
|
251
|
+
open(options["output"], 'w') do |f|
|
252
|
+
f.puts JSON.pretty_generate(results)
|
253
|
+
end
|
254
|
+
elsif (options["output_type"].nil? || options["output_type"].empty?) && options["output"]
|
255
|
+
open(options["output"], 'w') do |f|
|
256
|
+
f.puts JSON.pretty_generate(results)
|
257
|
+
end
|
258
|
+
else
|
244
259
|
puts JSON.pretty_generate(results)
|
245
260
|
end
|
246
261
|
|
data/lib/ssh_scan/public_key.rb
CHANGED
@@ -39,10 +39,11 @@ module SSHScan
|
|
39
39
|
|
40
40
|
def fingerprint_sha1
|
41
41
|
SSHKey.sha1_fingerprint(@key_string)
|
42
|
-
end
|
42
|
+
end
|
43
43
|
|
44
44
|
def fingerprint_sha256
|
45
|
-
|
45
|
+
# We're translating this to hex because the SSHKEY default isn't as useful for comparing with SSHFP records
|
46
|
+
Base64.decode64(SSHKey.sha256_fingerprint(@key_string)).hexify(:delim => ":")
|
46
47
|
end
|
47
48
|
|
48
49
|
def to_hash
|
data/lib/ssh_scan/result.rb
CHANGED
@@ -157,12 +157,20 @@ module SSHScan
|
|
157
157
|
@auth_methods = auth_methods
|
158
158
|
end
|
159
159
|
|
160
|
+
def keys
|
161
|
+
@keys || {}
|
162
|
+
end
|
163
|
+
|
160
164
|
def keys=(keys)
|
161
165
|
@keys = keys
|
162
166
|
end
|
163
167
|
|
164
|
-
def
|
165
|
-
@
|
168
|
+
def dns_keys
|
169
|
+
@dns_keys
|
170
|
+
end
|
171
|
+
|
172
|
+
def dns_keys=(dns_keys)
|
173
|
+
@dns_keys = dns_keys
|
166
174
|
end
|
167
175
|
|
168
176
|
def duplicate_host_key_ips=(duplicate_host_key_ips)
|
@@ -250,6 +258,7 @@ module SSHScan
|
|
250
258
|
"languages_server_to_client" => self.languages_server_to_client,
|
251
259
|
"auth_methods" => self.auth_methods,
|
252
260
|
"keys" => self.keys,
|
261
|
+
"dns_keys" => self.dns_keys,
|
253
262
|
"duplicate_host_key_ips" => self.duplicate_host_key_ips.uniq,
|
254
263
|
"compliance" => @compliance,
|
255
264
|
"start_time" => self.start_time,
|
data/lib/ssh_scan/scan_engine.rb
CHANGED
@@ -3,6 +3,7 @@ require 'ssh_scan/client'
|
|
3
3
|
require 'ssh_scan/public_key'
|
4
4
|
require 'ssh_scan/fingerprint_database'
|
5
5
|
require 'ssh_scan/subprocess'
|
6
|
+
require 'ssh_scan/ssh_fp'
|
6
7
|
require 'net/ssh'
|
7
8
|
require 'logger'
|
8
9
|
require 'open3'
|
@@ -221,6 +222,15 @@ module SSHScan
|
|
221
222
|
end
|
222
223
|
end
|
223
224
|
|
225
|
+
# Decorate all the results with SSHFP records
|
226
|
+
sshfp = SSHScan::SshFp.new()
|
227
|
+
results.each do |result|
|
228
|
+
if !result.hostname.empty?
|
229
|
+
dns_keys = sshfp.query(result.hostname)
|
230
|
+
result.dns_keys = dns_keys
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
224
234
|
# Decorate all the results with compliance information
|
225
235
|
results.each do |result|
|
226
236
|
# Do this only when we have all the information we need
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'resolv'
|
2
|
+
|
3
|
+
module SSHScan
|
4
|
+
class SshFp
|
5
|
+
|
6
|
+
ALGO_MAP = {
|
7
|
+
0 => "reserved", # Reference: https://tools.ietf.org/html/rfc4255#section-2.4
|
8
|
+
1 => "rsa", # Reference: https://tools.ietf.org/html/rfc4255#section-2.4
|
9
|
+
2 => "dss", # Reference: https://tools.ietf.org/html/rfc4255#section-2.4
|
10
|
+
3 => "ecdsa", # Reference: https://tools.ietf.org/html/rfc6594#section-5.3.1
|
11
|
+
4 => "ed25519" # Reference: https://tools.ietf.org/html/rfc7479
|
12
|
+
}
|
13
|
+
|
14
|
+
|
15
|
+
FPTYPE_MAP = {
|
16
|
+
0 => "reserved", # Reference: https://tools.ietf.org/html/rfc4255#section-2.4
|
17
|
+
1 => "sha1", # Reference: https://tools.ietf.org/html/rfc4255#section-2.4
|
18
|
+
2 => "sha256" # Reference: https://tools.ietf.org/html/rfc6594#section-5.1.2
|
19
|
+
}
|
20
|
+
|
21
|
+
|
22
|
+
def query(fqdn)
|
23
|
+
sshfp_records = []
|
24
|
+
|
25
|
+
# Reference: https://stackoverflow.com/questions/28867626/how-to-use-resolvdnsresourcegeneric
|
26
|
+
# Note: this includes some fixes too, I'll post a direct link back to the SO article.
|
27
|
+
Resolv::DNS.open do |dns|
|
28
|
+
all_records = dns.getresources(fqdn, Resolv::DNS::Resource::IN::ANY ) rescue nil
|
29
|
+
all_records.each do |rr|
|
30
|
+
if rr.is_a? Resolv::DNS::Resource::Generic then
|
31
|
+
classname = rr.class.name.split('::').last
|
32
|
+
if classname == "Type44_Class1"
|
33
|
+
data = rr.data.bytes
|
34
|
+
algo = data[0].to_s
|
35
|
+
fptype = data[1].to_s
|
36
|
+
fp = data[2..-1]
|
37
|
+
hex = fp.map{|b| b.to_s(16).rjust(2,'0') }.join(':')
|
38
|
+
sshfp_records << {"fptype" => FPTYPE_MAP[fptype.to_i], "algo" => ALGO_MAP[algo.to_i], "hex" => hex}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
return sshfp_records.sort_by { |k| k["hex"] }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module SSHScan
|
2
|
+
module Tests
|
3
|
+
class DnsKeyVerification
|
4
|
+
def initialize(result)
|
5
|
+
@result = result
|
6
|
+
end
|
7
|
+
|
8
|
+
def pass?
|
9
|
+
@result.keys.each do |key,value|
|
10
|
+
valid = false
|
11
|
+
|
12
|
+
@result.dns_keys.each do |dns_key|
|
13
|
+
if key == dns_key["algo"] &&
|
14
|
+
value["fingerprints"].values.include?(dns_key["hex"])
|
15
|
+
valid = true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# This means we fail any key that's offered that's not verifiable via information from DNS
|
20
|
+
return false unless valid == true
|
21
|
+
end
|
22
|
+
|
23
|
+
return true
|
24
|
+
end
|
25
|
+
|
26
|
+
def fail_description
|
27
|
+
if pass?
|
28
|
+
""
|
29
|
+
else
|
30
|
+
"One or more of the keys offered by the SSH service were not able to be verified using an SSHFS record"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def score_deduction
|
35
|
+
if pass?
|
36
|
+
0
|
37
|
+
else
|
38
|
+
-5
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/ssh_scan/version.rb
CHANGED
data/lib/string_ext.rb
CHANGED
@@ -69,6 +69,31 @@ class String
|
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
72
|
+
# Stolen from: https://github.com/emonti/rbkb/blob/master/lib/rbkb/extends/string.rb
|
73
|
+
def hexify(opts={})
|
74
|
+
delim = opts[:delim]
|
75
|
+
pre = (opts[:prefix] || "")
|
76
|
+
suf = (opts[:suffix] || "")
|
77
|
+
|
78
|
+
if (rx=opts[:rx]) and not rx.kind_of? Regexp
|
79
|
+
raise "rx must be a regular expression for a character class"
|
80
|
+
end
|
81
|
+
|
82
|
+
hx = [("0".."9").to_a, ("a".."f").to_a].flatten
|
83
|
+
|
84
|
+
out=Array.new
|
85
|
+
|
86
|
+
self.each_byte do |c|
|
87
|
+
hc = if (rx and not rx.match c.chr)
|
88
|
+
c.chr
|
89
|
+
else
|
90
|
+
pre + (hx[(c >> 4)] + hx[(c & 0xf )]) + suf
|
91
|
+
end
|
92
|
+
out << (hc)
|
93
|
+
end
|
94
|
+
out.join(delim)
|
95
|
+
end
|
96
|
+
|
72
97
|
def fqdn?
|
73
98
|
begin
|
74
99
|
resolve_fqdn
|
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.
|
4
|
+
version: 0.0.41
|
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: 2019-
|
15
|
+
date: 2019-05-03 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: bindata
|
@@ -190,6 +190,7 @@ files:
|
|
190
190
|
- lib/ssh_scan/public_key.rb
|
191
191
|
- lib/ssh_scan/result.rb
|
192
192
|
- lib/ssh_scan/scan_engine.rb
|
193
|
+
- lib/ssh_scan/ssh_fp.rb
|
193
194
|
- lib/ssh_scan/ssh_lib.rb
|
194
195
|
- lib/ssh_scan/ssh_lib/ciscossh.rb
|
195
196
|
- lib/ssh_scan/ssh_lib/cryptlib.rb
|
@@ -208,6 +209,7 @@ files:
|
|
208
209
|
- lib/ssh_scan/ssh_lib/unknown.rb
|
209
210
|
- lib/ssh_scan/subprocess.rb
|
210
211
|
- lib/ssh_scan/target_parser.rb
|
212
|
+
- lib/ssh_scan/tests/test_dns_key_verification.rb
|
211
213
|
- lib/ssh_scan/update.rb
|
212
214
|
- lib/ssh_scan/version.rb
|
213
215
|
- lib/string_ext.rb
|