mihari 0.17.5 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:)
|