x509_sleuth 0.0.3
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 +7 -0
- data/lib/x509_sleuth/cli.rb +29 -0
- data/lib/x509_sleuth/client.rb +44 -0
- data/lib/x509_sleuth/scanner/target.rb +31 -0
- data/lib/x509_sleuth/scanner.rb +37 -0
- data/lib/x509_sleuth/scanner_detailed_presenter.rb +72 -0
- data/lib/x509_sleuth/scanner_presenter.rb +54 -0
- data/lib/x509_sleuth/version.rb +3 -0
- data/lib/x509_sleuth.rb +14 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/x509_sleuth/client_spec.rb +145 -0
- data/spec/x509_sleuth/scanner/target_spec.rb +87 -0
- data/spec/x509_sleuth/scanner_detailed_presenter_spec.rb +116 -0
- data/spec/x509_sleuth/scanner_presenter_spec.rb +94 -0
- data/spec/x509_sleuth/scanner_spec.rb +86 -0
- data/spec/x509_sleuth_spec.rb +7 -0
- metadata +178 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a6d6c6b79aa075f55cc50230060d95749d4cc0a6
|
4
|
+
data.tar.gz: 72f5b9d78fb620d03cce1bbbb1a20ba662a18462
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 76eca03d48bdcf7b8f67b3fc4898a8d0af4b85680519222c8cad40c11a9419c7ae05022c48c56b4ffd554675f3a0d012d555c5241799ae57f5425977eeebb585
|
7
|
+
data.tar.gz: 6d2b0f8e992830068534bfbe5a3f822341ab4d0fbccd9bd8086bf453e8f4d71a3a3713de78c7f6b8ad4416908abab053cb8e11b5ae7c681987d060d8774c92aa
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module X509Sleuth
|
4
|
+
class Cli < Thor
|
5
|
+
|
6
|
+
def self.exit_on_failure?
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
class_option :target, :type => :array, :required => true
|
11
|
+
|
12
|
+
desc "scan", "Scan the specified target(s) for certificate details"
|
13
|
+
def scan
|
14
|
+
options[:target].each do |target|
|
15
|
+
my_client.add_target(target)
|
16
|
+
end
|
17
|
+
my_client.run
|
18
|
+
output = X509Sleuth::ScannerPresenter.new(my_client)
|
19
|
+
puts output.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
no_commands do
|
23
|
+
def my_client
|
24
|
+
@client ||= X509Sleuth::Scanner.new
|
25
|
+
@client
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "socket"
|
2
|
+
require "openssl"
|
3
|
+
require "timeout"
|
4
|
+
|
5
|
+
module X509Sleuth
|
6
|
+
class Client
|
7
|
+
attr_reader :host, :port, :timeout_secs, :connect_error
|
8
|
+
|
9
|
+
def initialize(host, options = {})
|
10
|
+
options = {
|
11
|
+
port: 443,
|
12
|
+
timeout_secs: 15
|
13
|
+
}.merge(options)
|
14
|
+
|
15
|
+
@host = host
|
16
|
+
@port = options[:port]
|
17
|
+
@timeout_secs = options[:timeout_secs]
|
18
|
+
end
|
19
|
+
|
20
|
+
def tcp_socket
|
21
|
+
@tcp_socket ||= TCPSocket.new(@host, @port)
|
22
|
+
end
|
23
|
+
|
24
|
+
def ssl_socket
|
25
|
+
@ssl_socket ||= OpenSSL::SSL::SSLSocket.new(tcp_socket)
|
26
|
+
@ssl_socket.hostname = @host
|
27
|
+
@ssl_socket
|
28
|
+
end
|
29
|
+
|
30
|
+
def connect
|
31
|
+
Timeout::timeout(@timeout_secs) { ssl_socket.connect }
|
32
|
+
rescue SystemCallError, SocketError, OpenSSL::SSL::SSLError, Timeout::Error => e
|
33
|
+
@connect_error = e
|
34
|
+
end
|
35
|
+
|
36
|
+
def connect_failed?
|
37
|
+
connect_error ? true : false
|
38
|
+
end
|
39
|
+
|
40
|
+
def peer_certificate
|
41
|
+
@peer_certficate ||= ssl_socket.peer_cert
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "netaddr"
|
2
|
+
|
3
|
+
module X509Sleuth
|
4
|
+
class Scanner
|
5
|
+
class Target
|
6
|
+
attr_reader :target
|
7
|
+
|
8
|
+
def initialize(target)
|
9
|
+
@target = target
|
10
|
+
end
|
11
|
+
|
12
|
+
def is_a_range?
|
13
|
+
NetAddr::CIDR.create(target).size > 1 ? true : false
|
14
|
+
rescue NetAddr::ValidationError
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
def hosts
|
19
|
+
@hosts ||=
|
20
|
+
if is_a_range?
|
21
|
+
cidr = NetAddr::CIDR.create(target)
|
22
|
+
cidr.enumerate.reject do |address|
|
23
|
+
[cidr.network, cidr.broadcast].include?(address)
|
24
|
+
end
|
25
|
+
else
|
26
|
+
[target]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "x509_sleuth/scanner/target"
|
2
|
+
require "parallel"
|
3
|
+
|
4
|
+
module X509Sleuth
|
5
|
+
class Scanner
|
6
|
+
attr_accessor :concurrency
|
7
|
+
attr_reader :clients, :targets
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
options = {
|
11
|
+
concurrency: 5
|
12
|
+
}.merge(options)
|
13
|
+
|
14
|
+
@concurrency = options[:concurrency]
|
15
|
+
@targets = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_target(target_string)
|
19
|
+
@targets << X509Sleuth::Scanner::Target.new(target_string)
|
20
|
+
end
|
21
|
+
|
22
|
+
def clients
|
23
|
+
@clients ||=
|
24
|
+
targets.collect do |target|
|
25
|
+
target.hosts.collect do |host|
|
26
|
+
X509Sleuth::Client.new(host)
|
27
|
+
end
|
28
|
+
end.flatten
|
29
|
+
end
|
30
|
+
|
31
|
+
def run
|
32
|
+
Parallel.each(clients, in_threads: concurrency) do |client|
|
33
|
+
client.connect
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "formatador"
|
2
|
+
require "x509_sleuth/scanner_presenter"
|
3
|
+
|
4
|
+
module X509Sleuth
|
5
|
+
class ScannerDetailedPresenter < ScannerPresenter
|
6
|
+
def tableize(clients)
|
7
|
+
clients.collect do |client|
|
8
|
+
if client.peer_certificate
|
9
|
+
{
|
10
|
+
host: client.host,
|
11
|
+
subject: client.peer_certificate.subject,
|
12
|
+
common_name: parse_cn(client.peer_certificate),
|
13
|
+
alt_names: parse_san(client.peer_certificate).join(","),
|
14
|
+
issuer: client.peer_certificate.issuer,
|
15
|
+
serial: client.peer_certificate.serial,
|
16
|
+
not_before: client.peer_certificate.not_before,
|
17
|
+
not_after: client.peer_certificate.not_after
|
18
|
+
}
|
19
|
+
else
|
20
|
+
{
|
21
|
+
host: client.host,
|
22
|
+
subject: "",
|
23
|
+
common_name: "",
|
24
|
+
alt_names: [],
|
25
|
+
issuer: "",
|
26
|
+
serial: "",
|
27
|
+
not_before: "",
|
28
|
+
not_after: ""
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
Formatador.display_compact_table(
|
36
|
+
tableize(filter),
|
37
|
+
[
|
38
|
+
:host,
|
39
|
+
:subject,
|
40
|
+
:common_name,
|
41
|
+
:alt_names,
|
42
|
+
:issuer,
|
43
|
+
:serial,
|
44
|
+
:not_before,
|
45
|
+
:not_after
|
46
|
+
]
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def parse_cn(cert)
|
51
|
+
subject_parts = cert.subject.to_s.split("/").collect{ |p| p.split("=") }
|
52
|
+
common_name = ""
|
53
|
+
subject_parts.each do |part|
|
54
|
+
if part[0] && part[0] == "CN"
|
55
|
+
common_name = part[1]
|
56
|
+
break
|
57
|
+
end
|
58
|
+
end
|
59
|
+
common_name
|
60
|
+
end
|
61
|
+
|
62
|
+
def parse_san(cert)
|
63
|
+
subject_alt_names = []
|
64
|
+
cert.extensions.each do |extension|
|
65
|
+
if extension.oid == "subjectAltName"
|
66
|
+
subject_alt_names = extension.value.split(/[:,]|\s/).reject{ |part| part.nil? || part.empty? || part == "DNS" }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
subject_alt_names
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require "formatador"
|
2
|
+
|
3
|
+
module X509Sleuth
|
4
|
+
class ScannerPresenter
|
5
|
+
attr_reader :scanner
|
6
|
+
|
7
|
+
def initialize(scanner)
|
8
|
+
@scanner = scanner
|
9
|
+
end
|
10
|
+
|
11
|
+
def filter
|
12
|
+
@scanner.clients.reject do |client|
|
13
|
+
client.connect_failed?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def tableize(clients)
|
18
|
+
clients.collect do |client|
|
19
|
+
if client.peer_certificate
|
20
|
+
{
|
21
|
+
host: client.host,
|
22
|
+
subject: client.peer_certificate.subject,
|
23
|
+
issuer: client.peer_certificate.issuer,
|
24
|
+
serial: client.peer_certificate.serial,
|
25
|
+
not_before: client.peer_certificate.not_before,
|
26
|
+
not_after: client.peer_certificate.not_after
|
27
|
+
}
|
28
|
+
else
|
29
|
+
{
|
30
|
+
host: client.host,
|
31
|
+
subject: "",
|
32
|
+
issuer: "",
|
33
|
+
serial: "",
|
34
|
+
not_before: "",
|
35
|
+
not_after: ""
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s
|
42
|
+
Formatador.display_compact_table(
|
43
|
+
tableize(filter),
|
44
|
+
[
|
45
|
+
:host,
|
46
|
+
:subject,
|
47
|
+
:serial,
|
48
|
+
:not_before,
|
49
|
+
:not_after
|
50
|
+
]
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/x509_sleuth.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "x509_sleuth/cli"
|
2
|
+
require "x509_sleuth/client"
|
3
|
+
require "x509_sleuth/scanner"
|
4
|
+
require "x509_sleuth/scanner_detailed_presenter"
|
5
|
+
require "x509_sleuth/scanner_presenter"
|
6
|
+
require "x509_sleuth/version"
|
7
|
+
|
8
|
+
module X509Sleuth
|
9
|
+
class << self
|
10
|
+
def version_string
|
11
|
+
"X509 Sleuth version #{VERSION}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
|
3
|
+
describe X509Sleuth::Client do
|
4
|
+
let(:host) { "somehost" }
|
5
|
+
let(:tcp_socket_double) { double(TCPSocket) }
|
6
|
+
let(:ssl_socket_double) { double(OpenSSL::SSL::SSLSocket) }
|
7
|
+
let(:client) { described_class.new(host) }
|
8
|
+
|
9
|
+
before do
|
10
|
+
allow(TCPSocket).to receive(:new).and_return(tcp_socket_double)
|
11
|
+
allow(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(ssl_socket_double)
|
12
|
+
allow(ssl_socket_double).to receive(:hostname=)
|
13
|
+
end
|
14
|
+
|
15
|
+
context "when initialized" do
|
16
|
+
subject { described_class.new(host) }
|
17
|
+
|
18
|
+
it { should respond_to(:host) }
|
19
|
+
it { should respond_to(:port) }
|
20
|
+
it { should respond_to(:timeout_secs) }
|
21
|
+
it { should respond_to(:connect) }
|
22
|
+
it { should respond_to(:connect_failed?) }
|
23
|
+
it { should respond_to(:connect_error) }
|
24
|
+
it { should respond_to(:peer_certificate) }
|
25
|
+
#it { should respond_to(:peer_certificate_subject) }
|
26
|
+
#it { should respond_to(:peer_certificate_issuer) }
|
27
|
+
#it { should respond_to(:peer_certificate_serial) }
|
28
|
+
#it { should respond_to(:peer_certificate_activation_time) }
|
29
|
+
#it { should respond_to(:peer_certificate_expiration_time) }
|
30
|
+
|
31
|
+
context "with no options" do
|
32
|
+
it "returns defaults" do
|
33
|
+
expect(subject.host).to eq(host)
|
34
|
+
expect(subject.port).to eq(443)
|
35
|
+
expect(subject.timeout_secs).to eq(15)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "with options" do
|
40
|
+
subject { described_class.new(host, port: 40443, timeout_secs: 3) }
|
41
|
+
|
42
|
+
it "returns options" do
|
43
|
+
expect(subject.host).to eq(host)
|
44
|
+
expect(subject.port).to eq(40443)
|
45
|
+
expect(subject.timeout_secs).to eq(3)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "#tcp_socket" do
|
51
|
+
it "creates a TCP socket with the correct host and port" do
|
52
|
+
expect(TCPSocket).to receive(:new).with(host, 443).and_return(tcp_socket_double)
|
53
|
+
|
54
|
+
client.tcp_socket
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#ssl_socket" do
|
59
|
+
it "creates an SSL socket from the tcp_socket" do
|
60
|
+
expect(OpenSSL::SSL::SSLSocket).to receive(:new).with(tcp_socket_double).and_return(ssl_socket_double)
|
61
|
+
|
62
|
+
client.ssl_socket
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "#connect" do
|
67
|
+
it "connects to the remote server using OpenSSL" do
|
68
|
+
expect(ssl_socket_double).to receive(:connect).once
|
69
|
+
|
70
|
+
client.connect
|
71
|
+
end
|
72
|
+
|
73
|
+
context "gracefully handles some exceptions" do
|
74
|
+
[
|
75
|
+
SocketError,
|
76
|
+
OpenSSL::SSL::SSLError,
|
77
|
+
Timeout::Error,
|
78
|
+
Errno::EINTR,
|
79
|
+
Errno::ENETUNREACH,
|
80
|
+
Errno::ENETDOWN,
|
81
|
+
Errno::ENETRESET,
|
82
|
+
Errno::ECONNABORTED,
|
83
|
+
Errno::ECONNRESET,
|
84
|
+
Errno::ETIMEDOUT,
|
85
|
+
Errno::ECONNREFUSED,
|
86
|
+
Errno::EHOSTDOWN,
|
87
|
+
Errno::EHOSTUNREACH
|
88
|
+
].each do |e|
|
89
|
+
it "handles #{e}" do
|
90
|
+
allow(ssl_socket_double).to receive(:connect).and_raise(e)
|
91
|
+
|
92
|
+
expect { client.connect }.to_not raise_error
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "#peer_certificate" do
|
99
|
+
it "retrieves the remote host's X.509 certificate" do
|
100
|
+
expect(ssl_socket_double).to receive(:peer_cert).once
|
101
|
+
|
102
|
+
client.peer_certificate
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context "when a connection error occurs" do
|
107
|
+
before do
|
108
|
+
allow(ssl_socket_double).to receive(:connect).and_raise(SocketError)
|
109
|
+
end
|
110
|
+
|
111
|
+
describe "#connect_error" do
|
112
|
+
it "returns the exception" do
|
113
|
+
client.connect
|
114
|
+
expect(client.connect_error).to be_instance_of(SocketError)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "#connect_failed?" do
|
119
|
+
it "returns true" do
|
120
|
+
client.connect
|
121
|
+
expect(client.connect_failed?).to eq(true)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
context "when a successful connection occurs" do
|
127
|
+
before do
|
128
|
+
allow(ssl_socket_double).to receive(:connect)
|
129
|
+
end
|
130
|
+
|
131
|
+
describe "#connect_error" do
|
132
|
+
it "returns nil" do
|
133
|
+
client.connect
|
134
|
+
expect(client.connect_error).to be_nil
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "#connect_failed?" do
|
139
|
+
it "returns false" do
|
140
|
+
client.connect
|
141
|
+
expect(client.connect_failed?).to eq(false)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require_relative "../../spec_helper"
|
2
|
+
|
3
|
+
describe X509Sleuth::Scanner::Target do
|
4
|
+
let(:host_target) { described_class.new("localhost") }
|
5
|
+
let(:ip_target) { described_class.new("127.0.0.1") }
|
6
|
+
let(:cidr_target) { described_class.new("127.0.0.1/30") }
|
7
|
+
let(:ip_subnet_pair_target) { described_class.new("127.0.0.1/255.255.255.248") }
|
8
|
+
|
9
|
+
context "instances" do
|
10
|
+
subject { host_target }
|
11
|
+
|
12
|
+
it { should respond_to(:is_a_range?) }
|
13
|
+
it { should respond_to(:hosts) }
|
14
|
+
it { should respond_to(:target) }
|
15
|
+
end
|
16
|
+
|
17
|
+
context "when target is an IPv4 address and CIDR subnet mask" do
|
18
|
+
describe "#is_a_range?" do
|
19
|
+
it "returns true" do
|
20
|
+
expect(cidr_target.is_a_range?).to be true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#hosts" do
|
25
|
+
it "returns the correct host ip addresses" do
|
26
|
+
expect(cidr_target.hosts).to have(2).items
|
27
|
+
expect(cidr_target.hosts).to include("127.0.0.1", "127.0.0.2")
|
28
|
+
end
|
29
|
+
|
30
|
+
it "omits the network and broadcast addresses" do
|
31
|
+
expect(cidr_target.hosts).to_not include("127.0.0.0", "127.0.0.3")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "when target is an IPv4 address and dotted-decimal subnet mask" do
|
37
|
+
describe "#is_a_range?" do
|
38
|
+
it "returns true" do
|
39
|
+
expect(ip_subnet_pair_target.is_a_range?).to be true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "#hosts" do
|
44
|
+
it "returns the correct host ip addresses" do
|
45
|
+
expect(ip_subnet_pair_target.hosts).to have(6).items
|
46
|
+
expect(ip_subnet_pair_target.hosts).to include(
|
47
|
+
"127.0.0.1", "127.0.0.2", "127.0.0.3",
|
48
|
+
"127.0.0.4", "127.0.0.5", "127.0.0.6"
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "omits the network and broadcast addresses" do
|
53
|
+
expect(ip_subnet_pair_target.hosts).to_not include("127.0.0.0", "127.0.0.7")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "when target is a single IPv4 address" do
|
59
|
+
describe "#is_a_range?" do
|
60
|
+
it "returns false" do
|
61
|
+
expect(ip_target.is_a_range?).to be false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "#hosts" do
|
66
|
+
it "returns the lone ip address" do
|
67
|
+
expect(ip_target.hosts).to have(1).item
|
68
|
+
expect(ip_target.hosts).to include("127.0.0.1")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "when target is assumed to be a hostname" do
|
74
|
+
describe "#is_a_range?" do
|
75
|
+
it "returns false" do
|
76
|
+
expect(host_target.is_a_range?).to be false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "#hosts" do
|
81
|
+
it "returns the lone hostname" do
|
82
|
+
expect(host_target.hosts).to have(1).item
|
83
|
+
expect(host_target.hosts).to include("localhost")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
|
3
|
+
describe X509Sleuth::ScannerDetailedPresenter do
|
4
|
+
let(:simple_x509_cert_double) do
|
5
|
+
double(
|
6
|
+
OpenSSL::X509::Certificate,
|
7
|
+
subject: "/OU=Domain Control Validated"\
|
8
|
+
"/CN=*.mydomain.tld",
|
9
|
+
issuer: "/C=US"\
|
10
|
+
"/ST=Arizona"\
|
11
|
+
"/L=Scottsdale"\
|
12
|
+
"/O=GoDaddy.com, Inc."\
|
13
|
+
"/OU=http://certs.godaddy.com/repository/"\
|
14
|
+
"/CN=Go Daddy Secure Certificate Authority - G2",
|
15
|
+
extensions: [],
|
16
|
+
serial: 12227668858515977,
|
17
|
+
not_before: Time.utc(2014, 4, 10, 20, 19, 9),
|
18
|
+
not_after: Time.utc(2014, 10, 3, 23, 22, 57)
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
let(:san_x509_cert_double) do
|
23
|
+
double(
|
24
|
+
OpenSSL::X509::Certificate,
|
25
|
+
subject: "/C=US"\
|
26
|
+
"/postalCode=19103"\
|
27
|
+
"/ST=PA"\
|
28
|
+
"/L=Philadelphia"\
|
29
|
+
"/street=123 Fake Street"\
|
30
|
+
"/O=Fake Corp"\
|
31
|
+
"/OU=Fake Department"\
|
32
|
+
"/CN=fake.example.com",
|
33
|
+
issuer: "/C=GB"\
|
34
|
+
"/ST=Greater Manchester"\
|
35
|
+
"/L=Salford"\
|
36
|
+
"/O=COMODO CA Limited"\
|
37
|
+
"/CN=COMODO High-Assurance Secure Server CA",
|
38
|
+
extensions: [
|
39
|
+
OpenSSL::X509::Extension.new("authorityKeyIdentifier", "keyid:3F:D5:B5:D0:D6:44:79:50:4A:17:A3:9B:8C:4A:DC:B8:B0:23:64:6F"),
|
40
|
+
OpenSSL::X509::Extension.new("subjectKeyIdentifier", "A3:F1:44:92:8F:53:2D:69:66:56:0A:69:20:8B:F4:6B:5F:18:88:1D"),
|
41
|
+
OpenSSL::X509::Extension.new("keyUsage", "Digital Signature, Key Encipherment", true),
|
42
|
+
OpenSSL::X509::Extension.new("basicConstraints", "CA:FALSE", true),
|
43
|
+
OpenSSL::X509::Extension.new("extendedKeyUsage", "TLS Web Server Authentication, TLS Web Client Authentication"),
|
44
|
+
OpenSSL::X509::Extension.new("certificatePolicies", "Policy: 1.3.6.1.4.1.6449.1.2.1.3.4\nCPS: https://secure.comodo.com/CPS\nPolicy: 2.23.140.1.2.2"),
|
45
|
+
OpenSSL::X509::Extension.new("crlDistributionPoints", "URI:http://crl.comodoca.com/COMODOHigh-AssuranceSecureServerCA.crl"),
|
46
|
+
OpenSSL::X509::Extension.new("authorityInfoAccess", "CA Issuers - URI:http://crt.comodoca.com/COMODOHigh-AssuranceSecureServerCA.crt\nOCSP - URI:http://ocsp.comodoca.com"),
|
47
|
+
OpenSSL::X509::Extension.new("subjectAltName", "DNS:foo.example.com, DNS:bar.example.com")
|
48
|
+
|
49
|
+
],
|
50
|
+
serial: 12227668858515977,
|
51
|
+
not_before: Time.utc(2014, 4, 10, 20, 19, 9),
|
52
|
+
not_after: Time.utc(2014, 10, 3, 23, 22, 57)
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
let(:simple_client_double) do
|
57
|
+
double(
|
58
|
+
X509Sleuth::Client,
|
59
|
+
host: "www.mydomain.tld",
|
60
|
+
connect_failed?: false,
|
61
|
+
peer_certificate: simple_x509_cert_double
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
let(:san_client_double) do
|
66
|
+
double(
|
67
|
+
X509Sleuth::Client,
|
68
|
+
host: "fake.example.com",
|
69
|
+
connect_failed?: false,
|
70
|
+
peer_certificate: san_x509_cert_double
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
let(:scanner_double) do
|
75
|
+
double(
|
76
|
+
X509Sleuth::Scanner,
|
77
|
+
clients: [simple_client_double, san_client_double]
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
let(:scanner_presenter) { described_class.new(scanner_double) }
|
82
|
+
|
83
|
+
|
84
|
+
describe "#tableize" do
|
85
|
+
let(:ok_client_tableized_results) do
|
86
|
+
[
|
87
|
+
{
|
88
|
+
host: simple_client_double.host,
|
89
|
+
subject: simple_x509_cert_double.subject,
|
90
|
+
common_name: "*.mydomain.tld",
|
91
|
+
alt_names: "",
|
92
|
+
issuer: simple_x509_cert_double.issuer,
|
93
|
+
serial: simple_x509_cert_double.serial,
|
94
|
+
not_before: simple_x509_cert_double.not_before,
|
95
|
+
not_after: simple_x509_cert_double.not_after
|
96
|
+
},
|
97
|
+
{
|
98
|
+
host: san_client_double.host,
|
99
|
+
subject: san_x509_cert_double.subject,
|
100
|
+
common_name: "fake.example.com",
|
101
|
+
alt_names: "foo.example.com,bar.example.com",
|
102
|
+
issuer: san_x509_cert_double.issuer,
|
103
|
+
serial: san_x509_cert_double.serial,
|
104
|
+
not_before: san_x509_cert_double.not_before,
|
105
|
+
not_after: san_x509_cert_double.not_after
|
106
|
+
}
|
107
|
+
]
|
108
|
+
end
|
109
|
+
|
110
|
+
it "returns the expected Formatador table" do
|
111
|
+
ok_client_tableized_results.each do |cert_details|
|
112
|
+
expect(scanner_presenter.tableize(scanner_double.clients)).to include(cert_details)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
|
3
|
+
describe X509Sleuth::ScannerPresenter do
|
4
|
+
let(:x509_cert_double) do
|
5
|
+
double(
|
6
|
+
OpenSSL::X509::Certificate,
|
7
|
+
subject: "/OU=Domain Control Validated"\
|
8
|
+
"/CN=*.mydomain.tld",
|
9
|
+
issuer: "/C=US"\
|
10
|
+
"/ST=Arizona"\
|
11
|
+
"/L=Scottsdale"\
|
12
|
+
"/O=GoDaddy.com, Inc."\
|
13
|
+
"/OU=http://certs.godaddy.com/repository/"\
|
14
|
+
"/CN=Go Daddy Secure Certificate Authority - G2",
|
15
|
+
serial: 12227668858515977,
|
16
|
+
not_before: Time.utc(2014, 4, 10, 20, 19, 9),
|
17
|
+
not_after: Time.utc(2014, 10, 3, 23, 22, 57)
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
let(:ok_client_double) do
|
22
|
+
instance_double(
|
23
|
+
X509Sleuth::Client,
|
24
|
+
host: "ok.client.tld",
|
25
|
+
connect_failed?: false,
|
26
|
+
peer_certificate: x509_cert_double
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
let(:failed_client_double) do
|
31
|
+
instance_double(
|
32
|
+
X509Sleuth::Client,
|
33
|
+
host: "failed.client.tld",
|
34
|
+
connect_failed?: true,
|
35
|
+
connect_error: TimeoutError.new,
|
36
|
+
peer_certificate: nil
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
let(:scanner_double) do
|
41
|
+
double(
|
42
|
+
X509Sleuth::Scanner,
|
43
|
+
clients: [ok_client_double, failed_client_double]
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
let(:scanner_presenter) { described_class.new(scanner_double) }
|
48
|
+
|
49
|
+
context "instances" do
|
50
|
+
subject { scanner_presenter }
|
51
|
+
|
52
|
+
it { should respond_to(:scanner).with(0).arguments }
|
53
|
+
it { should respond_to(:tableize).with(1).argument }
|
54
|
+
it { should respond_to(:filter).with(0).arguments }
|
55
|
+
it { should respond_to(:to_s).with(0).arguments }
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#filter" do
|
59
|
+
it "removes failed clients" do
|
60
|
+
filtered = scanner_presenter.filter()
|
61
|
+
expect(filtered).to include(ok_client_double)
|
62
|
+
expect(filtered).to_not include(failed_client_double)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "#tableize" do
|
67
|
+
let(:ok_client_tableized_result) do
|
68
|
+
{
|
69
|
+
host: ok_client_double.host,
|
70
|
+
subject: x509_cert_double.subject,
|
71
|
+
issuer: x509_cert_double.issuer,
|
72
|
+
serial: x509_cert_double.serial,
|
73
|
+
not_before: x509_cert_double.not_before,
|
74
|
+
not_after: x509_cert_double.not_after
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
let(:failed_client_tableized_result) do
|
79
|
+
{
|
80
|
+
host: "failed.client.tld",
|
81
|
+
connect_failed?: true,
|
82
|
+
connect_error: TimeoutError.new
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
it "returns the expected Formatador table" do
|
87
|
+
expect(scanner_presenter.tableize(scanner_double.clients())).to include(ok_client_tableized_result)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "excludes clients with failed connections" do
|
91
|
+
expect(scanner_presenter.tableize(scanner_presenter.filter())).to_not include(failed_client_tableized_result)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
|
3
|
+
describe X509Sleuth::Scanner do
|
4
|
+
let(:scanner) { described_class.new }
|
5
|
+
let(:scanner_with_overrides) { described_class.new(concurrency: 127) }
|
6
|
+
let(:cidr_target_string) { "127.0.0.1/30" }
|
7
|
+
let(:host_target_string) { "bigserver.domain.local" }
|
8
|
+
let(:scanner_targets) do
|
9
|
+
[
|
10
|
+
X509Sleuth::Scanner::Target.new(cidr_target_string),
|
11
|
+
X509Sleuth::Scanner::Target.new(host_target_string)
|
12
|
+
]
|
13
|
+
end
|
14
|
+
|
15
|
+
context "instances" do
|
16
|
+
subject { scanner }
|
17
|
+
|
18
|
+
it { should respond_to(:add_target).with(1).arguments }
|
19
|
+
it { should_not respond_to(:add_target).with(0).arguments }
|
20
|
+
it { should respond_to(:targets).with(0).arguments }
|
21
|
+
it { should respond_to(:clients).with(0).arguments }
|
22
|
+
it { should respond_to(:concurrency) }
|
23
|
+
it { should respond_to(:clients).with(0).arguments }
|
24
|
+
it { should respond_to(:run).with(0).arguments }
|
25
|
+
|
26
|
+
context "with option overrides" do
|
27
|
+
subject { scanner_with_overrides }
|
28
|
+
it "returns overrides" do
|
29
|
+
expect(subject.concurrency).to eq(127)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#add_target" do
|
35
|
+
it "adds the target to the targets list" do
|
36
|
+
scanner.add_target(cidr_target_string)
|
37
|
+
|
38
|
+
expect(scanner.targets).to have(1).item
|
39
|
+
expect(scanner.targets).to be_all { |item| item.is_a?(X509Sleuth::Scanner::Target) }
|
40
|
+
end
|
41
|
+
|
42
|
+
it "appends to an existing targets list" do
|
43
|
+
scanner.add_target(cidr_target_string)
|
44
|
+
scanner.add_target(host_target_string)
|
45
|
+
|
46
|
+
expect(scanner.targets).to have(2).items
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "#clients" do
|
51
|
+
before do
|
52
|
+
allow(scanner).to receive(:targets).and_return(scanner_targets)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "contains a collection of clients from the loaded targets" do
|
56
|
+
scanner.add_target(cidr_target_string)
|
57
|
+
scanner.add_target(host_target_string)
|
58
|
+
|
59
|
+
expect(scanner.clients).to have(3).items
|
60
|
+
expect(scanner.clients).to be_all { |item| item.is_a?(X509Sleuth::Client) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#run" do
|
65
|
+
let(:client_double) { double(X509Sleuth::Client) }
|
66
|
+
let(:scanner_clients) { [client_double, client_double] }
|
67
|
+
|
68
|
+
before do
|
69
|
+
allow(scanner).to receive(:clients).and_return(scanner_clients)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "connects to all the hosts" do
|
73
|
+
expect(client_double).to receive(:connect).exactly(2).times
|
74
|
+
scanner.run
|
75
|
+
end
|
76
|
+
|
77
|
+
it "scans the expected number of hosts in parallel" do
|
78
|
+
expect(Parallel).to receive(:each) do |arg1, arg2|
|
79
|
+
expect(arg1).to eq(scanner_clients)
|
80
|
+
expect(arg2).to include(in_threads: 5)
|
81
|
+
end
|
82
|
+
|
83
|
+
scanner.run
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
metadata
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: x509_sleuth
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Richard Henning
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-04-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: formatador
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: netaddr
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: parallel
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: thor
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.1.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 3.1.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec-collection_matchers
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ~>
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 1.1.2
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ~>
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 1.1.2
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: travis-lint
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description:
|
126
|
+
email: rich@tonaleclipse.com
|
127
|
+
executables: []
|
128
|
+
extensions: []
|
129
|
+
extra_rdoc_files: []
|
130
|
+
files:
|
131
|
+
- lib/x509_sleuth/cli.rb
|
132
|
+
- lib/x509_sleuth/client.rb
|
133
|
+
- lib/x509_sleuth/scanner/target.rb
|
134
|
+
- lib/x509_sleuth/scanner.rb
|
135
|
+
- lib/x509_sleuth/scanner_detailed_presenter.rb
|
136
|
+
- lib/x509_sleuth/scanner_presenter.rb
|
137
|
+
- lib/x509_sleuth/version.rb
|
138
|
+
- lib/x509_sleuth.rb
|
139
|
+
- spec/spec_helper.rb
|
140
|
+
- spec/x509_sleuth/client_spec.rb
|
141
|
+
- spec/x509_sleuth/scanner/target_spec.rb
|
142
|
+
- spec/x509_sleuth/scanner_detailed_presenter_spec.rb
|
143
|
+
- spec/x509_sleuth/scanner_presenter_spec.rb
|
144
|
+
- spec/x509_sleuth/scanner_spec.rb
|
145
|
+
- spec/x509_sleuth_spec.rb
|
146
|
+
homepage: https://github.com/rhenning/x509_sleuth
|
147
|
+
licenses:
|
148
|
+
- MIT
|
149
|
+
metadata: {}
|
150
|
+
post_install_message:
|
151
|
+
rdoc_options: []
|
152
|
+
require_paths:
|
153
|
+
- lib
|
154
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - '>='
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
160
|
+
requirements:
|
161
|
+
- - '>='
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: '0'
|
164
|
+
requirements: []
|
165
|
+
rubyforge_project:
|
166
|
+
rubygems_version: 2.0.14
|
167
|
+
signing_key:
|
168
|
+
specification_version: 4
|
169
|
+
summary: A tool to remotely scan for and investigate X.509 certificates used in SSL/TLS
|
170
|
+
test_files:
|
171
|
+
- spec/spec_helper.rb
|
172
|
+
- spec/x509_sleuth/client_spec.rb
|
173
|
+
- spec/x509_sleuth/scanner/target_spec.rb
|
174
|
+
- spec/x509_sleuth/scanner_detailed_presenter_spec.rb
|
175
|
+
- spec/x509_sleuth/scanner_presenter_spec.rb
|
176
|
+
- spec/x509_sleuth/scanner_spec.rb
|
177
|
+
- spec/x509_sleuth_spec.rb
|
178
|
+
has_rdoc:
|