mihari 0.17.5 → 1.2.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 +3 -0
- data/.rubocop.yml +155 -0
- data/.travis.yml +7 -1
- data/Gemfile +2 -0
- data/README.md +45 -73
- data/config/pre_commit.yml +3 -0
- data/docker/Dockerfile +1 -1
- data/lib/mihari.rb +13 -8
- data/lib/mihari/alert_viewer.rb +16 -34
- data/lib/mihari/analyzers/base.rb +7 -19
- data/lib/mihari/analyzers/basic.rb +3 -1
- data/lib/mihari/analyzers/binaryedge.rb +2 -2
- data/lib/mihari/analyzers/censys.rb +2 -2
- data/lib/mihari/analyzers/circl.rb +2 -2
- data/lib/mihari/analyzers/onyphe.rb +3 -3
- data/lib/mihari/analyzers/otx.rb +74 -0
- data/lib/mihari/analyzers/passive_dns.rb +2 -1
- data/lib/mihari/analyzers/passivetotal.rb +2 -2
- data/lib/mihari/analyzers/pulsedive.rb +2 -2
- data/lib/mihari/analyzers/securitytrails.rb +2 -2
- data/lib/mihari/analyzers/securitytrails_domain_feed.rb +2 -2
- data/lib/mihari/analyzers/shodan.rb +2 -2
- data/lib/mihari/analyzers/virustotal.rb +2 -2
- data/lib/mihari/analyzers/zoomeye.rb +2 -2
- data/lib/mihari/cli.rb +23 -4
- data/lib/mihari/config.rb +70 -2
- data/lib/mihari/configurable.rb +1 -1
- data/lib/mihari/database.rb +68 -0
- data/lib/mihari/emitters/base.rb +1 -1
- data/lib/mihari/emitters/database.rb +29 -0
- data/lib/mihari/emitters/misp.rb +8 -1
- data/lib/mihari/emitters/slack.rb +4 -2
- data/lib/mihari/emitters/stdout.rb +2 -1
- data/lib/mihari/emitters/the_hive.rb +28 -14
- data/lib/mihari/models/alert.rb +11 -0
- data/lib/mihari/models/artifact.rb +27 -0
- data/lib/mihari/models/tag.rb +10 -0
- data/lib/mihari/models/tagging.rb +10 -0
- data/lib/mihari/notifiers/slack.rb +7 -4
- data/lib/mihari/serializers/alert.rb +12 -0
- data/lib/mihari/serializers/artifact.rb +9 -0
- data/lib/mihari/serializers/tag.rb +9 -0
- data/lib/mihari/slack_monkeypatch.rb +16 -0
- data/lib/mihari/status.rb +1 -1
- data/lib/mihari/type_checker.rb +1 -1
- data/lib/mihari/version.rb +1 -1
- data/mihari.gemspec +13 -5
- metadata +149 -30
- data/lib/mihari/artifact.rb +0 -36
- data/lib/mihari/cache.rb +0 -35
- data/lib/mihari/the_hive.rb +0 -42
- data/lib/mihari/the_hive/alert.rb +0 -25
- data/lib/mihari/the_hive/artifact.rb +0 -33
- data/lib/mihari/the_hive/base.rb +0 -14
data/lib/mihari/alert_viewer.rb
CHANGED
@@ -2,40 +2,22 @@
|
|
2
2
|
|
3
3
|
module Mihari
|
4
4
|
class AlertViewer
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
alerts.map { |alert| convert alert }
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
def validate_limit
|
27
|
-
return true if limit == "all"
|
28
|
-
|
29
|
-
raise ArgumentError, "limit should be bigger than zero" unless limit.to_i.positive?
|
30
|
-
end
|
31
|
-
|
32
|
-
def convert(alert)
|
33
|
-
attributes = alert.select { |k, _v| ALERT_KEYS.include? k }
|
34
|
-
attributes["createdAt"] = Time.at(attributes["createdAt"] / 1000).to_s
|
35
|
-
attributes["artifacts"] = (attributes.dig("artifacts") || []).map do |artifact|
|
36
|
-
artifact.dig("data")
|
37
|
-
end.sort
|
38
|
-
attributes
|
5
|
+
def list(title: nil, source: nil, tag: nil, limit: 5)
|
6
|
+
limit = limit.to_i
|
7
|
+
raise ArgumentError, "limit should be bigger than zero" unless limit.positive?
|
8
|
+
|
9
|
+
relation = Alert.includes(:tags, :artifacts)
|
10
|
+
relation = relation.where(title: title) if title
|
11
|
+
relation = relation.where(source: source) if source
|
12
|
+
relation = relation.where(tags: { name: tag } ) if tag
|
13
|
+
|
14
|
+
alerts = relation.limit(limit).order(id: :desc)
|
15
|
+
alerts.map do |alert|
|
16
|
+
json = AlertSerializer.new(alert).as_json
|
17
|
+
json[:artifacts] = (json.dig(:artifacts) || []).map { |artifact_| artifact_.dig(:data) }
|
18
|
+
json[:tags] = (json.dig(:tags) || []).map { |tag_| tag_.dig(:name) }
|
19
|
+
json
|
20
|
+
end
|
39
21
|
end
|
40
22
|
end
|
41
23
|
end
|
@@ -23,6 +23,10 @@ module Mihari
|
|
23
23
|
raise NotImplementedError, "You must implement #{self.class}##{__method__}"
|
24
24
|
end
|
25
25
|
|
26
|
+
def source
|
27
|
+
self.class.to_s.split("::").last
|
28
|
+
end
|
29
|
+
|
26
30
|
# @return [Array<String>]
|
27
31
|
def tags
|
28
32
|
[]
|
@@ -37,7 +41,7 @@ module Mihari
|
|
37
41
|
end
|
38
42
|
|
39
43
|
def run_emitter(emitter)
|
40
|
-
emitter.run(title: title, description: description, artifacts: unique_artifacts, tags: tags)
|
44
|
+
emitter.run(title: title, description: description, artifacts: unique_artifacts, source: source, tags: tags)
|
41
45
|
rescue StandardError => e
|
42
46
|
puts "Emission by #{emitter.class} is failed: #{e}"
|
43
47
|
end
|
@@ -48,32 +52,16 @@ module Mihari
|
|
48
52
|
|
49
53
|
private
|
50
54
|
|
51
|
-
def the_hive
|
52
|
-
@the_hive ||= TheHive.new
|
53
|
-
end
|
54
|
-
|
55
|
-
def cache
|
56
|
-
@cache ||= Cache.new
|
57
|
-
end
|
58
|
-
|
59
55
|
# @return [Array<Mihari::Artifact>]
|
60
56
|
def normalized_artifacts
|
61
57
|
@normalized_artifacts ||= artifacts.compact.uniq.sort.map do |artifact|
|
62
|
-
artifact.is_a?(Artifact) ? artifact : Artifact.new(artifact)
|
58
|
+
artifact.is_a?(Artifact) ? artifact : Artifact.new(data: artifact)
|
63
59
|
end.select(&:valid?)
|
64
60
|
end
|
65
61
|
|
66
|
-
def uncached_artifacts
|
67
|
-
@uncached_artifacts ||= normalized_artifacts.reject do |artifact|
|
68
|
-
cache.cached? artifact.data
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
62
|
# @return [Array<Mihari::Artifact>]
|
73
63
|
def unique_artifacts
|
74
|
-
|
75
|
-
|
76
|
-
@unique_artifacts ||= the_hive.artifact.find_non_existing_artifacts(uncached_artifacts)
|
64
|
+
@unique_artifacts ||= normalized_artifacts.select(&:unique?)
|
77
65
|
end
|
78
66
|
|
79
67
|
def set_unique_artifacts
|
@@ -6,12 +6,14 @@ module Mihari
|
|
6
6
|
attr_accessor :title
|
7
7
|
attr_reader :description
|
8
8
|
attr_reader :artifacts
|
9
|
+
attr_reader :source
|
9
10
|
attr_reader :tags
|
10
11
|
|
11
|
-
def initialize(title:, description:, artifacts:, tags: [])
|
12
|
+
def initialize(title:, description:, artifacts:, source:, tags: [])
|
12
13
|
@title = title
|
13
14
|
@description = description
|
14
15
|
@artifacts = artifacts
|
16
|
+
@source = source
|
15
17
|
@tags = tags
|
16
18
|
end
|
17
19
|
end
|
@@ -86,11 +86,11 @@ module Mihari
|
|
86
86
|
end
|
87
87
|
|
88
88
|
def config_keys
|
89
|
-
%w(
|
89
|
+
%w(censys_id censys_secret)
|
90
90
|
end
|
91
91
|
|
92
92
|
def api
|
93
|
-
@api ||= ::Censys::API.new
|
93
|
+
@api ||= ::Censys::API.new(Mihari.config.censys_id, Mihari.config.censys_secret)
|
94
94
|
end
|
95
95
|
end
|
96
96
|
end
|
@@ -27,11 +27,11 @@ module Mihari
|
|
27
27
|
private
|
28
28
|
|
29
29
|
def config_keys
|
30
|
-
%w(
|
30
|
+
%w(circl_passive_password circl_passive_username)
|
31
31
|
end
|
32
32
|
|
33
33
|
def api
|
34
|
-
@api ||= ::PassiveCIRCL::API.new
|
34
|
+
@api ||= ::PassiveCIRCL::API.new(username: Mihari.config.circl_passive_username, password: Mihari.config.circl_passive_password)
|
35
35
|
end
|
36
36
|
|
37
37
|
def lookup
|
@@ -35,15 +35,15 @@ module Mihari
|
|
35
35
|
PAGE_SIZE = 10
|
36
36
|
|
37
37
|
def config_keys
|
38
|
-
%w(
|
38
|
+
%w(onyphe_api_key)
|
39
39
|
end
|
40
40
|
|
41
41
|
def api
|
42
|
-
@api ||= ::Onyphe::API.new
|
42
|
+
@api ||= ::Onyphe::API.new(Mihari.config.onyphe_api_key)
|
43
43
|
end
|
44
44
|
|
45
45
|
def search_with_page(query, page: 1)
|
46
|
-
api.datascan(query, page: page)
|
46
|
+
api.simple.datascan(query, page: page)
|
47
47
|
end
|
48
48
|
|
49
49
|
def search
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "otx_ruby"
|
4
|
+
|
5
|
+
module Mihari
|
6
|
+
module Analyzers
|
7
|
+
class OTX < Base
|
8
|
+
attr_reader :query
|
9
|
+
attr_reader :type
|
10
|
+
|
11
|
+
attr_reader :title
|
12
|
+
attr_reader :description
|
13
|
+
attr_reader :tags
|
14
|
+
|
15
|
+
def initialize(query, title: nil, description: nil, tags: [])
|
16
|
+
super()
|
17
|
+
|
18
|
+
@query = query
|
19
|
+
@type = TypeChecker.type(query)
|
20
|
+
|
21
|
+
@title = title || "OTX lookup"
|
22
|
+
@description = description || "query = #{query}"
|
23
|
+
@tags = tags
|
24
|
+
end
|
25
|
+
|
26
|
+
def artifacts
|
27
|
+
lookup || []
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def config_keys
|
33
|
+
%w(otx_api_key)
|
34
|
+
end
|
35
|
+
|
36
|
+
def domain_client
|
37
|
+
@domain_client ||= ::OTX::Domain.new(Mihari.config.otx_api_key)
|
38
|
+
end
|
39
|
+
|
40
|
+
def ip_client
|
41
|
+
@ip_client ||= ::OTX::IP.new(Mihari.config.otx_api_key)
|
42
|
+
end
|
43
|
+
|
44
|
+
def valid_type?
|
45
|
+
%w(ip domain).include? type
|
46
|
+
end
|
47
|
+
|
48
|
+
def lookup
|
49
|
+
case type
|
50
|
+
when "domain"
|
51
|
+
domain_lookup
|
52
|
+
when "ip"
|
53
|
+
ip_lookup
|
54
|
+
else
|
55
|
+
raise InvalidInputError, "#{query}(type: #{type || 'unknown'}) is not supported." unless valid_type?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def domain_lookup
|
60
|
+
records = domain_client.get_passive_dns(query)
|
61
|
+
records.map do |record|
|
62
|
+
record.address if record.record_type == "A"
|
63
|
+
end.compact.uniq
|
64
|
+
end
|
65
|
+
|
66
|
+
def ip_lookup
|
67
|
+
records = ip_client.get_passive_dns(query)
|
68
|
+
records.map do |record|
|
69
|
+
record.hostname if record.record_type == "A"
|
70
|
+
end.compact.uniq
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -14,6 +14,7 @@ module Mihari
|
|
14
14
|
|
15
15
|
ANALYZERS = [
|
16
16
|
Mihari::Analyzers::CIRCL,
|
17
|
+
Mihari::Analyzers::OTX,
|
17
18
|
Mihari::Analyzers::PassiveTotal,
|
18
19
|
Mihari::Analyzers::Pulsedive,
|
19
20
|
Mihari::Analyzers::SecurityTrails,
|
@@ -55,7 +56,7 @@ module Mihari
|
|
55
56
|
analyzer.artifacts
|
56
57
|
rescue ArgumentError, InvalidInputError => _e
|
57
58
|
nil
|
58
|
-
rescue ::PassiveCIRCL::Error, ::PassiveTotal::Error, ::Pulsedive::ResponseError, ::SecurityTrails::Error, ::VirusTotal::Error => _e
|
59
|
+
rescue Faraday::Error, ::PassiveCIRCL::Error, ::PassiveTotal::Error, ::Pulsedive::ResponseError, ::SecurityTrails::Error, ::VirusTotal::Error => _e
|
59
60
|
nil
|
60
61
|
end
|
61
62
|
end
|
@@ -30,11 +30,11 @@ module Mihari
|
|
30
30
|
private
|
31
31
|
|
32
32
|
def config_keys
|
33
|
-
%w(
|
33
|
+
%w(passivetotal_username passivetotal_api_key)
|
34
34
|
end
|
35
35
|
|
36
36
|
def api
|
37
|
-
@api ||= ::PassiveTotal::API.new
|
37
|
+
@api ||= ::PassiveTotal::API.new(username: Mihari.config.passivetotal_username, api_key: Mihari.config.passivetotal_api_key)
|
38
38
|
end
|
39
39
|
|
40
40
|
def valid_type?
|
@@ -30,11 +30,11 @@ module Mihari
|
|
30
30
|
private
|
31
31
|
|
32
32
|
def config_keys
|
33
|
-
%w(
|
33
|
+
%w(pulsedive_api_key)
|
34
34
|
end
|
35
35
|
|
36
36
|
def api
|
37
|
-
@api ||= ::Pulsedive::API.new
|
37
|
+
@api ||= ::Pulsedive::API.new(Mihari.config.pulsedive_api_key)
|
38
38
|
end
|
39
39
|
|
40
40
|
def valid_type?
|
@@ -30,11 +30,11 @@ module Mihari
|
|
30
30
|
private
|
31
31
|
|
32
32
|
def config_keys
|
33
|
-
%w(
|
33
|
+
%w(securitytrails_api_key)
|
34
34
|
end
|
35
35
|
|
36
36
|
def api
|
37
|
-
@api ||= ::SecurityTrails::API.new
|
37
|
+
@api ||= ::SecurityTrails::API.new(Mihari.config.securitytrails_api_key)
|
38
38
|
end
|
39
39
|
|
40
40
|
def valid_type?
|
@@ -32,11 +32,11 @@ module Mihari
|
|
32
32
|
private
|
33
33
|
|
34
34
|
def config_keys
|
35
|
-
%w(
|
35
|
+
%w(securitytrails_api_key)
|
36
36
|
end
|
37
37
|
|
38
38
|
def api
|
39
|
-
@api ||= ::SecurityTrails::API.new
|
39
|
+
@api ||= ::SecurityTrails::API.new(Mihari.config.securitytrails_api_key)
|
40
40
|
end
|
41
41
|
|
42
42
|
def valid_type?
|
@@ -36,11 +36,11 @@ module Mihari
|
|
36
36
|
PAGE_SIZE = 100
|
37
37
|
|
38
38
|
def config_keys
|
39
|
-
%w(
|
39
|
+
%w(shodan_api_key)
|
40
40
|
end
|
41
41
|
|
42
42
|
def api
|
43
|
-
@api ||= ::Shodan::API.new
|
43
|
+
@api ||= ::Shodan::API.new(key: Mihari.config.shodan_api_key)
|
44
44
|
end
|
45
45
|
|
46
46
|
def search_with_page(query, page: 1)
|
@@ -30,11 +30,11 @@ module Mihari
|
|
30
30
|
private
|
31
31
|
|
32
32
|
def config_keys
|
33
|
-
%w(
|
33
|
+
%w(virustotal_api_key)
|
34
34
|
end
|
35
35
|
|
36
36
|
def api
|
37
|
-
@api = ::VirusTotal::API.new
|
37
|
+
@api = ::VirusTotal::API.new(key: Mihari.config.virustotal_api_key)
|
38
38
|
end
|
39
39
|
|
40
40
|
def valid_type?
|
@@ -41,11 +41,11 @@ module Mihari
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def config_keys
|
44
|
-
%w(
|
44
|
+
%w(zoomeye_password zoomeye_username)
|
45
45
|
end
|
46
46
|
|
47
47
|
def api
|
48
|
-
@api ||= ::ZoomEye::API.new
|
48
|
+
@api ||= ::ZoomEye::API.new(username: Mihari.config.zoomeye_username, password: Mihari.config.zoomeye_password)
|
49
49
|
end
|
50
50
|
|
51
51
|
def convert_responses(responses)
|
data/lib/mihari/cli.rb
CHANGED
@@ -164,6 +164,16 @@ module Mihari
|
|
164
164
|
end
|
165
165
|
end
|
166
166
|
|
167
|
+
desc "otx [IP|DOMAIN]", "OTX lookup by an IP or domain"
|
168
|
+
method_option :title, type: :string, desc: "title"
|
169
|
+
method_option :description, type: :string, desc: "description"
|
170
|
+
method_option :tags, type: :array, desc: "tags"
|
171
|
+
def otx(domain)
|
172
|
+
with_error_handling do
|
173
|
+
run_analyzer Analyzers::OTX, query: refang(domain), options: options
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
167
177
|
desc "passive_dns [IP|DOMAIN]", "Cross search with passive DNS services by an ip or domain"
|
168
178
|
method_option :title, type: :string, desc: "title"
|
169
179
|
method_option :description, type: :string, desc: "description"
|
@@ -242,17 +252,22 @@ module Mihari
|
|
242
252
|
artifacts = json.dig("artifacts")
|
243
253
|
tags = json.dig("tags") || []
|
244
254
|
|
245
|
-
basic = Analyzers::Basic.new(title: title, description: description, artifacts: artifacts, tags: tags)
|
255
|
+
basic = Analyzers::Basic.new(title: title, description: description, artifacts: artifacts, source: "json", tags: tags)
|
246
256
|
basic.run
|
247
257
|
end
|
248
258
|
end
|
249
259
|
|
250
260
|
desc "alerts", "Show the alerts on TheHive"
|
251
261
|
method_option :limit, type: :string, default: "5", desc: "Number of alerts to show (or 'all' to show all the alerts)"
|
262
|
+
method_option :title, type: :string, desc: "Title to filter"
|
263
|
+
method_option :source, type: :string, desc: "Source to filter"
|
264
|
+
method_option :tag, type: :string, desc: "Tag to filter"
|
252
265
|
def alerts
|
253
266
|
with_error_handling do
|
254
|
-
|
255
|
-
|
267
|
+
load_configuration
|
268
|
+
|
269
|
+
viewer = AlertViewer.new
|
270
|
+
alerts = viewer.list(limit: options["limit"], title: options["title"], source: options["source"], tag: options[:tag])
|
256
271
|
puts JSON.pretty_generate(alerts)
|
257
272
|
end
|
258
273
|
end
|
@@ -261,6 +276,7 @@ module Mihari
|
|
261
276
|
def status
|
262
277
|
with_error_handling do
|
263
278
|
load_configuration
|
279
|
+
|
264
280
|
puts JSON.pretty_generate(Status.check)
|
265
281
|
end
|
266
282
|
end
|
@@ -286,7 +302,10 @@ module Mihari
|
|
286
302
|
|
287
303
|
def load_configuration
|
288
304
|
config = options["config"]
|
289
|
-
|
305
|
+
return unless config
|
306
|
+
|
307
|
+
Config.load_from_yaml(config)
|
308
|
+
Database.connect
|
290
309
|
end
|
291
310
|
|
292
311
|
def run_analyzer(analyzer_class, query:, options:)
|