x509_sleuth 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ module X509Sleuth
2
+ VERSION = "0.0.3"
3
+ end
@@ -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
@@ -0,0 +1,10 @@
1
+ require "rspec"
2
+ require "x509_sleuth"
3
+ require "x509_sleuth/scanner_presenter"
4
+
5
+ require "rspec/collection_matchers"
6
+
7
+ RSpec.configure do |config|
8
+ config.color = true
9
+ config.formatter = "documentation"
10
+ end
@@ -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
@@ -0,0 +1,7 @@
1
+ require "spec_helper"
2
+
3
+ describe X509Sleuth do
4
+ it "returns the correct version string" do
5
+ expect(described_class.version_string).to eq("X509 Sleuth version #{X509Sleuth::VERSION}")
6
+ end
7
+ 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: