ukemi 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a77579f014e97cc048e95dc5488e228ee19baba62d5074dbc9bfcb5c75f9d568
4
- data.tar.gz: e156b9c6de521ea49d7a69725c724176a5d1739deee7bba32aef4e662c64ddae
3
+ metadata.gz: ad938a7cb405569da9b6ba12dfe4c1bfa7eb382a4e08d2b204a8a6705572c1b9
4
+ data.tar.gz: d9b4a43bc7837c92b8fa692f7ecd12ba0f6de5aeda82f5066ea868b8b01b73e3
5
5
  SHA512:
6
- metadata.gz: f4e3eb6f8f4dd2223ba2eed1846ff8d95ee8d14913f5f6e682269489ee9348fea3af858c9bf849bac4d612a42a0a3c1c7bae3b27dde3461b1104c3e0e3752964
7
- data.tar.gz: 34467c49986dc11f2ac4fae646c1d96419ebe199838fe20a8f859c1619137ce2f3d0bcfc11e1f356545e14f63a6b5ff81b7ad9f39b206b70469959a8ca9a5986
6
+ metadata.gz: 31c864b566fe92d6ca5b71691c3517d95e29922ba704a7cd483c4df96a6731a7b42523d8102ea76b946ec1bc5068684475b4f60648064a2f466a41e70f014952
7
+ data.tar.gz: 561d1bd6ed05ec3392c2137a1b73df0a95ad1e20e0ccc66f354ce1a7725603fa7a50572bcbecb96edf4f932f9717b8520a35cd0879ff7e801d60e4dcc16e5486
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # ukemi
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/ukemi.svg)](https://badge.fury.io/rb/ukemi)
3
4
  [![Build Status](https://travis-ci.com/ninoseki/ukemi.svg?branch=master)](https://travis-ci.com/ninoseki/ukemi)
4
5
  [![Coverage Status](https://coveralls.io/repos/github/ninoseki/ukemi/badge.svg?branch=master)](https://coveralls.io/github/ninoseki/ukemi?branch=master)
5
6
  [![CodeFactor](https://www.codefactor.io/repository/github/ninoseki/ukemi/badge)](https://www.codefactor.io/repository/github/ninoseki/ukemi)
@@ -15,7 +16,7 @@ It supports the following services.
15
16
 
16
17
  It outputs passive DNS resolutions as JSON.
17
18
 
18
- ## Instalattion
19
+ ## Installation
19
20
 
20
21
  ```bash
21
22
  gem install ukemi
@@ -46,375 +47,138 @@ $ ukemi help looup
46
47
  Usage:
47
48
  ukemi lookup [IP|DOMAIN]
48
49
 
49
- Lookup passive DNS services
50
+ Options:
51
+ [--order-by=ORDER_BY] # Ordering of the passve DNS resolutions (last_seen or first_seen)
52
+ # Default: -last_seen
53
+
54
+ Lookup passive DNS servicess
50
55
  ```
51
56
 
52
57
  ```bash
53
- $ ukemi lookup circl.lu
58
+ $ ukemi lookup example.com
54
59
  {
55
- "149.13.33.14": [
56
- {
57
- "firtst_seen": "2016-10-07",
58
- "last_seen": "2018-10-26",
59
- "source": "CIRCL"
60
- },
61
- {
62
- "firtst_seen": "2017-05-26",
63
- "last_seen": "2020-03-15",
64
- "source": "SecurityTrails"
65
- },
66
- {
67
- "firtst_seen": "2019-12-04",
68
- "last_seen": "2019-12-04",
69
- "source": "VirusTotal"
70
- }
71
- ],
72
- "149.13.33.4": [
73
- {
74
- "firtst_seen": "2011-03-08",
75
- "last_seen": "2012-02-13",
76
- "source": "CIRCL"
77
- },
78
- {
79
- "firtst_seen": "2013-07-30",
80
- "last_seen": "2013-07-30",
81
- "source": "VirusTotal"
82
- }
83
- ],
84
- "194.154.205.24": [
85
- {
86
- "firtst_seen": "2011-03-03",
87
- "last_seen": "2011-03-03",
88
- "source": "CIRCL"
89
- }
90
- ]
60
+ "93.184.216.34": {
61
+ "first_seen": "2016-03-01",
62
+ "last_seen": "2020-03-16",
63
+ "sources": [
64
+ {
65
+ "first_seen": "2016-10-07",
66
+ "last_seen": "2018-10-30",
67
+ "source": "CIRCL"
68
+ },
69
+ {
70
+ "first_seen": "2016-03-01",
71
+ "last_seen": "2020-03-16",
72
+ "source": "SecurityTrails"
73
+ },
74
+ {
75
+ "first_seen": "2020-03-03",
76
+ "last_seen": "2020-03-03",
77
+ "source": "VirusTotal"
78
+ }
79
+ ]
80
+ },
81
+ ...
91
82
  }
92
83
 
93
84
  $ ukemi lookup 195.123.226.243
94
85
  {
95
- "liankt.club": [
96
- {
97
- "firtst_seen": "2020-02-15",
98
- "last_seen": "2020-03-13",
99
- "source": "PassiveTotal"
100
- },
101
- {
102
- "firtst_seen": "2020-02-16",
103
- "last_seen": "2020-02-16",
104
- "source": "VirusTotal"
105
- }
106
- ],
107
- "weidt.club": [
108
- {
109
- "firtst_seen": "2020-03-12",
110
- "last_seen": "2020-03-12",
111
- "source": "PassiveTotal"
112
- }
113
- ],
114
- "jikt.club": [
115
- {
116
- "firtst_seen": "2020-03-04",
117
- "last_seen": "2020-03-12",
118
- "source": "PassiveTotal"
119
- },
120
- {
121
- "firtst_seen": "2020-03-05",
122
- "last_seen": "2020-03-05",
123
- "source": "VirusTotal"
124
- }
125
- ],
126
- "biesi.club": [
127
- {
128
- "firtst_seen": "2020-02-15",
129
- "last_seen": "2020-03-12",
130
- "source": "PassiveTotal"
131
- },
132
- {
133
- "firtst_seen": "2020-02-20",
134
- "last_seen": "2020-02-20",
135
- "source": "VirusTotal"
136
- }
137
- ],
138
- "kaikt.club": [
139
- {
140
- "firtst_seen": "2020-02-15",
141
- "last_seen": "2020-03-12",
142
- "source": "PassiveTotal"
143
- },
144
- {
145
- "firtst_seen": "2020-02-21",
146
- "last_seen": "2020-02-21",
147
- "source": "VirusTotal"
148
- }
149
- ],
150
- "zhaokt.club": [
151
- {
152
- "firtst_seen": "2020-02-15",
153
- "last_seen": "2020-03-11",
154
- "source": "PassiveTotal"
155
- },
156
- {
157
- "firtst_seen": "2020-02-18",
158
- "last_seen": "2020-02-18",
159
- "source": "VirusTotal"
160
- }
161
- ],
162
- "yangdt.club": [
163
- {
164
- "firtst_seen": "2020-02-26",
165
- "last_seen": "2020-03-10",
166
- "source": "PassiveTotal"
167
- },
168
- {
169
- "firtst_seen": "2020-02-27",
170
- "last_seen": "2020-02-27",
171
- "source": "VirusTotal"
172
- }
173
- ],
174
- "jinkt.club": [
175
- {
176
- "firtst_seen": "2020-02-21",
177
- "last_seen": "2020-03-10",
178
- "source": "PassiveTotal"
179
- },
180
- {
181
- "firtst_seen": "2020-02-22",
182
- "last_seen": "2020-02-22",
183
- "source": "VirusTotal"
184
- }
185
- ],
186
- "taokt.club": [
187
- {
188
- "firtst_seen": "2020-03-10",
189
- "last_seen": "2020-03-10",
190
- "source": "PassiveTotal"
191
- }
192
- ],
193
- "xinkt.club": [
194
- {
195
- "firtst_seen": "2020-02-17",
196
- "last_seen": "2020-03-09",
197
- "source": "PassiveTotal"
198
- },
199
- {
200
- "firtst_seen": "2020-02-19",
201
- "last_seen": "2020-02-19",
202
- "source": "VirusTotal"
203
- }
204
- ],
205
- "mail.realty-advertising.ru": [
206
- {
207
- "firtst_seen": "2019-11-08",
208
- "last_seen": "2020-03-09",
209
- "source": "PassiveTotal"
210
- }
211
- ],
212
- "realty-advertising.ru": [
213
- {
214
- "firtst_seen": "2019-11-08",
215
- "last_seen": "2020-03-06",
216
- "source": "PassiveTotal"
217
- }
218
- ],
219
- "ns1.realty-advertising.ru": [
220
- {
221
- "firtst_seen": "2019-12-02",
222
- "last_seen": "2020-03-04",
223
- "source": "PassiveTotal"
224
- }
225
- ],
226
- "ns2.realty-advertising.ru": [
227
- {
228
- "firtst_seen": "2019-12-04",
229
- "last_seen": "2020-03-04",
230
- "source": "PassiveTotal"
231
- }
232
- ],
233
- "xiankt.club": [
234
- {
235
- "firtst_seen": "2020-02-15",
236
- "last_seen": "2020-03-03",
237
- "source": "PassiveTotal"
238
- },
239
- {
240
- "firtst_seen": "2020-02-16",
241
- "last_seen": "2020-02-16",
242
- "source": "VirusTotal"
243
- }
244
- ],
245
- "nittsu-si.com": [
246
- {
247
- "firtst_seen": "2020-02-15",
248
- "last_seen": "2020-03-03",
249
- "source": "PassiveTotal"
250
- },
251
- {
252
- "firtst_seen": "2020-02-21",
253
- "last_seen": "2020-02-21",
254
- "source": "VirusTotal"
255
- }
256
- ],
257
- "mailer.realty-advertising.ru": [
258
- {
259
- "firtst_seen": "2020-02-23",
260
- "last_seen": "2020-02-23",
261
- "source": "PassiveTotal"
262
- }
263
- ],
264
- "mail7.realty-advertising.ru": [
265
- {
266
- "firtst_seen": "2020-02-23",
267
- "last_seen": "2020-02-23",
268
- "source": "PassiveTotal"
269
- }
270
- ],
271
- "zimbra.realty-advertising.ru": [
272
- {
273
- "firtst_seen": "2020-02-23",
274
- "last_seen": "2020-02-23",
275
- "source": "PassiveTotal"
276
- }
277
- ],
278
- "relay2.realty-advertising.ru": [
279
- {
280
- "firtst_seen": "2020-02-23",
281
- "last_seen": "2020-02-23",
282
- "source": "PassiveTotal"
283
- }
284
- ],
285
- "sniper.realty-advertising.ru": [
286
- {
287
- "firtst_seen": "2020-02-22",
288
- "last_seen": "2020-02-22",
289
- "source": "PassiveTotal"
290
- }
291
- ],
292
- "mailx.realty-advertising.ru": [
293
- {
294
- "firtst_seen": "2020-02-22",
295
- "last_seen": "2020-02-22",
296
- "source": "PassiveTotal"
297
- }
298
- ],
299
- "send.realty-advertising.ru": [
300
- {
301
- "firtst_seen": "2020-02-22",
302
- "last_seen": "2020-02-22",
303
- "source": "PassiveTotal"
304
- }
305
- ],
306
- "mta.realty-advertising.ru": [
307
- {
308
- "firtst_seen": "2020-02-22",
309
- "last_seen": "2020-02-22",
310
- "source": "PassiveTotal"
311
- }
312
- ],
313
- "home.realty-advertising.ru": [
314
- {
315
- "firtst_seen": "2020-02-22",
316
- "last_seen": "2020-02-22",
317
- "source": "PassiveTotal"
318
- }
319
- ],
320
- "pbrand.realty-advertising.ru": [
321
- {
322
- "firtst_seen": "2020-02-22",
323
- "last_seen": "2020-02-22",
324
- "source": "PassiveTotal"
325
- }
326
- ],
327
- "smtpauth.realty-advertising.ru": [
328
- {
329
- "firtst_seen": "2020-02-22",
330
- "last_seen": "2020-02-22",
331
- "source": "PassiveTotal"
332
- }
333
- ],
334
- "gate.realty-advertising.ru": [
335
- {
336
- "firtst_seen": "2020-02-21",
337
- "last_seen": "2020-02-21",
338
- "source": "PassiveTotal"
339
- }
340
- ],
341
- "mx02.realty-advertising.ru": [
342
- {
343
- "firtst_seen": "2020-02-21",
344
- "last_seen": "2020-02-21",
345
- "source": "PassiveTotal"
346
- }
347
- ],
348
- "outmail.realty-advertising.ru": [
349
- {
350
- "firtst_seen": "2020-02-21",
351
- "last_seen": "2020-02-21",
352
- "source": "PassiveTotal"
353
- }
354
- ],
355
- "exchange.realty-advertising.ru": [
356
- {
357
- "firtst_seen": "2020-02-21",
358
- "last_seen": "2020-02-21",
359
- "source": "PassiveTotal"
360
- }
361
- ],
362
- "ms.realty-advertising.ru": [
363
- {
364
- "firtst_seen": "2020-02-21",
365
- "last_seen": "2020-02-21",
366
- "source": "PassiveTotal"
367
- }
368
- ],
369
- "owa.realty-advertising.ru": [
370
- {
371
- "firtst_seen": "2020-02-20",
372
- "last_seen": "2020-02-20",
373
- "source": "PassiveTotal"
374
- }
375
- ],
376
- "mail8.realty-advertising.ru": [
377
- {
378
- "firtst_seen": "2020-02-20",
379
- "last_seen": "2020-02-20",
380
- "source": "PassiveTotal"
381
- }
382
- ],
383
- "mta-sts.realty-advertising.ru": [
384
- {
385
- "firtst_seen": "2019-11-11",
386
- "last_seen": "2020-02-08",
387
- "source": "PassiveTotal"
388
- }
389
- ],
390
- "mail02.realty-advertising.ru": [
391
- {
392
- "firtst_seen": "2020-01-18",
393
- "last_seen": "2020-01-18",
394
- "source": "PassiveTotal"
395
- }
396
- ],
397
- "www.realty-advertising.ru": [
398
- {
399
- "firtst_seen": "2019-11-08",
400
- "last_seen": "2019-11-12",
401
- "source": "PassiveTotal"
402
- }
403
- ],
404
- "ln-048.rd-00003024.id-11744955.v0.tun.vpnoverdns.com": [
405
- {
406
- "firtst_seen": "2017-04-06",
407
- "last_seen": "2017-04-06",
408
- "source": "PassiveTotal"
409
- }
410
- ],
411
- "mnen6k7g.info": [
412
- {
413
- "firtst_seen": "2010-10-28",
414
- "last_seen": "2010-10-28",
415
- "source": "PassiveTotal"
416
- }
417
- ]
86
+ "example.org": {
87
+ "first_seen": "2011-04-11",
88
+ "last_seen": "2020-03-16",
89
+ "sources": [
90
+ {
91
+ "first_seen": "2011-04-11",
92
+ "last_seen": "2011-04-11",
93
+ "source": "CIRCL"
94
+ },
95
+ {
96
+ "first_seen": "2016-10-09",
97
+ "last_seen": "2018-10-28",
98
+ "source": "CIRCL"
99
+ },
100
+ {
101
+ "first_seen": "2014-12-09",
102
+ "last_seen": "2020-03-16",
103
+ "source": "PassiveTotal"
104
+ },
105
+ {
106
+ "first_seen": null,
107
+ "last_seen": null,
108
+ "source": "SecurityTrails"
109
+ }
110
+ ]
111
+ },
112
+ ...
113
+ }
114
+
115
+ # You can specify the order of resolutions
116
+
117
+ # Order by last_seen DESC
118
+ $ ukemi lookup example.com --order-by -last_seen
119
+
120
+ # Order by last_seen ASC
121
+ $ ukemi lookup example.com --order-by last_seen
122
+
123
+ # Order by first_seen DESC
124
+ $ ukemi lookup example.com --order-by -first_seen
125
+
126
+ # Order by first_seen ASC
127
+ $ ukemi lookup example.com --order-by first_seen
128
+ ```
129
+
130
+ ### Using with jq
131
+
132
+ [jq](https://stedolan.github.io/jq/)'s powerful processor helps to interact with the output.
133
+
134
+ ```bash
135
+ # List up resolutions only
136
+ $ ukemi lookup example.com | jq "keys"
137
+ [
138
+ "192.0.32.10",
139
+ "192.0.43.10",
140
+ "208.77.188.166",
141
+ "209.67.208.202",
142
+ "221.121.159.162",
143
+ "93.184.216.119",
144
+ "93.184.216.34"
145
+ ]
146
+
147
+ # List up the first 2 objects
148
+ $ ukemi lookup example.com | jq "to_entries | .[:2] | from_entries"
149
+ {
150
+ "93.184.216.34": {
151
+ "first_seen": "2016-03-01",
152
+ "last_seen": "2020-03-16",
153
+ "sources": [
154
+ {
155
+ "first_seen": "2016-10-07",
156
+ "last_seen": "2018-10-30",
157
+ "source": "CIRCL"
158
+ },
159
+ {
160
+ "first_seen": "2016-03-01",
161
+ "last_seen": "2020-03-16",
162
+ "source": "SecurityTrails"
163
+ },
164
+ {
165
+ "first_seen": "2020-03-03",
166
+ "last_seen": "2020-03-03",
167
+ "source": "VirusTotal"
168
+ }
169
+ ]
170
+ },
171
+ "221.121.159.162": {
172
+ "first_seen": "2019-11-04",
173
+ "last_seen": "2019-11-04",
174
+ "sources": [
175
+ {
176
+ "first_seen": "2019-11-04",
177
+ "last_seen": "2019-11-04",
178
+ "source": "VirusTotal"
179
+ }
180
+ ]
181
+ }
418
182
  }
419
183
  ```
420
184
 
@@ -28,4 +28,6 @@ require "ukemi/services/virustotal"
28
28
 
29
29
  require "ukemi/moderator"
30
30
 
31
+ require "ukemi/configuration"
32
+
31
33
  require "ukemi/cli"
@@ -6,8 +6,11 @@ require "thor"
6
6
  module Ukemi
7
7
  class CLI < Thor
8
8
  desc "lookup [IP|DOMAIN]", "Lookup passive DNS services"
9
+ method_option :order_by, type: :string, desc: "Ordering of the passve DNS resolutions (last_seen or first_seen)", default: "-last_seen"
9
10
  def lookup(data)
10
11
  data = refang(data)
12
+ set_ordering options["order_by"]
13
+
11
14
  result = Moderator.lookup(data)
12
15
  puts JSON.pretty_generate(result)
13
16
  end
@@ -16,6 +19,17 @@ module Ukemi
16
19
  def refang(data)
17
20
  data.gsub("[.]", ".").gsub("(.)", ".")
18
21
  end
22
+
23
+ def set_ordering(order_by)
24
+ parts = order_by.split("-")
25
+ ordering_key = parts.last
26
+ sort_order = parts.length == 2 ? "DESC" : "ASC"
27
+
28
+ Ukemi.configure do |config|
29
+ config.ordering_key = ordering_key
30
+ config.sort_order = sort_order
31
+ end
32
+ end
19
33
  end
20
34
  end
21
35
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ukemi
4
+ class Configuration
5
+ attr_accessor :ordering_key
6
+ attr_accessor :sort_order
7
+
8
+ def initialize
9
+ @ordering_key = "last_seen"
10
+ @sort_order = "DESC"
11
+ end
12
+ end
13
+
14
+ class << self
15
+ def configuration
16
+ @configuration ||= Configuration.new
17
+ end
18
+
19
+ attr_writer :configuration
20
+
21
+ def configure
22
+ yield configuration
23
+ end
24
+ end
25
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "parallel"
4
4
  require "time"
5
+ require "date"
5
6
 
6
7
  module Ukemi
7
8
  class Moderator
@@ -17,29 +18,58 @@ module Ukemi
17
18
  end
18
19
  end.flatten.compact
19
20
 
21
+ format records
22
+ end
23
+
24
+ def format(records)
20
25
  memo = Hash.new { |h, k| h[k] = [] }
26
+
21
27
  records.each do |record|
22
28
  memo[record.data] << {
23
- firtst_seen: record.first_seen,
29
+ first_seen: record.first_seen,
24
30
  last_seen: record.last_seen,
25
31
  source: record.source,
26
32
  }
27
33
  end
34
+ # Merge first seen last seen and make the sources a list.
35
+ formatted = memo.map do |key, sources|
36
+ first_seens = sources.map { |record| convert_to_unixtime record.dig(:first_seen) }.compact
37
+ last_seens = sources.map { |record| convert_to_unixtime record.dig(:last_seen) }.compact
38
+ [
39
+ key,
40
+ {
41
+ first_seen: convert_to_date(first_seens.min),
42
+ last_seen: convert_to_date(last_seens.max),
43
+ sources: sources
44
+ }
45
+ ]
46
+ end.to_h
28
47
 
29
- memo.sort_by do |_key, array|
30
- last_seens = array.map do |record|
31
- parse_to_unixtime record.dig(:last_seen)
48
+ # Sorting
49
+ ordering_key = Ukemi.configuration.ordering_key.to_sym
50
+ sort_order = Ukemi.configuration.sort_order
51
+ formatted.sort_by do |_key, hash|
52
+ value = hash.dig(ordering_key)
53
+ if sort_order == "DESC"
54
+ value ? -convert_to_unixtime(value) : -1
55
+ else
56
+ value ? convert_to_unixtime(value) : Float::MAX.to_i
32
57
  end
33
- -last_seens.max
34
58
  end.to_h
35
59
  end
36
60
 
37
- def parse_to_unixtime(date)
38
- return -1 unless date
61
+ def convert_to_unixtime(date)
62
+ return nil unless date
39
63
 
40
64
  Time.parse(date).to_i
41
65
  end
42
66
 
67
+ def convert_to_date(time)
68
+ return nil unless time
69
+
70
+ Time.at(time).to_date.to_s
71
+ end
72
+
43
73
  class << self
44
74
  def lookup(data)
45
75
  new.lookup data
@@ -33,23 +33,24 @@ module Ukemi
33
33
  def lookup_by_domain(data)
34
34
  result = api.history.get_all_dns_history(data, type: "a")
35
35
  records = result.dig("records") || []
36
- records.map do |record|
36
+
37
+ memo = Hash.new { |h, k| h[k] = [] }
38
+ records.each do |record|
37
39
  values = record.dig("values") || []
38
- values.map do |value|
39
- Record.new(
40
- data: value.dig("ip"),
41
- first_seen: record.dig("first_seen"),
42
- last_seen: record.dig("last_seen"),
43
- source: name
44
- )
40
+ values.each do |value|
41
+ ip = value.dig("ip")
42
+ memo[ip] << record.dig("first_seen")
43
+ memo[ip] << record.dig("last_seen")
45
44
  end
46
- end.flatten
47
- end
45
+ end
48
46
 
49
- def extract_attributes(response)
50
- data = response.dig("data") || []
51
- data.map do |item|
52
- item.dig("attributes") || []
47
+ memo.keys.map do |ip|
48
+ Record.new(
49
+ data: ip,
50
+ first_seen: memo[ip].min,
51
+ last_seen: memo[ip].max,
52
+ source: name
53
+ )
53
54
  end
54
55
  end
55
56
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ukemi
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ukemi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Manabu Niseki
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-03-15 00:00:00.000000000 Z
11
+ date: 2020-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -240,6 +240,7 @@ files:
240
240
  - exe/ukemi
241
241
  - lib/ukemi.rb
242
242
  - lib/ukemi/cli.rb
243
+ - lib/ukemi/configuration.rb
243
244
  - lib/ukemi/error.rb
244
245
  - lib/ukemi/moderator.rb
245
246
  - lib/ukemi/record.rb