canari 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: eed48dec494e225babfa73dca64d9c3cdabaa01de2db3b958e134bc1a5e100aa
4
+ data.tar.gz: a03a4f319a2056fa64e98b7561916063b081a467fe9f0a093b6c614e4e6cf20a
5
+ SHA512:
6
+ metadata.gz: 48db2f2f19292c4f7d62a380b974dafc432f6c00d3b4f7a90fcc42c55ed58f7f0d51aa9279ad10e2f99b5baacbaa05c11f396dc341866c55e43c3d3821161e43
7
+ data.tar.gz: 16a8b3639454595049007a05146f511ea5dffa519b4caf6027bbedd535af931f19b9a30ca3708b157a9a03b714eae6365576619f2e2c79f9e37977e03a250e8c
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ domains.txt
10
+ canari.yml
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.6.1
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.1
5
+ before_install: gem install bundler -v 1.16.2
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in canari.gemspec
8
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,58 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ canari (0.1.0)
5
+ dalli (~> 2.7)
6
+ eventmachine (~> 1.2)
7
+ mail (~> 2.7)
8
+ permessage_deflate (~> 0.1)
9
+ thor (~> 0.19)
10
+ websocket-driver (~> 0.7)
11
+
12
+ GEM
13
+ remote: https://rubygems.org/
14
+ specs:
15
+ coveralls (0.8.22)
16
+ json (>= 1.8, < 3)
17
+ simplecov (~> 0.16.1)
18
+ term-ansicolor (~> 1.3)
19
+ thor (~> 0.19.4)
20
+ tins (~> 1.6)
21
+ dalli (2.7.9)
22
+ docile (1.3.1)
23
+ eventmachine (1.2.7)
24
+ json (2.2.0)
25
+ mail (2.7.1)
26
+ mini_mime (>= 0.1.1)
27
+ mini_mime (1.0.1)
28
+ minitest (5.11.3)
29
+ minitest-implicit-subject (1.4.0)
30
+ minitest
31
+ permessage_deflate (0.1.4)
32
+ rake (10.5.0)
33
+ simplecov (0.16.1)
34
+ docile (~> 1.1)
35
+ json (>= 1.8, < 3)
36
+ simplecov-html (~> 0.10.0)
37
+ simplecov-html (0.10.2)
38
+ term-ansicolor (1.7.1)
39
+ tins (~> 1.0)
40
+ thor (0.19.4)
41
+ tins (1.20.2)
42
+ websocket-driver (0.7.0)
43
+ websocket-extensions (>= 0.1.0)
44
+ websocket-extensions (0.1.3)
45
+
46
+ PLATFORMS
47
+ ruby
48
+
49
+ DEPENDENCIES
50
+ bundler (~> 1.16)
51
+ canari!
52
+ coveralls (~> 0.8, >= 0.8.9)
53
+ minitest (~> 5.0)
54
+ minitest-implicit-subject (~> 1.4)
55
+ rake (~> 10.0)
56
+
57
+ BUNDLED WITH
58
+ 1.17.2
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Jef Mathiot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # Canari
2
+
3
+ A gem to monitor TLS certificates using Cert Stream
4
+
5
+ ## Installation
6
+
7
+ Install the gem (deployment to Rubygems coming soon) :
8
+
9
+ ```ruby
10
+ gem install canari
11
+ ```
12
+
13
+ Install memcached and create the `canari.yml` configuration file:
14
+
15
+ ```
16
+ ---
17
+ :memcached:
18
+ :host: localhost
19
+ :port: 11211
20
+ :namespace: canari
21
+ :smtp:
22
+ :address: smtp.server.example
23
+ :port: 25
24
+ :notifier:
25
+ :from: your-email@example.com
26
+ :to: your-email@example.com
27
+ ```
28
+
29
+ Create the `domains.txt` file containing the domain names you'd like to
30
+ monitor, one name per line:
31
+
32
+ ```
33
+ google.com
34
+ gouv.fr
35
+ fr
36
+ ...
37
+ ```
38
+
39
+ Please notice each match, even a partial one, will trigger an
40
+ email notification. In the example above, every certificate issued for the
41
+ `.fr` TLD would result in a notification.
42
+
43
+ Then execute:
44
+
45
+ $ canari start
46
+
47
+ More options available using:
48
+
49
+ $ canari help
50
+
51
+ Or:
52
+
53
+ $ canari help start
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'spec'
8
+ t.libs << 'lib'
9
+ t.test_files = FileList['spec/**/*_spec.rb']
10
+ end
11
+
12
+ task default: :test
data/bin/canari ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'canari/cli'
4
+
5
+ # See https://github.com/erikhuda/thor/issues/244
6
+ Thor.send(:define_singleton_method, :exit_on_failure?) { true }
7
+ Canari::CLI.start(ARGV)
data/canari.gemspec ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'canari/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'canari'
9
+ spec.version = Canari::VERSION
10
+ spec.authors = ['Jef Mathiot']
11
+ spec.email = ['jef@nonblocking.info']
12
+
13
+ spec.summary = 'A gem to monitor TLS certificates using Cert Stream.'
14
+ spec.description = 'A gem to monitor TLS certificates using Cert Stream.'
15
+ spec.homepage = 'https://github.com/jefmathiot/canari'
16
+ spec.license = 'MIT'
17
+
18
+ # Specify which files should be added to the gem when it is released.
19
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
+ end
23
+ spec.bindir = 'bin'
24
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
25
+ spec.require_paths = ['lib']
26
+
27
+ spec.add_development_dependency 'bundler', '~> 1.16'
28
+ spec.add_development_dependency 'coveralls', '~> 0.8', '>= 0.8.9'
29
+ spec.add_development_dependency 'minitest', '~> 5.0'
30
+ spec.add_development_dependency 'minitest-implicit-subject', '~> 1.4'
31
+ spec.add_development_dependency 'rake', '~> 10.0'
32
+
33
+ spec.add_dependency 'dalli', '~> 2.7'
34
+ spec.add_dependency 'eventmachine', '~> 1.2'
35
+ spec.add_dependency 'mail', '~> 2.7'
36
+ spec.add_dependency 'permessage_deflate', '~> 0.1'
37
+ spec.add_dependency 'thor', '~> 0.19'
38
+ spec.add_dependency 'websocket-driver', '~> 0.7'
39
+ end
@@ -0,0 +1,7 @@
1
+ version: "3"
2
+ services:
3
+ memcached:
4
+ image: memcached
5
+ command: memcached -m 128
6
+ ports:
7
+ - "11211:11211"
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'websocket/driver'
4
+ require 'permessage_deflate'
5
+ require 'json'
6
+ require 'uri'
7
+
8
+ module Canari
9
+ module CertStream
10
+ attr_accessor :url
11
+
12
+ def connection_completed
13
+ @driver = WebSocket::Driver.client(self)
14
+ @driver.add_extension(PermessageDeflate)
15
+ attach_listeners
16
+ @driver.start
17
+ end
18
+
19
+ def receive_data(data)
20
+ @driver.parse(data)
21
+ end
22
+
23
+ def write(data)
24
+ send_data(data)
25
+ end
26
+
27
+ def finalize(event)
28
+ Canari.logger.info "Connection closed, #{event.code}: #{event.reason}"
29
+ close_connection
30
+ Canari.logger.info 'Reconnecting'
31
+ reconnect(uri.host, 443)
32
+ end
33
+
34
+ def uri
35
+ URI.parse(url)
36
+ end
37
+
38
+ def attach_listeners
39
+ @driver.on(:open) { |_event| Canari.logger.info 'Connection opened' }
40
+ @driver.on(:message) do |event|
41
+ handle_message(event.data)
42
+ end
43
+ @driver.on(:close) { |event| finalize(event) }
44
+ end
45
+
46
+ def handle_message(data)
47
+ data = JSON.parse(data)
48
+ return unless data['message_type'] == 'certificate_update'
49
+
50
+ cert = data['data']['leaf_cert']
51
+ matching_names = DomainCache.fetch(cert['all_domains'])
52
+ return unless matching_names.any?
53
+
54
+ Canari.logger.info "Certificate matching #{matching_names}"
55
+ Notifier.notify(matching_names, data)
56
+ end
57
+ end
58
+ end
data/lib/canari/cli.rb ADDED
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'canari'
5
+ require 'eventmachine'
6
+ require 'net/http'
7
+ require 'uri'
8
+ require 'logger'
9
+ require 'yaml'
10
+ require 'dalli'
11
+
12
+ module Canari
13
+ # Command-line interface.
14
+ class CLI < Thor
15
+ desc 'start', 'Start monitoring TLS certificates'
16
+ method_option :config, aliases: %i[c], default: 'canari.yml'
17
+ method_option :domains, aliases: %i[d], default: 'domains.txt'
18
+ def start
19
+ Canari.load_config(options[:config])
20
+ DomainCache.preload(options[:domains])
21
+ run(URI.parse('wss://certstream.calidog.io'))
22
+ loop do
23
+ sleep 1
24
+ end
25
+ end
26
+
27
+ def run(uri)
28
+ EM.run do
29
+ EM.connect(uri.host, 443, CertStream) do |stream|
30
+ stream.url = uri.to_s
31
+ stream.start_tls(sni_hostname: uri.host)
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def self.logger
38
+ @logger ||= Logger.new(STDOUT)
39
+ end
40
+
41
+ def self.load_config(file)
42
+ config = @config = YAML.safe_load(
43
+ File.open(File.expand_path(file)), [Symbol]
44
+ )
45
+ Mail.defaults do
46
+ delivery_method :smtp, config[:smtp]
47
+ end
48
+ end
49
+
50
+ def self.config
51
+ @config
52
+ end
53
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canari
4
+ # Store and retrieve domains from Memcached
5
+ class DomainCache
6
+ class << self
7
+ def preload(domains_file)
8
+ cache.flush
9
+ count = 0
10
+ File.readlines(File.expand_path(domains_file)).each do |line|
11
+ line = line.strip
12
+ next if line.start_with?('#')
13
+
14
+ cache.set(line, 1)
15
+ count += 1
16
+ end
17
+ Canari.logger.info "Preloaded #{count} domains"
18
+ end
19
+
20
+ def fetch(domains)
21
+ keys = variants(domains)
22
+ cache.get_multi(keys).keys
23
+ end
24
+
25
+ def variants(domains)
26
+ domains.map do |domain|
27
+ domain = domain.split('.').reject { |part| part == '*' }.join('.')
28
+ [domain].tap do |values|
29
+ until domain.empty?
30
+ domain = domain.split('.').drop(1).join('.')
31
+ values << domain unless domain.empty?
32
+ end
33
+ end
34
+ end.flatten.uniq
35
+ end
36
+
37
+ def cache
38
+ return @cache if @cache
39
+
40
+ mconfig = Canari.config[:memcached]
41
+ @cache = Dalli::Client.new("#{mconfig[:host]}:#{mconfig[:port]}",
42
+ namespace: mconfig[:namespace])
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mail'
4
+
5
+ module Canari
6
+ # Send email notifications.
7
+ module Notifier
8
+ class << self
9
+ def notify(matching, payload)
10
+ Mail.new do
11
+ from Canari.config[:notifier][:from]
12
+ to Canari.config[:notifier][:to]
13
+ subject 'New match in the Certificate Transparency Log network'
14
+ body 'A new certificate has been emitted matching: ' \
15
+ "#{matching.join(', ')}. See the attached file for details."
16
+ add_file filename: 'certificate.json',
17
+ content: JSON.pretty_print(payload)
18
+ end.deliver
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Canari
4
+ VERSION = '0.1.0'.freeze
5
+ end
data/lib/canari.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'canari/version'
4
+ require 'canari/cert_stream'
5
+ require 'canari/domain_cache'
6
+ require 'canari/notifier'
7
+
8
+ module Canari
9
+ def self.version
10
+ VERSION
11
+ end
12
+ end
metadata ADDED
@@ -0,0 +1,221 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: canari
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jef Mathiot
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-03-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: coveralls
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.8'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 0.8.9
37
+ type: :development
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '0.8'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 0.8.9
47
+ - !ruby/object:Gem::Dependency
48
+ name: minitest
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '5.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '5.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: minitest-implicit-subject
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.4'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.4'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rake
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '10.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '10.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: dalli
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.7'
96
+ type: :runtime
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.7'
103
+ - !ruby/object:Gem::Dependency
104
+ name: eventmachine
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '1.2'
110
+ type: :runtime
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '1.2'
117
+ - !ruby/object:Gem::Dependency
118
+ name: mail
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '2.7'
124
+ type: :runtime
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '2.7'
131
+ - !ruby/object:Gem::Dependency
132
+ name: permessage_deflate
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '0.1'
138
+ type: :runtime
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '0.1'
145
+ - !ruby/object:Gem::Dependency
146
+ name: thor
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '0.19'
152
+ type: :runtime
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '0.19'
159
+ - !ruby/object:Gem::Dependency
160
+ name: websocket-driver
161
+ requirement: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '0.7'
166
+ type: :runtime
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '0.7'
173
+ description: A gem to monitor TLS certificates using Cert Stream.
174
+ email:
175
+ - jef@nonblocking.info
176
+ executables:
177
+ - canari
178
+ extensions: []
179
+ extra_rdoc_files: []
180
+ files:
181
+ - ".gitignore"
182
+ - ".ruby-version"
183
+ - ".travis.yml"
184
+ - Gemfile
185
+ - Gemfile.lock
186
+ - LICENSE.txt
187
+ - README.md
188
+ - Rakefile
189
+ - bin/canari
190
+ - canari.gemspec
191
+ - docker-compose.yml
192
+ - lib/canari.rb
193
+ - lib/canari/cert_stream.rb
194
+ - lib/canari/cli.rb
195
+ - lib/canari/domain_cache.rb
196
+ - lib/canari/notifier.rb
197
+ - lib/canari/version.rb
198
+ homepage: https://github.com/jefmathiot/canari
199
+ licenses:
200
+ - MIT
201
+ metadata: {}
202
+ post_install_message:
203
+ rdoc_options: []
204
+ require_paths:
205
+ - lib
206
+ required_ruby_version: !ruby/object:Gem::Requirement
207
+ requirements:
208
+ - - ">="
209
+ - !ruby/object:Gem::Version
210
+ version: '0'
211
+ required_rubygems_version: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ requirements: []
217
+ rubygems_version: 3.0.1
218
+ signing_key:
219
+ specification_version: 4
220
+ summary: A gem to monitor TLS certificates using Cert Stream.
221
+ test_files: []