mihari 2.2.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +7 -0
- data/.overcommit.yml +12 -0
- data/README.md +7 -9
- data/exe/mihari +1 -1
- data/images/tines.png +0 -0
- data/lib/mihari.rb +89 -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 +20 -30
- data/lib/mihari/analyzers/virustotal.rb +25 -22
- data/lib/mihari/analyzers/zoomeye.rb +16 -22
- 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 -4
- 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 -9
- data/lib/mihari/emitters/the_hive.rb +1 -1
- data/lib/mihari/emitters/webhook.rb +53 -0
- 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 -1
- 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/app.rb +2 -0
- data/lib/mihari/web/controllers/alerts_controller.rb +12 -3
- data/lib/mihari/web/controllers/artifacts_controller.rb +1 -3
- data/lib/mihari/web/controllers/base_controller.rb +22 -0
- data/lib/mihari/web/controllers/command_controller.rb +3 -4
- data/lib/mihari/web/controllers/config_controller.rb +1 -3
- data/lib/mihari/web/controllers/sources_controller.rb +1 -3
- data/lib/mihari/web/controllers/tags_controller.rb +1 -3
- data/lib/mihari/web/helpers/json.rb +2 -0
- 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/lib/mihari/web/public/static/js/{app.bcc595df.js → app.cccddb2b.js} +2 -2
- data/lib/mihari/web/public/static/js/app.cccddb2b.js.map +1 -0
- data/mihari.gemspec +20 -10
- metadata +180 -63
- 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 -124
- 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 -84
- data/lib/mihari/configurable.rb +0 -21
- data/lib/mihari/html.rb +0 -43
- data/lib/mihari/retriable.rb +0 -17
- data/lib/mihari/slack_monkeypatch.rb +0 -16
- data/lib/mihari/web/public/static/js/app.bcc595df.js.map +0 -1
@@ -5,26 +5,29 @@ require "securitytrails"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class SecurityTrails < Base
|
8
|
-
|
8
|
+
include Mixins::Refang
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
param :query
|
11
|
+
option :title, default: proc { "SecurityTrails 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[securitytrails_api_key]
|
29
32
|
end
|
30
33
|
|
@@ -36,20 +39,20 @@ module Mihari
|
|
36
39
|
%w[ip domain mail].include? type
|
37
40
|
end
|
38
41
|
|
39
|
-
def
|
42
|
+
def search
|
40
43
|
case type
|
41
44
|
when "domain"
|
42
|
-
|
45
|
+
domain_search
|
43
46
|
when "ip"
|
44
|
-
|
47
|
+
ip_search
|
45
48
|
when "mail"
|
46
|
-
|
49
|
+
mail_search
|
47
50
|
else
|
48
51
|
raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
49
52
|
end
|
50
53
|
end
|
51
54
|
|
52
|
-
def
|
55
|
+
def domain_search
|
53
56
|
result = api.history.get_all_dns_history(query, type: "a")
|
54
57
|
records = result["records"] || []
|
55
58
|
records.map do |record|
|
@@ -57,16 +60,16 @@ module Mihari
|
|
57
60
|
end.flatten.compact.uniq
|
58
61
|
end
|
59
62
|
|
60
|
-
def
|
63
|
+
def ip_search
|
61
64
|
result = api.domains.search(filter: { ipv4: query })
|
62
65
|
records = result["records"] || []
|
63
|
-
records.
|
66
|
+
records.filter_map { |record| record["hostname"] }.uniq
|
64
67
|
end
|
65
68
|
|
66
|
-
def
|
69
|
+
def mail_search
|
67
70
|
result = api.domains.search(filter: { whois_email: query })
|
68
71
|
records = result["records"] || []
|
69
|
-
records.
|
72
|
+
records.filter_map { |record| record["hostname"] }.uniq
|
70
73
|
end
|
71
74
|
end
|
72
75
|
end
|
@@ -5,16 +5,10 @@ require "shodan"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class Shodan < Base
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
@query = query
|
14
|
-
@title = title || "Shodan lookup"
|
15
|
-
@description = description || "query = #{query}"
|
16
|
-
@tags = tags
|
17
|
-
end
|
8
|
+
param :query
|
9
|
+
option :title, default: proc { "Shodan search" }
|
10
|
+
option :description, default: proc { "query = #{query}" }
|
11
|
+
option :tags, default: proc { [] }
|
18
12
|
|
19
13
|
def artifacts
|
20
14
|
results = search
|
@@ -22,9 +16,9 @@ module Mihari
|
|
22
16
|
|
23
17
|
results.map do |result|
|
24
18
|
matches = result["matches"] || []
|
25
|
-
matches.
|
19
|
+
matches.filter_map do |match|
|
26
20
|
match["ip_str"]
|
27
|
-
end
|
21
|
+
end
|
28
22
|
end.flatten.compact.uniq
|
29
23
|
end
|
30
24
|
|
@@ -32,7 +26,7 @@ module Mihari
|
|
32
26
|
|
33
27
|
PAGE_SIZE = 100
|
34
28
|
|
35
|
-
def
|
29
|
+
def configuration_keys
|
36
30
|
%w[shodan_api_key]
|
37
31
|
end
|
38
32
|
|
@@ -6,21 +6,14 @@ require "json"
|
|
6
6
|
module Mihari
|
7
7
|
module Analyzers
|
8
8
|
class Spyse < Base
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
@query = query
|
15
|
-
|
16
|
-
@title = title || "Spyse lookup"
|
17
|
-
@description = description || "query = #{query}"
|
18
|
-
@tags = tags
|
19
|
-
@type = type
|
20
|
-
end
|
9
|
+
param :query
|
10
|
+
option :title, default: proc { "Spyse search" }
|
11
|
+
option :description, default: proc { "query = #{query}" }
|
12
|
+
option :type, default: proc { "domain" }
|
13
|
+
option :tags, default: proc { [] }
|
21
14
|
|
22
15
|
def artifacts
|
23
|
-
|
16
|
+
search || []
|
24
17
|
end
|
25
18
|
|
26
19
|
private
|
@@ -29,7 +22,7 @@ module Mihari
|
|
29
22
|
@search_params ||= JSON.parse(query)
|
30
23
|
end
|
31
24
|
|
32
|
-
def
|
25
|
+
def configuration_keys
|
33
26
|
%w[spyse_api_key]
|
34
27
|
end
|
35
28
|
|
@@ -41,7 +34,7 @@ module Mihari
|
|
41
34
|
%w[ip domain cert].include? type
|
42
35
|
end
|
43
36
|
|
44
|
-
def
|
37
|
+
def domain_search
|
45
38
|
res = api.domain.search(search_params, limit: 100)
|
46
39
|
items = res.dig("data", "items") || []
|
47
40
|
items.map do |item|
|
@@ -49,7 +42,7 @@ module Mihari
|
|
49
42
|
end.uniq.compact
|
50
43
|
end
|
51
44
|
|
52
|
-
def
|
45
|
+
def ip_search
|
53
46
|
res = api.ip.search(search_params, limit: 100)
|
54
47
|
items = res.dig("data", "items") || []
|
55
48
|
items.map do |item|
|
@@ -57,12 +50,12 @@ module Mihari
|
|
57
50
|
end.uniq.compact
|
58
51
|
end
|
59
52
|
|
60
|
-
def
|
53
|
+
def search
|
61
54
|
case type
|
62
55
|
when "domain"
|
63
|
-
|
56
|
+
domain_search
|
64
57
|
when "ip"
|
65
|
-
|
58
|
+
ip_search
|
66
59
|
else
|
67
60
|
raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
68
61
|
end
|
@@ -2,34 +2,22 @@
|
|
2
2
|
|
3
3
|
require "urlscan"
|
4
4
|
|
5
|
+
SUPPORTED_DATA_TYPES = %w[url domain ip].freeze
|
6
|
+
|
5
7
|
module Mihari
|
6
8
|
module Analyzers
|
7
9
|
class Urlscan < Base
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
tags: [],
|
15
|
-
target_type: "url",
|
16
|
-
title: nil,
|
17
|
-
use_pro: false,
|
18
|
-
use_similarity: false
|
19
|
-
)
|
20
|
-
super()
|
10
|
+
param :query
|
11
|
+
option :title, default: proc { "urlscan search" }
|
12
|
+
option :description, default: proc { "query = #{query}" }
|
13
|
+
option :tags, default: proc { [] }
|
14
|
+
option :allowed_data_types, default: proc { SUPPORTED_DATA_TYPES }
|
15
|
+
option :use_similarity, default: proc { false }
|
21
16
|
|
22
|
-
|
23
|
-
|
24
|
-
@description = description || "query = #{query}"
|
25
|
-
@tags = tags
|
17
|
+
def initialize(*args, **kwargs)
|
18
|
+
super
|
26
19
|
|
27
|
-
|
28
|
-
@target_type = target_type
|
29
|
-
@use_pro = use_pro
|
30
|
-
@use_similarity = use_similarity
|
31
|
-
|
32
|
-
raise InvalidInputError, "type should be url, domain or ip." unless valid_target_type?
|
20
|
+
raise InvalidInputError, "allowed_data_types should be any of url, domain and ip." unless valid_alllowed_data_types?
|
33
21
|
end
|
34
22
|
|
35
23
|
def artifacts
|
@@ -37,14 +25,17 @@ module Mihari
|
|
37
25
|
return [] unless result
|
38
26
|
|
39
27
|
results = result["results"] || []
|
40
|
-
|
41
|
-
|
42
|
-
|
28
|
+
|
29
|
+
allowed_data_types.map do |type|
|
30
|
+
results.filter_map do |match|
|
31
|
+
match.dig "page", type
|
32
|
+
end.uniq
|
33
|
+
end.flatten
|
43
34
|
end
|
44
35
|
|
45
36
|
private
|
46
37
|
|
47
|
-
def
|
38
|
+
def configuration_keys
|
48
39
|
%w[urlscan_api_key]
|
49
40
|
end
|
50
41
|
|
@@ -54,13 +45,12 @@ module Mihari
|
|
54
45
|
|
55
46
|
def search
|
56
47
|
return api.pro.similar(query) if use_similarity
|
57
|
-
return api.pro.search(query: query, filter: filter, size: 10_000) if use_pro
|
58
48
|
|
59
49
|
api.search(query, size: 10_000)
|
60
50
|
end
|
61
51
|
|
62
|
-
def
|
63
|
-
|
52
|
+
def valid_alllowed_data_types?
|
53
|
+
allowed_data_types.all? { |type| SUPPORTED_DATA_TYPES.include? type }
|
64
54
|
end
|
65
55
|
end
|
66
56
|
end
|
@@ -5,26 +5,29 @@ require "virustotal"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class VirusTotal < Base
|
8
|
-
|
8
|
+
include Mixins::Refang
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
param :query
|
11
|
+
option :title, default: proc { "VirusTotal search" }
|
12
|
+
option :description, default: proc { "query = #{query}" }
|
13
|
+
option :tags, default: proc { [] }
|
12
14
|
|
13
|
-
|
14
|
-
@type = TypeChecker.type(indicator)
|
15
|
+
attr_reader :type
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
17
|
+
def initialize(*args, **kwargs)
|
18
|
+
super
|
19
|
+
|
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[virustotal_api_key]
|
29
32
|
end
|
30
33
|
|
@@ -36,33 +39,33 @@ module Mihari
|
|
36
39
|
%w[ip domain].include? type
|
37
40
|
end
|
38
41
|
|
39
|
-
def
|
42
|
+
def search
|
40
43
|
case type
|
41
44
|
when "domain"
|
42
|
-
|
45
|
+
domain_search
|
43
46
|
when "ip"
|
44
|
-
|
47
|
+
ip_search
|
45
48
|
else
|
46
|
-
raise InvalidInputError, "#{
|
49
|
+
raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
47
50
|
end
|
48
51
|
end
|
49
52
|
|
50
|
-
def
|
51
|
-
res = api.domain.resolutions(
|
53
|
+
def domain_search
|
54
|
+
res = api.domain.resolutions(query)
|
52
55
|
|
53
56
|
data = res["data"] || []
|
54
|
-
data.
|
57
|
+
data.filter_map do |item|
|
55
58
|
item.dig("attributes", "ip_address")
|
56
|
-
end.
|
59
|
+
end.uniq
|
57
60
|
end
|
58
61
|
|
59
|
-
def
|
60
|
-
res = api.ip_address.resolutions(
|
62
|
+
def ip_search
|
63
|
+
res = api.ip_address.resolutions(query)
|
61
64
|
|
62
65
|
data = res["data"] || []
|
63
|
-
data.
|
66
|
+
data.filter_map do |item|
|
64
67
|
item.dig("attributes", "host_name")
|
65
|
-
end.
|
68
|
+
end.uniq
|
66
69
|
end
|
67
70
|
end
|
68
71
|
end
|
@@ -5,24 +5,18 @@ require "zoomeye"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class ZoomEye < Base
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
@query = query
|
14
|
-
@title = title || "ZoomEye lookup"
|
15
|
-
@description = description || "query = #{query}"
|
16
|
-
@tags = tags
|
17
|
-
@type = type
|
18
|
-
end
|
8
|
+
param :query
|
9
|
+
option :title, default: proc { "ZoomEye search" }
|
10
|
+
option :description, default: proc { "query = #{query}" }
|
11
|
+
option :tags, default: proc { [] }
|
12
|
+
option :type, default: proc { "host" }
|
19
13
|
|
20
14
|
def artifacts
|
21
15
|
case type
|
22
16
|
when "host"
|
23
|
-
|
17
|
+
host_search
|
24
18
|
when "web"
|
25
|
-
|
19
|
+
web_search
|
26
20
|
else
|
27
21
|
raise InvalidInputError, "#{type} type is not supported." unless valid_type?
|
28
22
|
end
|
@@ -36,12 +30,12 @@ module Mihari
|
|
36
30
|
%w[host web].include? type
|
37
31
|
end
|
38
32
|
|
39
|
-
def
|
40
|
-
%w[
|
33
|
+
def configuration_keys
|
34
|
+
%w[zoomeye_api_key]
|
41
35
|
end
|
42
36
|
|
43
37
|
def api
|
44
|
-
@api ||= ::ZoomEye::API.new(
|
38
|
+
@api ||= ::ZoomEye::API.new(api_key: Mihari.config.zoomeye_api_key)
|
45
39
|
end
|
46
40
|
|
47
41
|
def convert_responses(responses)
|
@@ -53,16 +47,16 @@ module Mihari
|
|
53
47
|
end.flatten.compact.uniq
|
54
48
|
end
|
55
49
|
|
56
|
-
def
|
50
|
+
def _host_search(query, page: 1)
|
57
51
|
api.host.search(query, page: page)
|
58
52
|
rescue ::ZoomEye::Error => _e
|
59
53
|
nil
|
60
54
|
end
|
61
55
|
|
62
|
-
def
|
56
|
+
def host_search
|
63
57
|
responses = []
|
64
58
|
(1..Float::INFINITY).each do |page|
|
65
|
-
res =
|
59
|
+
res = _host_search(query, page: page)
|
66
60
|
break unless res
|
67
61
|
|
68
62
|
total = res["total"].to_i
|
@@ -72,16 +66,16 @@ module Mihari
|
|
72
66
|
convert_responses responses.compact
|
73
67
|
end
|
74
68
|
|
75
|
-
def
|
69
|
+
def _web_search(query, page: 1)
|
76
70
|
api.web.search(query, page: page)
|
77
71
|
rescue ::ZoomEye::Error => _e
|
78
72
|
nil
|
79
73
|
end
|
80
74
|
|
81
|
-
def
|
75
|
+
def web_search
|
82
76
|
responses = []
|
83
77
|
(1..Float::INFINITY).each do |page|
|
84
|
-
res =
|
78
|
+
res = _web_search(query, page: page)
|
85
79
|
break unless res
|
86
80
|
|
87
81
|
total = res["total"].to_i
|