ukemi 0.1.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|