mihari 1.3.0 → 1.5.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/.github/workflows/test.yml +44 -0
- data/README.md +7 -7
- data/Rakefile +1 -0
- data/docker/Dockerfile +1 -1
- data/lib/mihari/alert_viewer.rb +3 -3
- data/lib/mihari/analyzers/base.rb +1 -1
- data/lib/mihari/analyzers/basic.rb +3 -4
- data/lib/mihari/analyzers/binaryedge.rb +8 -7
- data/lib/mihari/analyzers/censys.rb +3 -7
- data/lib/mihari/analyzers/circl.rb +3 -5
- data/lib/mihari/analyzers/crtsh.rb +2 -6
- data/lib/mihari/analyzers/dnpedia.rb +3 -6
- data/lib/mihari/analyzers/dnstwister.rb +4 -9
- data/lib/mihari/analyzers/free_text.rb +2 -6
- data/lib/mihari/analyzers/http_hash.rb +3 -11
- data/lib/mihari/analyzers/onyphe.rb +3 -6
- data/lib/mihari/analyzers/otx.rb +4 -9
- data/lib/mihari/analyzers/passive_dns.rb +4 -9
- data/lib/mihari/analyzers/passive_ssl.rb +4 -9
- data/lib/mihari/analyzers/passivetotal.rb +9 -14
- data/lib/mihari/analyzers/pulsedive.rb +7 -12
- data/lib/mihari/analyzers/reverse_whois.rb +4 -9
- data/lib/mihari/analyzers/securitytrails.rb +12 -17
- data/lib/mihari/analyzers/securitytrails_domain_feed.rb +3 -7
- data/lib/mihari/analyzers/shodan.rb +9 -8
- data/lib/mihari/analyzers/spyse.rb +6 -11
- data/lib/mihari/analyzers/ssh_fingerprint.rb +2 -6
- data/lib/mihari/analyzers/urlscan.rb +25 -9
- data/lib/mihari/analyzers/virustotal.rb +6 -11
- data/lib/mihari/analyzers/zoomeye.rb +7 -11
- data/lib/mihari/cli.rb +14 -7
- data/lib/mihari/config.rb +2 -24
- data/lib/mihari/database.rb +1 -1
- data/lib/mihari/emitters/misp.rb +4 -2
- data/lib/mihari/emitters/slack.rb +18 -7
- data/lib/mihari/emitters/the_hive.rb +2 -2
- data/lib/mihari/errors.rb +3 -0
- data/lib/mihari/models/artifact.rb +1 -1
- data/lib/mihari/notifiers/exception_notifier.rb +5 -5
- data/lib/mihari/retriable.rb +1 -1
- data/lib/mihari/status.rb +1 -1
- data/lib/mihari/type_checker.rb +4 -4
- data/lib/mihari/version.rb +1 -1
- data/mihari.gemspec +22 -23
- metadata +37 -51
- data/.travis.yml +0 -13
@@ -5,20 +5,29 @@ require "urlscan"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class Urlscan < Base
|
8
|
-
attr_reader :title
|
9
|
-
attr_reader :description
|
10
|
-
attr_reader :query
|
11
|
-
attr_reader :tags
|
12
|
-
attr_reader :target_type
|
8
|
+
attr_reader :title, :description, :query, :tags, :filter, :target_type, :use_pro, :use_similarity
|
13
9
|
|
14
|
-
def initialize(
|
10
|
+
def initialize(
|
11
|
+
query,
|
12
|
+
description: nil,
|
13
|
+
filter: nil,
|
14
|
+
tags: [],
|
15
|
+
target_type: "url",
|
16
|
+
title: nil,
|
17
|
+
use_pro: false,
|
18
|
+
use_similarity: false
|
19
|
+
)
|
15
20
|
super()
|
16
21
|
|
17
22
|
@query = query
|
18
23
|
@title = title || "urlscan lookup"
|
19
24
|
@description = description || "query = #{query}"
|
20
25
|
@tags = tags
|
26
|
+
|
27
|
+
@filter = filter
|
21
28
|
@target_type = target_type
|
29
|
+
@use_pro = use_pro
|
30
|
+
@use_similarity = use_similarity
|
22
31
|
|
23
32
|
raise InvalidInputError, "type should be url, domain or ip." unless valid_target_type?
|
24
33
|
end
|
@@ -27,7 +36,7 @@ module Mihari
|
|
27
36
|
result = search
|
28
37
|
return [] unless result
|
29
38
|
|
30
|
-
results = result
|
39
|
+
results = result["results"] || []
|
31
40
|
results.map do |match|
|
32
41
|
match.dig "page", target_type
|
33
42
|
end.compact.uniq
|
@@ -35,16 +44,23 @@ module Mihari
|
|
35
44
|
|
36
45
|
private
|
37
46
|
|
47
|
+
def config_keys
|
48
|
+
%w[urlscan_api_key]
|
49
|
+
end
|
50
|
+
|
38
51
|
def api
|
39
|
-
@api ||= ::UrlScan::API.new
|
52
|
+
@api ||= ::UrlScan::API.new(Mihari.config.urlscan_api_key)
|
40
53
|
end
|
41
54
|
|
42
55
|
def search
|
56
|
+
return api.pro.similar(query) if use_similarity
|
57
|
+
return api.pro.search(query: query, filter: filter, size: 10_000) if use_pro
|
58
|
+
|
43
59
|
api.search(query, size: 10_000)
|
44
60
|
end
|
45
61
|
|
46
62
|
def valid_target_type?
|
47
|
-
%w
|
63
|
+
%w[url domain ip].include? target_type
|
48
64
|
end
|
49
65
|
end
|
50
66
|
end
|
@@ -5,12 +5,7 @@ require "virustotal"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class VirusTotal < Base
|
8
|
-
attr_reader :indicator
|
9
|
-
attr_reader :type
|
10
|
-
|
11
|
-
attr_reader :title
|
12
|
-
attr_reader :description
|
13
|
-
attr_reader :tags
|
8
|
+
attr_reader :indicator, :type, :title, :description, :tags
|
14
9
|
|
15
10
|
def initialize(indicator, title: nil, description: nil, tags: [])
|
16
11
|
super()
|
@@ -30,7 +25,7 @@ module Mihari
|
|
30
25
|
private
|
31
26
|
|
32
27
|
def config_keys
|
33
|
-
%w
|
28
|
+
%w[virustotal_api_key]
|
34
29
|
end
|
35
30
|
|
36
31
|
def api
|
@@ -38,7 +33,7 @@ module Mihari
|
|
38
33
|
end
|
39
34
|
|
40
35
|
def valid_type?
|
41
|
-
%w
|
36
|
+
%w[ip domain].include? type
|
42
37
|
end
|
43
38
|
|
44
39
|
def lookup
|
@@ -48,14 +43,14 @@ module Mihari
|
|
48
43
|
when "ip"
|
49
44
|
ip_lookup
|
50
45
|
else
|
51
|
-
raise InvalidInputError, "#{indicator}(type: #{type ||
|
46
|
+
raise InvalidInputError, "#{indicator}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
52
47
|
end
|
53
48
|
end
|
54
49
|
|
55
50
|
def domain_lookup
|
56
51
|
res = api.domain.resolutions(indicator)
|
57
52
|
|
58
|
-
data = res
|
53
|
+
data = res["data"] || []
|
59
54
|
data.map do |item|
|
60
55
|
item.dig("attributes", "ip_address")
|
61
56
|
end.compact.uniq
|
@@ -64,7 +59,7 @@ module Mihari
|
|
64
59
|
def ip_lookup
|
65
60
|
res = api.ip_address.resolutions(indicator)
|
66
61
|
|
67
|
-
data = res
|
62
|
+
data = res["data"] || []
|
68
63
|
data.map do |item|
|
69
64
|
item.dig("attributes", "host_name")
|
70
65
|
end.compact.uniq
|
@@ -5,11 +5,7 @@ require "zoomeye"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class ZoomEye < Base
|
8
|
-
attr_reader :title
|
9
|
-
attr_reader :description
|
10
|
-
attr_reader :query
|
11
|
-
attr_reader :tags
|
12
|
-
attr_reader :type
|
8
|
+
attr_reader :title, :description, :query, :tags, :type
|
13
9
|
|
14
10
|
def initialize(query, title: nil, description: nil, tags: [], type: "host")
|
15
11
|
super()
|
@@ -37,11 +33,11 @@ module Mihari
|
|
37
33
|
PAGE_SIZE = 10
|
38
34
|
|
39
35
|
def valid_type?
|
40
|
-
%w
|
36
|
+
%w[host web].include? type
|
41
37
|
end
|
42
38
|
|
43
39
|
def config_keys
|
44
|
-
%w
|
40
|
+
%w[zoomeye_password zoomeye_username]
|
45
41
|
end
|
46
42
|
|
47
43
|
def api
|
@@ -50,9 +46,9 @@ module Mihari
|
|
50
46
|
|
51
47
|
def convert_responses(responses)
|
52
48
|
responses.map do |res|
|
53
|
-
matches = res
|
49
|
+
matches = res["matches"] || []
|
54
50
|
matches.map do |match|
|
55
|
-
match
|
51
|
+
match["ip"]
|
56
52
|
end
|
57
53
|
end.flatten.compact.uniq
|
58
54
|
end
|
@@ -69,7 +65,7 @@ module Mihari
|
|
69
65
|
res = _host_lookup(query, page: page)
|
70
66
|
break unless res
|
71
67
|
|
72
|
-
total = res
|
68
|
+
total = res["total"].to_i
|
73
69
|
responses << res
|
74
70
|
break if total <= page * PAGE_SIZE
|
75
71
|
end
|
@@ -88,7 +84,7 @@ module Mihari
|
|
88
84
|
res = _web_lookup(query, page: page)
|
89
85
|
break unless res
|
90
86
|
|
91
|
-
total = res
|
87
|
+
total = res["total"].to_i
|
92
88
|
responses << res
|
93
89
|
break if total <= page * PAGE_SIZE
|
94
90
|
end
|
data/lib/mihari/cli.rb
CHANGED
@@ -7,6 +7,10 @@ module Mihari
|
|
7
7
|
class CLI < Thor
|
8
8
|
class_option :config, type: :string, desc: "path to config file"
|
9
9
|
|
10
|
+
def self.exit_on_failure?
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
10
14
|
desc "censys [QUERY]", "Censys IPv4 search by a query"
|
11
15
|
method_option :title, type: :string, desc: "title"
|
12
16
|
method_option :description, type: :string, desc: "description"
|
@@ -42,7 +46,10 @@ module Mihari
|
|
42
46
|
method_option :title, type: :string, desc: "title"
|
43
47
|
method_option :description, type: :string, desc: "description"
|
44
48
|
method_option :tags, type: :array, desc: "tags"
|
49
|
+
method_option :filter, type: :string, desc: "filter for urlscan pro search"
|
45
50
|
method_option :target_type, type: :string, default: "url", desc: "target type to fetch from lookup results (target type should be 'url', 'domain' or 'ip')"
|
51
|
+
method_option :use_pro, type: :boolean, default: false, desc: "use pro search API or not"
|
52
|
+
method_option :use_similarity, type: :boolean, default: false, desc: "use similarity API or not"
|
46
53
|
def urlscan(query)
|
47
54
|
with_error_handling do
|
48
55
|
run_analyzer Analyzers::Urlscan, query: query, options: options
|
@@ -252,16 +259,16 @@ module Mihari
|
|
252
259
|
desc "import_from_json", "Give a JSON input via STDIN"
|
253
260
|
def import_from_json(input = nil)
|
254
261
|
with_error_handling do
|
255
|
-
json = input ||
|
262
|
+
json = input || $stdin.gets.chomp
|
256
263
|
raise ArgumentError, "Input not found: please give an input in a JSON format" unless json
|
257
264
|
|
258
265
|
json = parse_as_json(json)
|
259
266
|
raise ArgumentError, "Invalid input format: an input JSON data should have title, description and artifacts key" unless valid_json?(json)
|
260
267
|
|
261
|
-
title = json
|
262
|
-
description = json
|
263
|
-
artifacts = json
|
264
|
-
tags = json
|
268
|
+
title = json["title"]
|
269
|
+
description = json["description"]
|
270
|
+
artifacts = json["artifacts"]
|
271
|
+
tags = json["tags"] || []
|
265
272
|
|
266
273
|
basic = Analyzers::Basic.new(title: title, description: description, artifacts: artifacts, source: "json", tags: tags)
|
267
274
|
basic.run
|
@@ -295,7 +302,7 @@ module Mihari
|
|
295
302
|
no_commands do
|
296
303
|
def with_error_handling
|
297
304
|
yield
|
298
|
-
rescue
|
305
|
+
rescue => e
|
299
306
|
notifier = Notifiers::ExceptionNotifier.new
|
300
307
|
notifier.notify e
|
301
308
|
end
|
@@ -308,7 +315,7 @@ module Mihari
|
|
308
315
|
|
309
316
|
# @return [true, false]
|
310
317
|
def valid_json?(json)
|
311
|
-
%w
|
318
|
+
%w[title description artifacts].all? { |key| json.key? key }
|
312
319
|
end
|
313
320
|
|
314
321
|
def load_configuration
|
data/lib/mihari/config.rb
CHANGED
@@ -4,30 +4,7 @@ require "yaml"
|
|
4
4
|
|
5
5
|
module Mihari
|
6
6
|
class Config
|
7
|
-
attr_accessor :binaryedge_api_key
|
8
|
-
attr_accessor :censys_id
|
9
|
-
attr_accessor :censys_secret
|
10
|
-
attr_accessor :circl_passive_password
|
11
|
-
attr_accessor :circl_passive_username
|
12
|
-
attr_accessor :misp_api_endpoint
|
13
|
-
attr_accessor :misp_api_key
|
14
|
-
attr_accessor :onyphe_api_key
|
15
|
-
attr_accessor :otx_api_key
|
16
|
-
attr_accessor :passivetotal_api_key
|
17
|
-
attr_accessor :passivetotal_username
|
18
|
-
attr_accessor :pulsedive_api_key
|
19
|
-
attr_accessor :securitytrails_api_key
|
20
|
-
attr_accessor :shodan_api_key
|
21
|
-
attr_accessor :slack_channel
|
22
|
-
attr_accessor :slack_webhook_url
|
23
|
-
attr_accessor :spyse_api_key
|
24
|
-
attr_accessor :thehive_api_endpoint
|
25
|
-
attr_accessor :thehive_api_key
|
26
|
-
attr_accessor :virustotal_api_key
|
27
|
-
attr_accessor :zoomeye_password
|
28
|
-
attr_accessor :zoomeye_username
|
29
|
-
|
30
|
-
attr_accessor :database
|
7
|
+
attr_accessor :binaryedge_api_key, :censys_id, :censys_secret, :circl_passive_password, :circl_passive_username, :misp_api_endpoint, :misp_api_key, :onyphe_api_key, :otx_api_key, :passivetotal_api_key, :passivetotal_username, :pulsedive_api_key, :securitytrails_api_key, :shodan_api_key, :slack_channel, :slack_webhook_url, :spyse_api_key, :thehive_api_endpoint, :thehive_api_key, :urlscan_api_key, :virustotal_api_key, :zoomeye_password, :zoomeye_username, :database
|
31
8
|
|
32
9
|
def initialize
|
33
10
|
load_from_env
|
@@ -53,6 +30,7 @@ module Mihari
|
|
53
30
|
@spyse_api_key = ENV["SPYSE_API_KEY"]
|
54
31
|
@thehive_api_endpoint = ENV["THEHIVE_API_ENDPOINT"]
|
55
32
|
@thehive_api_key = ENV["THEHIVE_API_KEY"]
|
33
|
+
@urlscan_api_key = ENV["URLSCAN_API_KEY"]
|
56
34
|
@virustotal_api_key = ENV["VIRUSTOTAL_API_KEY"]
|
57
35
|
@zoomeye_password = ENV["ZOOMEYE_PASSWORD"]
|
58
36
|
@zoomeye_username = ENV["ZOOMEYE_USERNAME"]
|
data/lib/mihari/database.rb
CHANGED
data/lib/mihari/emitters/misp.rb
CHANGED
@@ -7,6 +7,8 @@ module Mihari
|
|
7
7
|
module Emitters
|
8
8
|
class MISP < Base
|
9
9
|
def initialize
|
10
|
+
super()
|
11
|
+
|
10
12
|
::MISP.configure do |config|
|
11
13
|
config.api_endpoint = Mihari.config.misp_api_endpoint
|
12
14
|
config.api_key = Mihari.config.misp_api_key
|
@@ -35,7 +37,7 @@ module Mihari
|
|
35
37
|
private
|
36
38
|
|
37
39
|
def config_keys
|
38
|
-
%w
|
40
|
+
%w[misp_api_endpoint misp_api_key]
|
39
41
|
end
|
40
42
|
|
41
43
|
def build_attribute(artifact)
|
@@ -61,7 +63,7 @@ module Mihari
|
|
61
63
|
ip: "ip-dst",
|
62
64
|
mail: "email-dst",
|
63
65
|
url: "url",
|
64
|
-
domain: "domain"
|
66
|
+
domain: "domain"
|
65
67
|
}
|
66
68
|
return table[type] if table.key?(type)
|
67
69
|
|
@@ -19,25 +19,31 @@ module Mihari
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def actions
|
22
|
-
[vt_link, urlscan_link, censys_link].compact
|
22
|
+
[vt_link, urlscan_link, censys_link, shodan_link].compact
|
23
23
|
end
|
24
24
|
|
25
25
|
def vt_link
|
26
26
|
return nil unless _vt_link
|
27
27
|
|
28
|
-
{ type: "button", text: "
|
28
|
+
{ type: "button", text: "VirusTotal", url: _vt_link }
|
29
29
|
end
|
30
30
|
|
31
31
|
def urlscan_link
|
32
32
|
return nil unless _urlscan_link
|
33
33
|
|
34
|
-
{ type: "button", text: "
|
34
|
+
{ type: "button", text: "urlscan.io", url: _urlscan_link }
|
35
35
|
end
|
36
36
|
|
37
37
|
def censys_link
|
38
38
|
return nil unless _censys_link
|
39
39
|
|
40
|
-
{ type: "button", text: "
|
40
|
+
{ type: "button", text: "Censys", url: _censys_link }
|
41
|
+
end
|
42
|
+
|
43
|
+
def shodan_link
|
44
|
+
return nil unless _shodan_link
|
45
|
+
|
46
|
+
{ type: "button", text: "Shodan", url: _shodan_link }
|
41
47
|
end
|
42
48
|
|
43
49
|
# @return [Array]
|
@@ -89,6 +95,11 @@ module Mihari
|
|
89
95
|
end
|
90
96
|
memoize :_censys_link
|
91
97
|
|
98
|
+
def _shodan_link
|
99
|
+
data_type == "ip" ? "https://www.shodan.io/host/#{data}" : nil
|
100
|
+
end
|
101
|
+
memoize :_shodan_link
|
102
|
+
|
92
103
|
# @return [String]
|
93
104
|
def sha256
|
94
105
|
Digest::SHA256.hexdigest data
|
@@ -96,7 +107,7 @@ module Mihari
|
|
96
107
|
|
97
108
|
# @return [String]
|
98
109
|
def defanged_data
|
99
|
-
@defanged_data ||= data.to_s.gsub
|
110
|
+
@defanged_data ||= data.to_s.gsub(/\./, "[.]")
|
100
111
|
end
|
101
112
|
end
|
102
113
|
|
@@ -121,7 +132,7 @@ module Mihari
|
|
121
132
|
[
|
122
133
|
"*#{title}*",
|
123
134
|
"*Desc.*: #{description}",
|
124
|
-
"*Tags*: #{tags.join(', ')}"
|
135
|
+
"*Tags*: #{tags.join(', ')}"
|
125
136
|
].join("\n")
|
126
137
|
end
|
127
138
|
|
@@ -137,7 +148,7 @@ module Mihari
|
|
137
148
|
private
|
138
149
|
|
139
150
|
def config_keys
|
140
|
-
%w
|
151
|
+
%w[slack_webhook_url]
|
141
152
|
end
|
142
153
|
end
|
143
154
|
end
|
@@ -17,7 +17,7 @@ module Mihari
|
|
17
17
|
api.alert.create(
|
18
18
|
title: title,
|
19
19
|
description: description,
|
20
|
-
artifacts: artifacts.map { |artifact| {
|
20
|
+
artifacts: artifacts.map { |artifact| {data: artifact.data, data_type: artifact.data_type, message: description} },
|
21
21
|
tags: tags,
|
22
22
|
type: "external",
|
23
23
|
source: "mihari"
|
@@ -27,7 +27,7 @@ module Mihari
|
|
27
27
|
private
|
28
28
|
|
29
29
|
def config_keys
|
30
|
-
%w
|
30
|
+
%w[thehive_api_endpoint thehive_api_key]
|
31
31
|
end
|
32
32
|
|
33
33
|
def api
|
data/lib/mihari/errors.rb
CHANGED
@@ -19,7 +19,7 @@ module Mihari
|
|
19
19
|
def notify(exception)
|
20
20
|
notify_to_stdout exception
|
21
21
|
|
22
|
-
clean_message = exception.message.tr(
|
22
|
+
clean_message = exception.message.tr("`", "'")
|
23
23
|
attachments = to_attachments(exception, clean_message)
|
24
24
|
notify_to_slack(text: clean_message, attachments: attachments) if @slack.valid?
|
25
25
|
end
|
@@ -51,20 +51,20 @@ module Mihari
|
|
51
51
|
|
52
52
|
def to_fields(clean_message, backtrace)
|
53
53
|
fields = [
|
54
|
-
{
|
55
|
-
{
|
54
|
+
{title: "Exception", value: clean_message},
|
55
|
+
{title: "Hostname", value: hostname}
|
56
56
|
]
|
57
57
|
|
58
58
|
if backtrace
|
59
59
|
formatted_backtrace = format_backtrace(backtrace)
|
60
|
-
fields << {
|
60
|
+
fields << {title: "Backtrace", value: formatted_backtrace}
|
61
61
|
end
|
62
62
|
fields
|
63
63
|
end
|
64
64
|
|
65
65
|
def hostname
|
66
66
|
Socket.gethostname
|
67
|
-
rescue
|
67
|
+
rescue => _e
|
68
68
|
"N/A"
|
69
69
|
end
|
70
70
|
|