x509_sleuth 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|