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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yaml +27 -0
- data/.overcommit.yml +9 -0
- data/.standard.yml +4 -0
- data/README.md +140 -364
- data/exe/ukemi +3 -1
- data/lib/ukemi.rb +4 -0
- data/lib/ukemi/cli.rb +16 -0
- data/lib/ukemi/configuration.rb +24 -0
- data/lib/ukemi/moderator.rb +39 -9
- data/lib/ukemi/record.rb +1 -4
- data/lib/ukemi/services/circl.rb +5 -5
- data/lib/ukemi/services/dnsdb.rb +53 -0
- data/lib/ukemi/services/otx.rb +72 -0
- data/lib/ukemi/services/passivetotal.rb +5 -5
- data/lib/ukemi/services/securitytrails.rb +21 -20
- data/lib/ukemi/services/virustotal.rb +5 -5
- data/lib/ukemi/version.rb +1 -1
- data/ukemi.gemspec +24 -20
- metadata +86 -25
- data/.travis.yml +0 -6
data/exe/ukemi
CHANGED
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
|
data/lib/ukemi/moderator.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
38
|
-
return
|
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
|
data/lib/ukemi/services/circl.rb
CHANGED
@@ -8,7 +8,7 @@ module Ukemi
|
|
8
8
|
private
|
9
9
|
|
10
10
|
def config_keys
|
11
|
-
%w
|
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
|
29
|
+
result["rrtype"] == "A"
|
30
30
|
end
|
31
31
|
|
32
32
|
results.map do |result|
|
33
33
|
Record.new(
|
34
|
-
data: result
|
35
|
-
first_seen: Time.at(result
|
36
|
-
last_seen: Time.at(result
|
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
|
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
|
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
|
31
|
-
first_seen = result
|
32
|
-
last_seen = result
|
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
|
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(
|
21
|
-
records = result
|
22
|
-
hostnames = records.map { |record| record
|
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
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
47
|
-
end
|
45
|
+
end
|
48
46
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
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
|
32
|
+
data = response["data"] || []
|
33
33
|
data.map do |item|
|
34
|
-
item
|
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
|
43
|
-
date = Time.at(attribute
|
42
|
+
data = attribute[key]
|
43
|
+
date = Time.at(attribute["date"]).to_date.to_s
|
44
44
|
memo[data] << date
|
45
45
|
end
|
46
46
|
|