mihari 2.4.0 → 3.0.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 +4 -4
- data/.gitignore +7 -0
- data/.overcommit.yml +12 -0
- data/README.md +1 -9
- data/exe/mihari +1 -1
- data/lib/mihari.rb +88 -15
- data/lib/mihari/analyzers/base.rb +49 -8
- data/lib/mihari/analyzers/basic.rb +1 -2
- data/lib/mihari/analyzers/binaryedge.rb +7 -13
- data/lib/mihari/analyzers/censys.rb +26 -63
- data/lib/mihari/analyzers/circl.rb +20 -17
- data/lib/mihari/analyzers/crtsh.rb +6 -13
- data/lib/mihari/analyzers/dnpedia.rb +6 -12
- data/lib/mihari/analyzers/dnstwister.rb +13 -10
- data/lib/mihari/analyzers/onyphe.rb +6 -12
- data/lib/mihari/analyzers/otx.rb +22 -19
- data/lib/mihari/analyzers/passivetotal.rb +22 -21
- data/lib/mihari/analyzers/pulsedive.rb +16 -13
- data/lib/mihari/analyzers/rule.rb +99 -0
- data/lib/mihari/analyzers/securitytrails.rb +22 -19
- data/lib/mihari/analyzers/shodan.rb +7 -13
- data/lib/mihari/analyzers/spyse.rb +12 -19
- data/lib/mihari/analyzers/urlscan.rb +22 -27
- data/lib/mihari/analyzers/virustotal.rb +25 -22
- data/lib/mihari/analyzers/zoomeye.rb +14 -20
- data/lib/mihari/cli/analyzer.rb +44 -0
- data/lib/mihari/cli/base.rb +27 -0
- data/lib/mihari/cli/init.rb +13 -0
- data/lib/mihari/cli/main.rb +30 -0
- data/lib/mihari/cli/mixins/utils.rb +88 -0
- data/lib/mihari/cli/validator.rb +11 -0
- data/lib/mihari/commands/binaryedge.rb +1 -1
- data/lib/mihari/commands/censys.rb +1 -1
- data/lib/mihari/commands/circl.rb +2 -2
- data/lib/mihari/commands/crtsh.rb +1 -1
- data/lib/mihari/commands/dnpedia.rb +1 -1
- data/lib/mihari/commands/dnstwister.rb +2 -2
- data/lib/mihari/commands/init.rb +46 -0
- data/lib/mihari/commands/json.rb +1 -1
- data/lib/mihari/commands/onyphe.rb +1 -1
- data/lib/mihari/commands/otx.rb +2 -2
- data/lib/mihari/commands/passivetotal.rb +2 -2
- data/lib/mihari/commands/pulsedive.rb +2 -2
- data/lib/mihari/commands/search.rb +77 -0
- data/lib/mihari/commands/securitytrails.rb +2 -2
- data/lib/mihari/commands/shodan.rb +1 -1
- data/lib/mihari/commands/spyse.rb +1 -1
- data/lib/mihari/commands/urlscan.rb +2 -2
- data/lib/mihari/commands/validator.rb +38 -0
- data/lib/mihari/commands/virustotal.rb +2 -2
- data/lib/mihari/commands/zoomeye.rb +1 -1
- data/lib/mihari/constraints.rb +5 -0
- data/lib/mihari/database.rb +13 -2
- data/lib/mihari/emitters/base.rb +2 -2
- data/lib/mihari/emitters/database.rb +1 -1
- data/lib/mihari/emitters/misp.rb +1 -1
- data/lib/mihari/emitters/slack.rb +5 -6
- data/lib/mihari/emitters/the_hive.rb +1 -1
- data/lib/mihari/emitters/webhook.rb +2 -9
- data/lib/mihari/mixins/configurable.rb +38 -0
- data/lib/mihari/mixins/configuration.rb +85 -0
- data/lib/mihari/mixins/hash.rb +20 -0
- data/lib/mihari/mixins/refang.rb +21 -0
- data/lib/mihari/mixins/retriable.rb +27 -0
- data/lib/mihari/mixins/rule.rb +79 -0
- data/lib/mihari/models/alert.rb +28 -1
- data/lib/mihari/models/artifact.rb +10 -0
- data/lib/mihari/notifiers/base.rb +9 -1
- data/lib/mihari/notifiers/exception_notifier.rb +50 -0
- data/lib/mihari/notifiers/slack.rb +29 -0
- data/lib/mihari/schemas/configuration.rb +42 -0
- data/lib/mihari/schemas/macros.rb +17 -0
- data/lib/mihari/schemas/rule.rb +72 -0
- data/lib/mihari/serializers/artifact.rb +1 -1
- data/lib/mihari/status.rb +14 -0
- data/lib/mihari/templates/rule.yml.erb +19 -0
- data/lib/mihari/type_checker.rb +8 -3
- data/lib/mihari/version.rb +1 -1
- data/lib/mihari/web/controllers/base_controller.rb +1 -1
- data/lib/mihari/web/public/index.html +1 -21
- data/lib/mihari/web/public/redoc-static.html +2 -2
- data/lib/mihari/web/public/static/js/app.ab213f7c.js +12 -0
- data/lib/mihari/web/public/static/js/app.ab213f7c.js.map +1 -0
- data/mihari.gemspec +12 -5
- metadata +123 -50
- data/.rubocop.yml +0 -161
- data/lib/mihari/analyzers/free_text.rb +0 -48
- data/lib/mihari/analyzers/http_hash.rb +0 -100
- data/lib/mihari/analyzers/passive_dns.rb +0 -59
- data/lib/mihari/analyzers/passive_ssl.rb +0 -55
- data/lib/mihari/analyzers/reverse_whois.rb +0 -55
- data/lib/mihari/analyzers/securitytrails_domain_feed.rb +0 -59
- data/lib/mihari/analyzers/ssh_fingerprint.rb +0 -58
- data/lib/mihari/cli.rb +0 -126
- data/lib/mihari/commands/config.rb +0 -27
- data/lib/mihari/commands/free_text.rb +0 -21
- data/lib/mihari/commands/http_hash.rb +0 -25
- data/lib/mihari/commands/passive_dns.rb +0 -21
- data/lib/mihari/commands/passive_ssl.rb +0 -21
- data/lib/mihari/commands/reverse_whois.rb +0 -21
- data/lib/mihari/commands/securitytrails_domain_feed.rb +0 -23
- data/lib/mihari/commands/ssh_fingerprint.rb +0 -21
- data/lib/mihari/config.rb +0 -85
- data/lib/mihari/configurable.rb +0 -21
- data/lib/mihari/html.rb +0 -43
- data/lib/mihari/retriable.rb +0 -17
|
@@ -5,26 +5,29 @@ require "passive_circl"
|
|
|
5
5
|
module Mihari
|
|
6
6
|
module Analyzers
|
|
7
7
|
class CIRCL < Base
|
|
8
|
-
|
|
8
|
+
include Mixins::Refang
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
param :query
|
|
11
|
+
option :title, default: proc { "CIRCL passive DNS/SSL search" }
|
|
12
|
+
option :description, default: proc { "query = #{query}" }
|
|
13
|
+
option :tags, default: proc { [] }
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
attr_reader :type
|
|
16
|
+
|
|
17
|
+
def initialize(*args, **kwargs)
|
|
18
|
+
super
|
|
15
19
|
|
|
16
|
-
@
|
|
17
|
-
@
|
|
18
|
-
@tags = tags
|
|
20
|
+
@query = refang(query)
|
|
21
|
+
@type = TypeChecker.type(query)
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
def artifacts
|
|
22
|
-
|
|
25
|
+
search || []
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
private
|
|
26
29
|
|
|
27
|
-
def
|
|
30
|
+
def configuration_keys
|
|
28
31
|
%w[circl_passive_password circl_passive_username]
|
|
29
32
|
end
|
|
30
33
|
|
|
@@ -32,26 +35,26 @@ module Mihari
|
|
|
32
35
|
@api ||= ::PassiveCIRCL::API.new(username: Mihari.config.circl_passive_username, password: Mihari.config.circl_passive_password)
|
|
33
36
|
end
|
|
34
37
|
|
|
35
|
-
def
|
|
38
|
+
def search
|
|
36
39
|
case @type
|
|
37
40
|
when "domain"
|
|
38
|
-
|
|
41
|
+
passive_dns_search
|
|
39
42
|
when "hash"
|
|
40
|
-
|
|
43
|
+
passive_ssl_search
|
|
41
44
|
else
|
|
42
45
|
raise InvalidInputError, "#{@query}(type: #{@type || "unknown"}) is not supported."
|
|
43
46
|
end
|
|
44
47
|
end
|
|
45
48
|
|
|
46
|
-
def
|
|
49
|
+
def passive_dns_search
|
|
47
50
|
results = api.dns.query(@query)
|
|
48
|
-
results.
|
|
51
|
+
results.filter_map do |result|
|
|
49
52
|
type = result["rrtype"]
|
|
50
53
|
type == "A" ? result["rdata"] : nil
|
|
51
|
-
end.
|
|
54
|
+
end.uniq
|
|
52
55
|
end
|
|
53
56
|
|
|
54
|
-
def
|
|
57
|
+
def passive_ssl_search
|
|
55
58
|
result = api.ssl.cquery(@query)
|
|
56
59
|
seen = result["seen"] || []
|
|
57
60
|
seen.uniq
|
|
@@ -5,22 +5,15 @@ require "crtsh"
|
|
|
5
5
|
module Mihari
|
|
6
6
|
module Analyzers
|
|
7
7
|
class Crtsh < Base
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@query = query
|
|
14
|
-
@title = title || "crt.sh lookup"
|
|
15
|
-
@description = description || "query = #{query}"
|
|
16
|
-
@tags = tags
|
|
17
|
-
|
|
18
|
-
@exclude_expired = exclude_expired.nil? ? true : exclude_expired
|
|
19
|
-
end
|
|
8
|
+
param :query
|
|
9
|
+
option :title, default: proc { "crt.sh search" }
|
|
10
|
+
option :description, default: proc { "query = #{query}" }
|
|
11
|
+
option :tags, default: proc { [] }
|
|
12
|
+
option :exclude_expired, default: proc { true }
|
|
20
13
|
|
|
21
14
|
def artifacts
|
|
22
15
|
results = search
|
|
23
|
-
name_values = results.
|
|
16
|
+
name_values = results.filter_map { |result| result["name_value"] }
|
|
24
17
|
name_values.map(&:lines).flatten.uniq.map(&:chomp)
|
|
25
18
|
end
|
|
26
19
|
|
|
@@ -5,19 +5,13 @@ require "dnpedia"
|
|
|
5
5
|
module Mihari
|
|
6
6
|
module Analyzers
|
|
7
7
|
class DNPedia < Base
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@query = query
|
|
14
|
-
@title = title || "DNPedia domain lookup"
|
|
15
|
-
@description = description || "query = #{query}"
|
|
16
|
-
@tags = tags
|
|
17
|
-
end
|
|
8
|
+
param :query
|
|
9
|
+
option :title, default: proc { "DNPedia domain search" }
|
|
10
|
+
option :description, default: proc { "query = #{query}" }
|
|
11
|
+
option :tags, default: proc { [] }
|
|
18
12
|
|
|
19
13
|
def artifacts
|
|
20
|
-
|
|
14
|
+
search || []
|
|
21
15
|
end
|
|
22
16
|
|
|
23
17
|
private
|
|
@@ -26,7 +20,7 @@ module Mihari
|
|
|
26
20
|
@api ||= ::DNPedia::API.new
|
|
27
21
|
end
|
|
28
22
|
|
|
29
|
-
def
|
|
23
|
+
def search
|
|
30
24
|
res = api.search(query)
|
|
31
25
|
rows = res["rows"] || []
|
|
32
26
|
rows.map do |row|
|
|
@@ -7,21 +7,24 @@ require "parallel"
|
|
|
7
7
|
module Mihari
|
|
8
8
|
module Analyzers
|
|
9
9
|
class DNSTwister < Base
|
|
10
|
-
|
|
10
|
+
include Mixins::Refang
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
param :query
|
|
13
|
+
option :title, default: proc { "dnstwister domain search" }
|
|
14
|
+
option :description, default: proc { "query = #{query}" }
|
|
15
|
+
option :tags, default: proc { [] }
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
attr_reader :type
|
|
18
|
+
|
|
19
|
+
def initialize(*args, **kwargs)
|
|
20
|
+
super
|
|
17
21
|
|
|
18
|
-
@
|
|
19
|
-
@
|
|
20
|
-
@tags = tags
|
|
22
|
+
@query = refang(query)
|
|
23
|
+
@type = TypeChecker.type(query)
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
def artifacts
|
|
24
|
-
|
|
27
|
+
search || []
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
private
|
|
@@ -41,7 +44,7 @@ module Mihari
|
|
|
41
44
|
false
|
|
42
45
|
end
|
|
43
46
|
|
|
44
|
-
def
|
|
47
|
+
def search
|
|
45
48
|
raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
|
46
49
|
|
|
47
50
|
res = api.fuzz(query)
|
|
@@ -5,16 +5,10 @@ require "onyphe"
|
|
|
5
5
|
module Mihari
|
|
6
6
|
module Analyzers
|
|
7
7
|
class Onyphe < Base
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@query = query
|
|
14
|
-
@title = title || "Onyphe lookup"
|
|
15
|
-
@description = description || "query = #{query}"
|
|
16
|
-
@tags = tags
|
|
17
|
-
end
|
|
8
|
+
param :query
|
|
9
|
+
option :title, default: proc { "Onyphe search" }
|
|
10
|
+
option :description, default: proc { "query = #{query}" }
|
|
11
|
+
option :tags, default: proc { [] }
|
|
18
12
|
|
|
19
13
|
def artifacts
|
|
20
14
|
results = search
|
|
@@ -24,14 +18,14 @@ module Mihari
|
|
|
24
18
|
result["results"]
|
|
25
19
|
end.flatten.compact
|
|
26
20
|
|
|
27
|
-
flat_results.
|
|
21
|
+
flat_results.filter_map { |result| result["ip"] }.uniq
|
|
28
22
|
end
|
|
29
23
|
|
|
30
24
|
private
|
|
31
25
|
|
|
32
26
|
PAGE_SIZE = 10
|
|
33
27
|
|
|
34
|
-
def
|
|
28
|
+
def configuration_keys
|
|
35
29
|
%w[onyphe_api_key]
|
|
36
30
|
end
|
|
37
31
|
|
data/lib/mihari/analyzers/otx.rb
CHANGED
|
@@ -5,26 +5,29 @@ require "otx_ruby"
|
|
|
5
5
|
module Mihari
|
|
6
6
|
module Analyzers
|
|
7
7
|
class OTX < Base
|
|
8
|
-
|
|
8
|
+
include Mixins::Refang
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
param :query
|
|
11
|
+
option :title, default: proc { "OTX search" }
|
|
12
|
+
option :description, default: proc { "query = #{query}" }
|
|
13
|
+
option :tags, default: proc { [] }
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
attr_reader :type
|
|
16
|
+
|
|
17
|
+
def initialize(*args, **kwargs)
|
|
18
|
+
super
|
|
15
19
|
|
|
16
|
-
@
|
|
17
|
-
@
|
|
18
|
-
@tags = tags
|
|
20
|
+
@query = refang(query)
|
|
21
|
+
@type = TypeChecker.type(query)
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
def artifacts
|
|
22
|
-
|
|
25
|
+
search || []
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
private
|
|
26
29
|
|
|
27
|
-
def
|
|
30
|
+
def configuration_keys
|
|
28
31
|
%w[otx_api_key]
|
|
29
32
|
end
|
|
30
33
|
|
|
@@ -40,29 +43,29 @@ module Mihari
|
|
|
40
43
|
%w[ip domain].include? type
|
|
41
44
|
end
|
|
42
45
|
|
|
43
|
-
def
|
|
46
|
+
def search
|
|
44
47
|
case type
|
|
45
48
|
when "domain"
|
|
46
|
-
|
|
49
|
+
domain_search
|
|
47
50
|
when "ip"
|
|
48
|
-
|
|
51
|
+
ip_search
|
|
49
52
|
else
|
|
50
53
|
raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
|
51
54
|
end
|
|
52
55
|
end
|
|
53
56
|
|
|
54
|
-
def
|
|
57
|
+
def domain_search
|
|
55
58
|
records = domain_client.get_passive_dns(query)
|
|
56
|
-
records.
|
|
59
|
+
records.filter_map do |record|
|
|
57
60
|
record.address if record.record_type == "A"
|
|
58
|
-
end.
|
|
61
|
+
end.uniq
|
|
59
62
|
end
|
|
60
63
|
|
|
61
|
-
def
|
|
64
|
+
def ip_search
|
|
62
65
|
records = ip_client.get_passive_dns(query)
|
|
63
|
-
records.
|
|
66
|
+
records.filter_map do |record|
|
|
64
67
|
record.hostname if record.record_type == "A"
|
|
65
|
-
end.
|
|
68
|
+
end.uniq
|
|
66
69
|
end
|
|
67
70
|
end
|
|
68
71
|
end
|
|
@@ -5,26 +5,29 @@ require "passivetotal"
|
|
|
5
5
|
module Mihari
|
|
6
6
|
module Analyzers
|
|
7
7
|
class PassiveTotal < Base
|
|
8
|
-
|
|
8
|
+
include Mixins::Refang
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
param :query
|
|
11
|
+
option :title, default: proc { "PassiveTotal search" }
|
|
12
|
+
option :description, default: proc { "query = #{query}" }
|
|
13
|
+
option :tags, default: proc { [] }
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
attr_reader :type
|
|
16
|
+
|
|
17
|
+
def initialize(*args, **kwargs)
|
|
18
|
+
super
|
|
15
19
|
|
|
16
|
-
@
|
|
17
|
-
@
|
|
18
|
-
@tags = tags
|
|
20
|
+
@query = refang(query)
|
|
21
|
+
@type = TypeChecker.type(query)
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
def artifacts
|
|
22
|
-
|
|
25
|
+
search || []
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
private
|
|
26
29
|
|
|
27
|
-
def
|
|
30
|
+
def configuration_keys
|
|
28
31
|
%w[passivetotal_username passivetotal_api_key]
|
|
29
32
|
end
|
|
30
33
|
|
|
@@ -33,30 +36,28 @@ module Mihari
|
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def valid_type?
|
|
36
|
-
%w[ip domain mail].include? type
|
|
39
|
+
%w[ip domain mail hash].include? type
|
|
37
40
|
end
|
|
38
41
|
|
|
39
|
-
def
|
|
42
|
+
def search
|
|
40
43
|
case type
|
|
41
|
-
when "domain"
|
|
42
|
-
|
|
43
|
-
when "ip"
|
|
44
|
-
passive_dns_lookup
|
|
44
|
+
when "domain", "ip"
|
|
45
|
+
passive_dns_search
|
|
45
46
|
when "mail"
|
|
46
|
-
|
|
47
|
+
reverse_whois_search
|
|
47
48
|
when "hash"
|
|
48
|
-
|
|
49
|
+
ssl_search
|
|
49
50
|
else
|
|
50
51
|
raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
|
51
52
|
end
|
|
52
53
|
end
|
|
53
54
|
|
|
54
|
-
def
|
|
55
|
+
def passive_dns_search
|
|
55
56
|
res = api.dns.passive_unique(query)
|
|
56
57
|
res["results"] || []
|
|
57
58
|
end
|
|
58
59
|
|
|
59
|
-
def
|
|
60
|
+
def reverse_whois_search
|
|
60
61
|
res = api.whois.search(query: query, field: "email")
|
|
61
62
|
results = res["results"] || []
|
|
62
63
|
results.map do |result|
|
|
@@ -64,7 +65,7 @@ module Mihari
|
|
|
64
65
|
end.flatten.compact.uniq
|
|
65
66
|
end
|
|
66
67
|
|
|
67
|
-
def
|
|
68
|
+
def ssl_search
|
|
68
69
|
res = api.ssl.history(query)
|
|
69
70
|
results = res["results"] || []
|
|
70
71
|
results.map do |result|
|
|
@@ -5,26 +5,29 @@ require "pulsedive"
|
|
|
5
5
|
module Mihari
|
|
6
6
|
module Analyzers
|
|
7
7
|
class Pulsedive < Base
|
|
8
|
-
|
|
8
|
+
include Mixins::Refang
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
param :query
|
|
11
|
+
option :title, default: proc { "Pulsedive search" }
|
|
12
|
+
option :description, default: proc { "query = #{query}" }
|
|
13
|
+
option :tags, default: proc { [] }
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
attr_reader :type
|
|
16
|
+
|
|
17
|
+
def initialize(*args, **kwargs)
|
|
18
|
+
super
|
|
15
19
|
|
|
16
|
-
@
|
|
17
|
-
@
|
|
18
|
-
@tags = tags
|
|
20
|
+
@query = refang(query)
|
|
21
|
+
@type = TypeChecker.type(query)
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
def artifacts
|
|
22
|
-
|
|
25
|
+
search || []
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
private
|
|
26
29
|
|
|
27
|
-
def
|
|
30
|
+
def configuration_keys
|
|
28
31
|
%w[pulsedive_api_key]
|
|
29
32
|
end
|
|
30
33
|
|
|
@@ -36,16 +39,16 @@ module Mihari
|
|
|
36
39
|
%w[ip domain].include? type
|
|
37
40
|
end
|
|
38
41
|
|
|
39
|
-
def
|
|
42
|
+
def search
|
|
40
43
|
raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
|
41
44
|
|
|
42
45
|
indicator = api.indicator.get_by_value(query)
|
|
43
46
|
iid = indicator["iid"]
|
|
44
47
|
|
|
45
48
|
properties = api.indicator.get_properties_by_id(iid)
|
|
46
|
-
(properties["dns"] || []).
|
|
49
|
+
(properties["dns"] || []).filter_map do |property|
|
|
47
50
|
property["value"] if ["A", "PTR"].include?(property["name"])
|
|
48
|
-
end
|
|
51
|
+
end
|
|
49
52
|
end
|
|
50
53
|
end
|
|
51
54
|
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uuidtools"
|
|
4
|
+
|
|
5
|
+
NIL = nil
|
|
6
|
+
|
|
7
|
+
module Mihari
|
|
8
|
+
module Analyzers
|
|
9
|
+
class Rule < Base
|
|
10
|
+
option :title
|
|
11
|
+
option :description
|
|
12
|
+
option :queries
|
|
13
|
+
|
|
14
|
+
option :id, default: proc {}
|
|
15
|
+
option :tags, default: proc { [] }
|
|
16
|
+
option :allowed_data_types, default: proc { ALLOWED_DATA_TYPES }
|
|
17
|
+
|
|
18
|
+
attr_reader :source
|
|
19
|
+
|
|
20
|
+
def initialize(**kwargs)
|
|
21
|
+
super(**kwargs)
|
|
22
|
+
|
|
23
|
+
@source = id || UUIDTools::UUID.md5_create(UUIDTools::UUID_URL_NAMESPACE, title + description).to_s
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
ANALYZER_TO_CLASS = {
|
|
27
|
+
"binaryedge" => BinaryEdge,
|
|
28
|
+
"censys" => Censys,
|
|
29
|
+
"circl" => CIRCL,
|
|
30
|
+
"crtsh" => Crtsh,
|
|
31
|
+
"dnpedia" => DNPedia,
|
|
32
|
+
"dnstwister" => DNSTwister,
|
|
33
|
+
"onyphe" => Onyphe,
|
|
34
|
+
"otx" => OTX,
|
|
35
|
+
"passivetotal" => PassiveTotal,
|
|
36
|
+
"pulsedive" => Pulsedive,
|
|
37
|
+
"securitytrails" => SecurityTrails,
|
|
38
|
+
"shodan" => Shodan,
|
|
39
|
+
"spyse" => Spyse,
|
|
40
|
+
"urlscan" => Urlscan,
|
|
41
|
+
"virustotal" => VirusTotal,
|
|
42
|
+
"zoomeye" => ZoomEye
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
# Returns a list of artifacts matched with queries
|
|
47
|
+
#
|
|
48
|
+
# @return [Array<Mihari::Artifact>]
|
|
49
|
+
#
|
|
50
|
+
def artifacts
|
|
51
|
+
artifacts = []
|
|
52
|
+
|
|
53
|
+
queries.each do |params|
|
|
54
|
+
analyzer_name = params[:analyzer]
|
|
55
|
+
klass = get_analyzer_class(analyzer_name)
|
|
56
|
+
|
|
57
|
+
query = params[:query]
|
|
58
|
+
analyzer = klass.new(query, **params)
|
|
59
|
+
|
|
60
|
+
# Use #normalized_artifacts method to get atrifacts as Array<Mihari::Artifact>
|
|
61
|
+
# So Mihari::Artifact object has "source" attribute (e.g. "Shodan")
|
|
62
|
+
artifacts << analyzer.normalized_artifacts
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
artifacts.flatten
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
#
|
|
69
|
+
# Normalize artifacts
|
|
70
|
+
# - Uniquefy artifacts by #uniq(&:data)
|
|
71
|
+
# - Reject an invalid artifact (for just in case)
|
|
72
|
+
# - Select artifacts with allowed data types
|
|
73
|
+
#
|
|
74
|
+
# @return [Array<Mihari::Artifact>]
|
|
75
|
+
#
|
|
76
|
+
def normalized_artifacts
|
|
77
|
+
@normalized_artifacts ||= artifacts.uniq(&:data).select(&:valid?).select do |artifact|
|
|
78
|
+
allowed_data_types.include? artifact.data_type
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
#
|
|
85
|
+
# Get analyzer class
|
|
86
|
+
#
|
|
87
|
+
# @param [String] analyzer_name
|
|
88
|
+
#
|
|
89
|
+
# @return [Class<Mihari::Analyzers::Base>] analyzer class
|
|
90
|
+
#
|
|
91
|
+
def get_analyzer_class(analyzer_name)
|
|
92
|
+
analyzer = ANALYZER_TO_CLASS[analyzer_name]
|
|
93
|
+
return analyzer if analyzer
|
|
94
|
+
|
|
95
|
+
raise ArgumentError, "#{analyzer_name} is not supported"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|