ukemi 0.1.0 → 0.4.1

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.
data/exe/ukemi CHANGED
@@ -5,4 +5,6 @@ $LOAD_PATH.unshift("#{__dir__}/../lib")
5
5
 
6
6
  require "ukemi"
7
7
 
8
- Ukemi::CLI.start
8
+ ARGV.unshift(Ukemi::CLI.default_task) unless Ukemi::CLI.all_tasks.key?(ARGV[0])
9
+
10
+ Ukemi::CLI.start(ARGV)
data/lib/ukemi.rb CHANGED
@@ -22,10 +22,14 @@ require "ukemi/record"
22
22
  require "ukemi/services/service"
23
23
 
24
24
  require "ukemi/services/circl"
25
+ require "ukemi/services/dnsdb"
26
+ require "ukemi/services/otx"
25
27
  require "ukemi/services/passivetotal"
26
28
  require "ukemi/services/securitytrails"
27
29
  require "ukemi/services/virustotal"
28
30
 
29
31
  require "ukemi/moderator"
30
32
 
33
+ require "ukemi/configuration"
34
+
31
35
  require "ukemi/cli"
data/lib/ukemi/cli.rb CHANGED
@@ -6,16 +6,32 @@ 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
14
17
 
18
+ default_command :lookup
19
+
15
20
  no_commands do
16
21
  def refang(data)
17
22
  data.gsub("[.]", ".").gsub("(.)", ".")
18
23
  end
24
+
25
+ def set_ordering(order_by)
26
+ parts = order_by.split("-")
27
+ ordering_key = parts.last
28
+ sort_order = parts.length == 2 ? "DESC" : "ASC"
29
+
30
+ Ukemi.configure do |config|
31
+ config.ordering_key = ordering_key
32
+ config.sort_order = sort_order
33
+ end
34
+ end
19
35
  end
20
36
  end
21
37
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ukemi
4
+ class Configuration
5
+ attr_accessor :ordering_key, :sort_order
6
+
7
+ def initialize
8
+ @ordering_key = "last_seen"
9
+ @sort_order = "DESC"
10
+ end
11
+ end
12
+
13
+ class << self
14
+ def configuration
15
+ @configuration ||= Configuration.new
16
+ end
17
+
18
+ attr_writer :configuration
19
+
20
+ def configure
21
+ yield configuration
22
+ end
23
+ end
24
+ 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
@@ -12,34 +13,63 @@ module Ukemi
12
13
 
13
14
  begin
14
15
  service.lookup data
15
- rescue ::PassiveTotal::Error, ::VirusTotal::Error, ::SecurityTrails::Error, PassiveCIRCL::Error
16
+ rescue ::PassiveTotal::Error, ::VirusTotal::Error, ::SecurityTrails::Error, PassiveCIRCL::Error, DNSDB::Error, Faraday::Error
16
17
  nil
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
- source: record.source,
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.filter_map { |record| convert_to_unixtime record[:first_seen] }
37
+ last_seens = sources.filter_map { |record| convert_to_unixtime record[:last_seen] }
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[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
data/lib/ukemi/record.rb CHANGED
@@ -2,10 +2,7 @@
2
2
 
3
3
  module Ukemi
4
4
  class Record
5
- attr_reader :data
6
- attr_reader :first_seen
7
- attr_reader :last_seen
8
- attr_reader :source
5
+ attr_reader :data, :first_seen, :last_seen, :source
9
6
 
10
7
  def initialize(data:, first_seen: nil, last_seen: nil, source: nil)
11
8
  @data = data
@@ -8,7 +8,7 @@ module Ukemi
8
8
  private
9
9
 
10
10
  def config_keys
11
- %w(CIRCL_PASSIVE_USERNAME CIRCL_PASSIVE_PASSWORD)
11
+ %w[CIRCL_PASSIVE_USERNAME CIRCL_PASSIVE_PASSWORD]
12
12
  end
13
13
 
14
14
  def api
@@ -26,14 +26,14 @@ module Ukemi
26
26
  def passive_dns_lookup(data, key = nil)
27
27
  results = api.dns.query(data)
28
28
  results = results.select do |result|
29
- result.dig("rrtype") == "A"
29
+ result["rrtype"] == "A"
30
30
  end
31
31
 
32
32
  results.map do |result|
33
33
  Record.new(
34
- data: result.dig(key),
35
- first_seen: Time.at(result.dig("time_first")).to_date.to_s,
36
- last_seen: Time.at(result.dig("time_last")).to_date.to_s,
34
+ data: result[key],
35
+ first_seen: Time.at(result["time_first"]).to_date.to_s,
36
+ last_seen: Time.at(result["time_last"]).to_date.to_s,
37
37
  source: name
38
38
  )
39
39
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "dnsdb"
5
+
6
+ module Ukemi
7
+ module Services
8
+ class DNSDB < Service
9
+ private
10
+
11
+ def config_keys
12
+ %w[DNSDB_API_KEY]
13
+ end
14
+
15
+ def api
16
+ @api ||= ::DNSDB::API.new
17
+ end
18
+
19
+ def lookup_by_ip(data)
20
+ results = api.lookup.rdata(type: "ip", value: data, rrtype: "A")
21
+ results.map do |result|
22
+ rrname = result["rrname"]
23
+ # Remove the last dot (e.g. "example.com.")
24
+ data = rrname[0..-2]
25
+ Record.new(
26
+ data: data,
27
+ first_seen: Time.at(result["time_first"]).to_date.to_s,
28
+ last_seen: Time.at(result["time_last"]).to_date.to_s,
29
+ source: name
30
+ )
31
+ end
32
+ end
33
+
34
+ def lookup_by_domain(data)
35
+ results = api.lookup.rrset(owner_name: data, rrtype: "A")
36
+ results.map do |result|
37
+ first_seen = Time.at(result["time_first"]).to_date.to_s
38
+ last_seen = Time.at(result["time_last"]).to_date.to_s
39
+
40
+ values = result["rdata"] || []
41
+ values.map do |value|
42
+ Record.new(
43
+ data: value,
44
+ first_seen: first_seen,
45
+ last_seen: last_seen,
46
+ source: name
47
+ )
48
+ end
49
+ end.flatten
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "otx_ruby"
5
+
6
+ module Ukemi
7
+ module Services
8
+ class OTX < Service
9
+ private
10
+
11
+ def config_keys
12
+ %w[OTX_API_KEY]
13
+ end
14
+
15
+ def api_key
16
+ @api_key ||= ENV["OTX_API_KEY"]
17
+ end
18
+
19
+ def domain_client
20
+ @domain_client ||= ::OTX::Domain.new(api_key)
21
+ end
22
+
23
+ def ip_client
24
+ @ip_client ||= ::OTX::IP.new(api_key)
25
+ end
26
+
27
+ def lookup_by_ip(data)
28
+ records = ip_client.get_passive_dns(data)
29
+ memo = Hash.new { |h, k| h[k] = [] }
30
+ records.each do |record|
31
+ next if record.record_type != "A"
32
+
33
+ domain = record.hostname
34
+ memo[domain] << Date.parse(record.last).to_s
35
+ memo[domain] << Date.parse(record.first).to_s
36
+ end
37
+
38
+ memo.keys.map do |domain|
39
+ Record.new(
40
+ data: domain,
41
+ first_seen: memo[domain].min,
42
+ last_seen: memo[domain].max,
43
+ source: name
44
+ )
45
+ end
46
+ end
47
+
48
+ def lookup_by_domain(data)
49
+ records = domain_client.get_passive_dns(data)
50
+
51
+ memo = Hash.new { |h, k| h[k] = [] }
52
+ records.each do |record|
53
+ next if record.record_type != "A"
54
+ next if record.hostname != data
55
+
56
+ ip = record.address
57
+ memo[ip] << Date.parse(record.last).to_s
58
+ memo[ip] << Date.parse(record.first).to_s
59
+ end
60
+
61
+ memo.keys.map do |ip|
62
+ Record.new(
63
+ data: ip,
64
+ first_seen: memo[ip].min,
65
+ last_seen: memo[ip].max,
66
+ source: name
67
+ )
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -12,12 +12,12 @@ module Ukemi
12
12
  end
13
13
 
14
14
  def config_keys
15
- %w(PASSIVETOTAL_USERNAME PASSIVETOTAL_API_KEY)
15
+ %w[PASSIVETOTAL_USERNAME PASSIVETOTAL_API_KEY]
16
16
  end
17
17
 
18
18
  def lookup_by_ip(data)
19
19
  res = api.dns.passive(data)
20
- results = res.dig("results") || []
20
+ results = res["results"] || []
21
21
  convert_to_records results
22
22
  end
23
23
 
@@ -27,9 +27,9 @@ module Ukemi
27
27
 
28
28
  def convert_to_records(results)
29
29
  results.map do |result|
30
- data = result.dig("resolve")
31
- first_seen = result.dig("firstSeen").to_s.split.first
32
- last_seen = result.dig("lastSeen").to_s.split.first
30
+ data = result["resolve"]
31
+ first_seen = result["firstSeen"].to_s.split.first
32
+ last_seen = result["lastSeen"].to_s.split.first
33
33
  Record.new(
34
34
  data: data,
35
35
  first_seen: first_seen,
@@ -9,7 +9,7 @@ module Ukemi
9
9
  private
10
10
 
11
11
  def config_keys
12
- %w(SECURITYTRAILS_API_KEY)
12
+ %w[SECURITYTRAILS_API_KEY]
13
13
  end
14
14
 
15
15
  def api
@@ -17,9 +17,9 @@ module Ukemi
17
17
  end
18
18
 
19
19
  def lookup_by_ip(data)
20
- result = api.domains.search( filter: { ipv4: data })
21
- records = result.dig("records") || []
22
- hostnames = records.map { |record| record.dig("hostname") }
20
+ result = api.domains.search(filter: { ipv4: data })
21
+ records = result["records"] || []
22
+ hostnames = records.map { |record| record["hostname"] }
23
23
  hostnames.map do |hostname|
24
24
  Record.new(
25
25
  data: hostname,
@@ -32,24 +32,25 @@ module Ukemi
32
32
 
33
33
  def lookup_by_domain(data)
34
34
  result = api.history.get_all_dns_history(data, type: "a")
35
- records = result.dig("records") || []
36
- records.map do |record|
37
- 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
- )
35
+ records = result["records"] || []
36
+
37
+ memo = Hash.new { |h, k| h[k] = [] }
38
+ records.each do |record|
39
+ values = record["values"] || []
40
+ values.each do |value|
41
+ ip = value["ip"]
42
+ memo[ip] << record["first_seen"]
43
+ memo[ip] << record["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
@@ -9,7 +9,7 @@ module Ukemi
9
9
  private
10
10
 
11
11
  def config_keys
12
- %w(VIRUSTOTAL_API_KEY)
12
+ %w[VIRUSTOTAL_API_KEY]
13
13
  end
14
14
 
15
15
  def api
@@ -29,9 +29,9 @@ module Ukemi
29
29
  end
30
30
 
31
31
  def extract_attributes(response)
32
- data = response.dig("data") || []
32
+ data = response["data"] || []
33
33
  data.map do |item|
34
- item.dig("attributes") || []
34
+ item["attributes"] || []
35
35
  end
36
36
  end
37
37
 
@@ -39,8 +39,8 @@ module Ukemi
39
39
  memo = Hash.new { |h, k| h[k] = [] }
40
40
 
41
41
  attributes.each do |attribute|
42
- data = attribute.dig(key)
43
- date = Time.at(attribute.dig("date")).to_date.to_s
42
+ data = attribute[key]
43
+ date = Time.at(attribute["date"]).to_date.to_s
44
44
  memo[data] << date
45
45
  end
46
46